How to get numeric value of Months between two dates in PostgreSQL? - sql

I have two dates in format Time Stamp Without Time Zone.
I want to compare them and get the numeric value of months between them:
select age(NOW(), '2012-03-24 14:44:55.454041+03')
Gives:
4 years 9 mons 2 days 21:00:27.165482
The trick here is that I need to convert this result into one value of months.
So:
In order to convert the YEARS to Months:
select EXTRACT(YEAR FROM age) * 12 + EXTRACT(MONTH FROM age)
FROM age(NOW(), '2012-06-24 14:44:55.454041+03') AS t(age)
I get 57 which is 4*12+9.
My problem is that I don't know how to convert the days.
In the above example I need to convert the '2 days' into it's value in months.
'2 days' isn't 0 months!
In Months of 30 days 15 days are 0.5 months.
How can I do that?
The final result should be 57.something

You can get a rough estimation with:
select (extract(epoch from timestamptz '2012-06-24 14:44:55.454041+03')
- extract(epoch from timestamptz '2017-03-27 00:00:00+03'))
/ extract(epoch from interval '30.44 days') rough_estimation
(you can divide with extract(epoch from interval '1 month') for an even more rough estimation).
The problem with your original formula is that it is designed to give a complete month difference between two dates. If you want to account days too an interesting problem arises: in your example, the result should be 57 months and 2 days 21:00:27.165482. But in what month should the 2 days 21:00:27.165482 part is calculated? An average-length month (30.44 days)? If you want to be precise, it should be noted that in your example case, the difference is really only 56 months, plus almost 7 days in 2012-06 (which had 30 days) and 27 days in 2017-03 (which has 31 days). The question you should ask yourself: is it really worth an advanced formula which takes account both range ends' days-in-a-month or not?
Edit: For completeness, here is a function, which can take both range end into consideration:
create or replace function abs_month_diff(timestamptz, timestamptz)
returns numeric
language sql
stable
as $func$
select extract(year from age)::numeric * 12 + extract(month from age)::numeric
+ (extract(epoch from (lt + interval '1 month' - l))::numeric / extract(epoch from (lt + interval '1 month' - lt))::numeric)
+ (extract(epoch from (g - gt))::numeric / extract(epoch from (gt + interval '1 month' - gt))::numeric)
- case when gt <= l or lt = l then 1 else 0 end
from least($1, $2) l,
greatest($1, $2) g,
date_trunc('month', l) lt,
date_trunc('month', g) gt,
age(gt, l)
$func$;
(Note: if you use timestamp instead of timestamptz, this function is immutable instead of stable. Just like the date_trunc functions.)
So:
select age('2017-03-27 00:00:00+03', '2012-06-24 14:44:55.454041+03'),
abs_month_diff('2017-03-27 00:00:00+03', '2012-06-24 14:44:55.454041+03');
will yield:
age | abs_month_diff
---------------------------------------+-------------------------
4 years 9 mons 2 days 09:15:04.545959 | 57.05138456751051843959
http://rextester.com/QLABV31257 (outdated)
Edit: function is corrected to produce exact results when the difference is less than a month.
See f.ex:
set time zone 'utc';
select abs_month_diff('2017-02-27 00:00:00+03', '2017-02-24 00:00:00+03'), 3.0 / 28,
abs_month_diff('2017-03-27 00:00:00+03', '2017-03-24 00:00:00+03'), 3.0 / 31,
abs_month_diff('2017-04-27 00:00:00+03', '2017-04-24 00:00:00+03'), 3.0 / 30,
abs_month_diff('2017-02-27 00:00:00+00', '2017-03-27 00:00:00+00'), 2.0 / 28 + 26.0 / 31;
http://rextester.com/TIYQC5325 (outdated)
Edit 2: This function is based on the following formula, to calculate the length of a month:
select (mon + interval '1 month' - mon)
from date_trunc('month', now()) mon
This will even take DST changes into account. F.ex. in my country there was a DST change yesterday (on 2017-03-26), so today (2017-03-27) the above query reports: 30 days 23:00:00.
Edit 3: Function is corrected again (thanks to #Jonathan who noticed an edge-case of an edge-case).
http://rextester.com/JLG68351

You can obtain an approximate value with something similar to this:
SELECT A, B*12+C-1+E/30 MONTHSBETWEEN
FROM (
SELECT current_timestamp A, EXTRACT(YEAR FROM current_timestamp) B, EXTRACT(MONTH FROM current_timestamp) C, EXTRACT(DAYS FROM current_timestamp) E
) X;
or better precision with something like this:
SELECT A, B*12+C-1+E/ DATE_PART('days',
DATE_TRUNC('month', A)
+ '1 MONTH'::INTERVAL
- '1 DAY'::INTERVAL
) MONTHSBETWEEN
FROM (
SELECT current_timestamp A, EXTRACT(YEAR FROM current_timestamp) B, EXTRACT(MONTH FROM current_timestamp) C, EXTRACT(DAYS FROM current_timestamp) E
) X;

This should do:
select (EXTRACT(YEAR FROM age) * 12 + EXTRACT(MONTH FROM age) + EXTRACT(DAY FROM age) / 30)::numeric
FROM age(NOW(), '2012-06-24 14:44:55.454041+03') AS t(age)
You can also add ROUND() to make it prettier:
select ROUND((EXTRACT(YEAR FROM age) * 12 + EXTRACT(MONTH FROM age) + EXTRACT(DAY FROM age) / 30)::numeric,2) as Months
FROM age(NOW(), '2012-06-24 14:44:55.454041+03') AS t(age)

Related

How to get number of days in month based on date?

Is there a way to use extract from date in format YYYY-MM-DD how many days were in this month?
example:
for 2016-02-05 it will give 29 (Feb 2016 has 29 days)
for 2016-03-12 it will give 31
for 2015-02-05 it will give 28 (Feb 2015 had 28 days)
I'm using PostgreSQL
EDIT:
LAST_DAY function in postgres is not what i'm looking for. it returns DATE while I expect an Integer
One way to achieve this would be to subtract the beginning of the following month from the beginning of the current month:
db=> SELECT DATE_TRUNC('MONTH', '2016-02-05'::DATE + INTERVAL '1 MONTH') -
DATE_TRUNC('MONTH', '2016-02-05'::DATE);
?column?
----------
29 days
(1 row)
Just needed this today and seems that I came up with pretty much the same as Mureinik, just that I needed it numeric. (PostgreSQL couldn't convert from interval to number directly)
previous month:
select CAST(to_char(date_trunc('month', current_date) - (date_trunc('month', current_date) - interval '1 month'),'dd') as integer)
current month:
select CAST(to_char(date_trunc('month', current_date) + interval '1 month' - date_trunc('month', current_date), 'dd') as integer)
You can try next:
SELECT
DATE_PART('days',
DATE_TRUNC('month', TO_DATE('2016-02-05', 'YYYY-MM-DD'))
+ '1 MONTH'::INTERVAL
- DATE_TRUNC('month', TO_DATE('2016-02-05', 'YYYY-MM-DD'))
);
Note: there date is used twice. And used convert function TO_DATE

postgres query to check for records not falling between two time periods using the ISO WEEK number and year from DB

i do have a query which works fine but I was just wondering if there are other ways or alternate method to bettter this.
I have a table where i am fetching those records exceeding or do not fall between 1 year time interval however there is only the year and ISO week number column in the table (integer values).
basically the logic is to check ISO WEEK - YEAR falls between 'current_date - interval '1 year' AND current_date.
My query is as below :
select * from raj_weekly_records where
(date_dimension_week > extract(week from current_date) and date_dimension_year = extract(year from current_date) )
or (date_dimension_week < extract(week from current_date) and (extract(year from current_date)-date_dimension_year=1) )
or(extract(year from current_date)-date_dimension_year>1);
Here date_dimension_week and date_dimension_year are the only integer parameters by which I need to check is there any other alternate or better way?.This code is working fine no issues here.
Here is an idea. Convert the year/week to a numeric format: YYYYWW. That is, the year times 100 plus the week number. Then you can do the logic with a single comparison:
select *
from raj_weekly_records
where date_dimension_year * 100 + date_dimension_week
not between (extract(year from current_date) - 1) * 100 + extract(week from current_date) and
extract(year from current_date) * 100 + extract(week from current_date)
(There might be an off-by one error, depending on whether the weeks at the ends are included or excluded.)
select *
from raj_weekly_records
where
date_trunc('week',
'0001-01-01 BC'::date + date_dimension_year * interval '1 year'
)
+ (date_dimension_week + 1) * interval '1 week'
- interval '1 day'
not between
current_date - interval '1 year' and current_date

Postgresql get first and last day of all iso week in a given year

select week_num, week_start, week_end,to_char(week_start,'dd Dy Mon yyyy'), to_char(week_end,'dd Dy Mon yyyy') from(
WITH RECURSIVE t(n) AS (
select (date_trunc('week',(date_trunc('week',(2016 || '-01-04')::date)::date - interval '1 day')::date))::date
UNION ALL
SELECT (n - interval '1 week')::date FROM t WHERE extract(WEEK from n ) > 1
)
SELECT n as week_start, (n + interval '6 days')::date as week_end, extract(WEEK from n ) as week_num
FROM t
) as weeks order by week_num
i wrote this postgresl script to get the first and last day of all iso week in a given year. It is working perfectly, i just need to know if it can be improved
You can use generate_series() to avoid the convoluted CTE and date arithmetics. Here's an example to get you started:
select d, d + interval '6 days'
from generate_series('2016-01-01'::date, '2016-12-31'::date, '1 day'::interval) d
where date_trunc('week', d) = d
You'll want to add a case in the second term to strip out anything in 2017, and it could be rewritten to step a week at a time, but it should get you on the right track.

How do I determine the last day of the previous month using PostgreSQL?

I need to query a PostgreSQL database to determine records that fall within today's date and the last day of the previous month. In other words, I'd like to retrieve everything that falls between December 31, 2011 and today. This query will be re-used each month, so next month, the query will be based upon the current date and January 31, 2012.
I've seen this option, but I'd prefer to avoid using a function (if possible).
Both solutions include the last day of the previous month and also include all of "today".
For a date column:
SELECT *
FROM tbl
WHERE my_date BETWEEN date_trunc('month', now())::date - 1
AND now()::date
You can subtract plain integer values from a date (but not from a timestamp) to subtract days. This is the simplest and fastest way.
For a timestamp column:
SELECT *
FROM tbl
WHERE my_timestamp >= date_trunc('month', now()) - interval '1 day'
AND my_timestamp < date_trunc('day' , now()) + interval '1 day'
I use the < operator for the second condition to get precise results (read: "before tomorrow").
I do not cast to date in the second query. Instead I add an interval '1 day', to avoid casting back and forth.
Have a look at date / time types and functions in the manual.
For getting date of previous/last month:
SELECT (date_trunc('month', now())::date - 1) as last_month_date
Result: 2012-11-30
For getting number of days of previous/last month:
SELECT DATE_PART('days', date_trunc('month', now())::date - 1) last_month_days
Result: 30
Try this:
SELECT ...
WHERE date_field between (date_trunc('MONTH', now()) - INTERVAL '1 day')::date
and now()::date
...
Try
select current_date - cast((date_part('day', current_date) + 1) as int)
take from http://wiki.postgresql.org/wiki/Date_LastDay, and modified to return just the days in a month
CREATE OR REPLACE FUNCTION calc_days_in_month(date)
RETURNS double precision AS
$$
SELECT EXTRACT(DAY FROM (date_trunc('MONTH', $1) + INTERVAL '1 MONTH - 1 day')::date);
$$ LANGUAGE 'sql' IMMUTABLE STRICT;
select calc_days_in_month('1999-05-01')
returns 31
Reference is taken from this blog:
You can use below function:
CREATE OR REPLACE FUNCTION fn_GetLastDayOfMonth(DATE)
RETURNS DATE AS
$$
SELECT (date_trunc('MONTH', $1) + INTERVAL '1 MONTH - 1 day')::DATE;
$$ LANGUAGE 'sql'
IMMUTABLE STRICT;
Sample executions:
SELECT *FROM fn_GetLastDayOfMonth(NOW()::DATE);

Postgresql generate_series of months

I'm trying to generate a series in PostgreSQL with the generate_series function. I need a series of months starting from Jan 2008 until current month + 12 (a year out). I'm using and restricted to PostgreSQL 8.3.14 (so I don't have the timestamp series options in 8.4).
I know how to get a series of days like:
select generate_series(0,365) + date '2008-01-01'
But I am not sure how to do months.
select DATE '2008-01-01' + (interval '1' month * generate_series(0,11))
Edit
If you need to calculate the number dynamically, the following could help:
select DATE '2008-01-01' + (interval '1' month * generate_series(0,month_count::int))
from (
select extract(year from diff) * 12 + extract(month from diff) + 12 as month_count
from (
select age(current_timestamp, TIMESTAMP '2008-01-01 00:00:00') as diff
) td
) t
This calculates the number of months since 2008-01-01 and then adds 12 on top of it.
But I agree with Scott: you should put this into a set returning function, so that you can do something like select * from calc_months(DATE '2008-01-01')
You can interval generate_series like this:
SELECT date '2014-02-01' + interval '1' month * s.a AS date
FROM generate_series(0,3,1) AS s(a);
Which would result in:
date
---------------------
2014-02-01 00:00:00
2014-03-01 00:00:00
2014-04-01 00:00:00
2014-05-01 00:00:00
(4 rows)
You can also join in other tables this way:
SELECT date '2014-02-01' + interval '1' month * s.a AS date, t.date, t.id
FROM generate_series(0,3,1) AS s(a)
LEFT JOIN <other table> t ON t.date=date '2014-02-01' + interval '1' month * s.a;
You can interval generate_series like this:
SELECT TO_CHAR(months, 'YYYY-MM') AS "dateMonth"
FROM generate_series(
'2008-01-01' :: DATE,
'2008-06-01' :: DATE ,
'1 month'
) AS months
Which would result in:
dateMonth
-----------
2008-01
2008-02
2008-03
2008-04
2008-05
2008-06
(6 rows)
Well, if you only need months, you could do:
select extract(month from days)
from(
select generate_series(0,365) + date'2008-01-01' as days
)dates
group by 1
order by 1;
and just parse that into a date string...
But since you know you'll end up with months 1,2,..,12, why not just go with select generate_series(1,12);?
In the generated_series() you can define the step, which is one month in your case. So, dynamically you can define the starting date (i.e. 2008-01-01), the ending date (i.e. 2008-01-01 + 12 months) and the step (i.e. 1 month).
SELECT generate_series('2008-01-01', '2008-01-01'::date + interval '12 month', '1 month')::date AS generated_dates
and you get
1/1/2008
2/1/2008
3/1/2008
4/1/2008
5/1/2008
6/1/2008
7/1/2008
8/1/2008
9/1/2008
10/1/2008
11/1/2008
12/1/2008
1/1/2009