postgres '1 year' equals '360 days'? - postgresql

Am wondering if anyone else has encountered this or knows information about it.
Today is November 3, 2014 and if i check whether or not November 5, 2013 is within the last year i get different answers depending on how i check: 1 year versus 365 days
select now() - '20131105' as diff,
case when now() - '20131105' <= '1 year' then 'within year' else 'not within year' end as yr_check,
case when now() - '20131105' <= '365 days' then 'within 365 days' else 'not within 365 days' end as day_check
2014-11-03 16:27:38.39669-06; 363 days 16:27:38.39669; not within year; within 365 days
Looks like when querying against November 9 tho, it's ok
select now() as right_now, now() - '20131109' as diff,
case when now() - '20131109' <= '1 year' then 'within year' else 'not within year' end as yr_check,
case when now() - '20131109' <= '365 days' then 'within 365 days' else 'not within 365 days' end as day_check
2014-11-03 16:31:12.464469-06; 359 days 16:31:12.464469; within year; within 365 days
anyone have an idea about this? or is there something about date arithmetic that's funny?
postgres version is 9.2.4

or is there something about date arithmetic that's funny?
It's funny alright, but not in the way that makes you laugh.
Twelve months has to equal a year doesn't it?
=> SELECT '12 months'::interval = '1 year'::interval;
?column?
----------
t
Good. Makes sense. Hmm - wonder how long a month is.
=> SELECT '30 days'::interval = '1 month'::interval;
?column?
----------
t
Fair enough. Suppose they had to pick something.
Hmm - but that means...
=> SELECT '360 days'::interval = '12 months'::interval;
?column?
----------
t
Which seems to imply...
=> SELECT '360 days'::interval = '1 year'::interval;
?column?
----------
t
That can't be right! What they need to do is have a month equal to 30.41666 days. No hang on, what about leap years? Hmm - does this affect weeks? AARGH!
Basically, you can't convert sensibly between time units. There aren't 60 seconds in a minute, or 24 hours in a day, 52 weeks in a year or even 365 days. Unfortunately, humans (particularly customer-shaped humans) like converting between time units so we end up with a mess like this.
PostgreSQL's system is no more loony than any other and in fact is better than most.

I'm not sure what is real problem with this check, but it works other way around:
select now() - interval '1 year' <= date '2013-11-05'
I'm no expert in Postgres, but it can be something with type comparisons, because:
select pg_typeof(now() - date '2013-11-05'),
pg_typeof(now() - interval '1 year')
yields result:
interval, timestamp with time zone
so your example compares interval with interval, but for different scales - days vs year, and my solution compares timestamp with date, which seems to work
UPDATE:
You can check that interval '1 year' when not attached to year (not added to date or timestamp) equals to 360 days:
select interval '1 year' <= interval '359 days',
interval '1 year' <= interval '360 days'
which yields:
f, t
From my understanding you can't just compare random year interval when you don't know year it is attached - always compare dates, and just use interval to create new date object.
select now() - interval '1 year' <= now() - interval '365 days'
t

From www.postgresql.org/docs/current/static/datatype-datetime.html:
Internally interval values are stored as months, days, and seconds. This is done because the number of days in a month varies, and a day can have 23 or 25 hours if a daylight savings time adjustment is involved. The months and days fields are integers while the seconds field can store fractions. Because intervals are usually created from constant strings or timestamp subtraction, this storage method works well in most cases. Functions justify_days and justify_hours are available for adjusting days and hours that overflow their normal ranges.
Because you compare two intervals, PostgreSQL internally normalizes values (like justify_interval()), before comparing:
SELECT INTERVAL '31 days' > INTERVAL '1 mon' -- yields 't'
But, if you apply interval substraction/addition, varying day & month length taken into consideration:
SELECT (timestamptz '2014-11-03 00:00:00 America/New_York' - INTERVAL '1 day') AT TIME ZONE 'America/New_York',
timestamptz '2014-11-03 00:00:00 America/New_York' - timestamptz '2014-11-02 00:00:00 America/New_York' <= interval '1 day';
-- | timestamp | boolean |
-- +---------------------+---------+
-- | 2014-11-02 01:00:00 | f |
So, if you need to test, whether a timestamp/date is within a range, you should manipulate timestampts/dates (or use timestamp/date ranges) & compare those values with <, > or BETWEEN.
SELECT timestamp '2014-11-03 00:00:00' - timestamp '2014-10-03 00:00:00' <= interval '1 mon',
timestamp '2014-11-03 00:00:00' - interval '1 mon' <= timestamp '2014-10-03 00:00:00';
-- | boolean | boolean |
-- +---------+---------+
-- | f | t |

Related

How are months intervals internally calculated in Postgres?

In PostgreSQL, the interval of '1 month' sometimes counts as 30 days and sometimes counts as 31 days. What are the criteria used to determine this?
I ran the below query to demonstrate my confusion.
select
now() - interval '1 month'
, now() - interval '30 days'
, interval '30 days' = interval '1 month'
, interval '31 days' = interval '1 month'
The query returns:
2022-03-27 21:09:30.933434+00 | 2022-03-28 21:09:30.933434+00 | true | false
I would expect the query to return both days on March 28th, since an interval of one month is equal to an interval of 30 days.
It comes down to the specific vs the general where day is the specific and month is not. The same happens with day and hour as in:
select '2022-03-13 12:00 PDT'::timestamptz - '1 day'::interval;
?column?
------------------------
2022-03-12 12:00:00-08
select '2022-03-13 12:00 PDT'::timestamptz - '24 hours'::interval;
?column?
------------------------
2022-03-12 11:00:00-08
DST occurred morning of 2022-03-13 in PST/PDT. So a day is generalized to the same time a day ago whereas 24 hours ago is actually 24 hours passing.
In your case:
select
now() - interval '1 month'
, now() - interval '30 days';
?column? | ?column?
-------------------------------+-------------------------------
2022-03-27 14:44:33.515669-07 | 2022-03-28 14:44:33.515669-07
The 1 month is going to go back to the same date and time one month back, whereas 30 days is going back an actual 30 days.
In this case:
select '2022-03-30 21:17:05'::timestamp - interval '1 month' ;
?column?
---------------------
2022-02-28 21:17:05
There is no day 30 in February so it goes to the actual end of the month the 28th.

How to calculate end of the month in Postgres?

How to calculate end of the month in Postgres? I have table with column date datatype. I want to calculate end of the month of every date. For Eg. In the table there values like "2015-07-10 17:52:51","2015-05-30 11:30:19" then end of the month should be like 31 July 2015,31 May 2015.
Please guide me in this.
How about truncating to the beginning of this month, jumping forward one month, then back one day?
=# select (date_trunc('month', now()) + interval '1 month - 1 day')::date;
date
------------
2015-07-31
(1 row)
Change now() to your date variable, which must be a timestamp, per the docs. You can then manipulate this output (with strftime, etc.) to any format you need.
Source
SELECT TO_CHAR(
DATE_TRUNC('month', CURRENT_DATE)
+ INTERVAL '1 month'
- INTERVAL '1 day',
'YYYY-MM-DD HH-MM-SS'
) endOfTheMonth
Hi I tried like this and it worked
Date(to_char(date_trunc('month'::text, msm013.msa011) + '1 mon - 1 day '::interval , 'DD-MON-YYYY') )
Thanks a lot!!

How to get last 24 hrs data in postgreSQL

I want to get last 24 hrs data. i wrote a query in postgreSQL as follows. But I couldn't get the answer as i expected.
SELECT startdate::timestamp AS startdate,
(DATE_PART('hour',startdate::timestamp)::integer) as hrs,count(guorderid)
FROM ord_entitlement
WHERE DATE_PART('Day',CURRENT_DATE::timestamp - startdate::timestamp) < 1
AND DATE_PART('hour',startdate::timestamp) <= 24
GROUP BY hrs,startdate
ORDER BY startdate
Instead of checking date parts, do the time math to get an interval. Use NOW() to get a timestamptz.
SELECT startdate::timestamp AS startdate,
(DATE_PART('hour',startdate::timestamp)::integer) as hrs,
count(guorderid)
FROM ord_entitlement
WHERE NOW() > startdate::timestamptz
AND NOW() - startdate::timestamptz <= interval '24 hours'
GROUP BY hrs,startdate
ORDER BY startdate
This ensures you will get the last 24 hours no matter what your time zone or daylight savings says. NOW() > startdate::timestamptz ensures you don't accidentally pick up things from the future.
If you use CURRENT_DATE you will not get the time instead use now() function. Try the following,
SELECT startdate::timestamp AS startdate,
(DATE_PART('hour',startdate::timestamp)::integer) as hrs,count(guorderid)
FROM ord_entitlement
WHERE DATE_PART('Day',now() - startdate::timestamptz) < 1
GROUP BY hrs,startdate
ORDER BY startdate
date_part() works like extract(), i.e. they will extract a subfield from the source:
-- they will both yield 9 as result
select date_part('day', date '2015-01-09') "day part of 2015-01-09",
date_part('day', date '2015-02-09') "day part of 2015-02-09";
Extracting day(s) therefore is not suited to select the last 24 hours. Similarly extracting hour(s) will (almost) always yield less than or equal to 24.
Extraction day(s) from interval (that's the result of substracting 2 timestamps) is a little different. The result might depend on, whether the interval is justified, or not:
-- they will both yield 1 as result
select date_part('day', interval '1 day') "day part of 1 day",
date_part('day', interval '1 month 1 day') "day part of 1 month 1 day";
-- they will yield 1, 32 and 397 respectively
select date_part('day', timestamp '2015-02-09' - timestamp '2015-02-08') "interval 1",
date_part('day', timestamp '2015-02-09' - timestamp '2015-01-08') "interval 2",
date_part('day', timestamp '2015-02-09' - timestamp '2014-01-08') "interval 3";
Depending on the fact, that the timestamp subtraction is not giving justified intervals is not the best option, I think. You could use simpler conditions to achieve your goal:
-- if startdate is a timestamp:
where current_timestamp - interval '1 day' <= startdate
-- if startdate is a date:
where current_date - 1 <= startdate
If you want to disallow future dates too (as your question's title suggests), you could use a single between condition:
-- if startdate is a timestamp:
where startdate between current_timestamp - interval '1 day' and current_timestamp
-- if startdate is a date:
where startdate between current_date - 1 and current_date

postgreSQL - return interval as time type

In a form we have, people will enter an age by first specifying a category (minutes, hours, days, months, years) and then secondarily entering an integer value. The problem with this practice is that ages become inconsistent - 18 months is greater than 1 year, this makes statistical analysis difficult.
Internally I need to store this data not as two integers (as it is currently stored) but as an interval and an integer. The second integer indicates the original unit.
Here is the question: what is the simple method for telling postgreSQL to return this interval back as the time it was entered as? So for instance, the user enters: 18 months. Right now, it will return it as 1 year, 6 months. -- can't redisplay that on the form. I know based on the other integer what they entered it as, so there will be no fractional component - i.e. if they entered it in days, there will be no hours, etc.
Ideally it would be something like
SELECT as_time((person.age).value,'months')
Note that these intervals are entered as-is, so the months would not ever be anything other than the 30-day interval 'months' (representational months). If need be I can use a plpgsql function for it, since I'll need to do a lookup on the time type code; but the strict requirement in all cases is that if they entered some time, they get back exactly that time, no rounding, no 'correction', etc.
WITH x(unit, t) AS (
VALUES
(1, interval '1 month')
,(1, interval '2 month')
,(1, interval '3 month')
,(1, interval '12 month')
,(1, interval '13 month')
,(1, interval '18 month')
,(1, interval '24 month')
,(1, interval '30 month')
,(1, interval '300 month')
,(2, interval '1 year')
,(2, interval '2 year')
,(2, interval '5 year')
,(2, interval '100 year')
)
SELECT CASE unit
WHEN 1 THEN EXTRACT(year FROM t)::int * 12
+ EXTRACT(month FROM t)::int
WHEN 2 THEN EXTRACT(year FROM t)::int
ELSE -1 -- should not occour
END AS units
,CASE unit
WHEN 1 THEN 'months'
WHEN 2 THEN 'years'
ELSE 'unknown' -- should not occour
END as unit
FROM x;
EXTRACT() in the manual.
You could try and extract the epoch value (count of seconds) and convert it - like #Magnus demonstrates here, but months and years are justified for the irregular nature of months, so the calculation would be off with bigger intervals.

How to get the number of days in a month?

I am trying to get the following in Postgres:
select day_in_month(2);
Expected output:
28
Is there any built-in way in Postgres to do that?
SELECT
DATE_PART('days',
DATE_TRUNC('month', NOW())
+ '1 MONTH'::INTERVAL
- '1 DAY'::INTERVAL
)
Substitute NOW() with any other date.
Using the smart "trick" to extract the day part from the last date of the month, as demonstrated by Quassnoi. But it can be a bit simpler / faster:
SELECT extract(days FROM date_trunc('month', now()) + interval '1 month - 1 day');
Rationale
extract is standard SQL, so maybe preferable, but it resolves to the same function internally as date_part(). The manual:
The date_part function is modeled on the traditional Ingres equivalent to the SQL-standard function extract:
But we only need to add a single interval. Postgres allows multiple time units at once. The manual:
interval values can be written using the following verbose syntax:
[#] quantity unit[quantity unit...] [direction]
where quantity is a number (possibly signed); unit is microsecond,
millisecond, second, minute, hour, day, week, month, year, decade,
century, millennium, or abbreviations or plurals of these units;
ISO 8601 or standard SQL format are also accepted. Either way, the manual again:
Internally interval values are stored as months, days, and seconds.
This is done because the number of days in a month varies, and a day
can have 23 or 25 hours if a daylight savings time adjustment is
involved. The months and days fields are integers while the seconds
field can store fractions.
(Output / display depends on the setting of IntervalStyle.)
The above example uses default Postgres format: interval '1 month - 1 day'. These are also valid (while less readable):
interval '1 mon - 1 d' -- unambiguous abbreviations of time units are allowed
IS0 8601 format:
interval '0-1 -1 0:0'
Standard SQL format:
interval 'P1M-1D';
All the same.
Note that expected output for day_in_month(2) can be 29 because of leap years. You might want to pass a date instead of an int.
Also, beware of daylight saving : remove the timezone or else some monthes calculations could be wrong (next example in CET / CEST) :
SELECT DATE_TRUNC('month', '2016-03-12'::timestamptz) + '1 MONTH'::INTERVAL
- DATE_TRUNC('month', '2016-03-12'::timestamptz) ;
------------------
30 days 23:00:00
SELECT DATE_TRUNC('month', '2016-03-12'::timestamp) + '1 MONTH'::INTERVAL
- DATE_TRUNC('month', '2016-03-12'::timestamp) ;
----------
31 days
This works as well.
WITH date_ AS (SELECT your_date AS d)
SELECT d + INTERVAL '1 month' - d FROM date_;
Or just:
SELECT your_date + INTERVAL '1 month' - your_date;
These two return interval, not integer.
SELECT cnt_dayofmonth(2016, 2); -- 29
create or replace function cnt_dayofmonth(_year int, _month int)
returns int2 as
$BODY$
-- ZU 2017.09.15, returns the count of days in mounth, inputs are year and month
declare
datetime_start date := ('01.01.'||_year::char(4))::date;
datetime_month date := ('01.'||_month||'.'||_year)::date;
cnt int2;
begin
select extract(day from (select (datetime_month + INTERVAL '1 month -1 day'))) into cnt;
return cnt;
end;
$BODY$
language plpgsql;
You can write a function:
CREATE OR REPLACE FUNCTION get_total_days_in_month(timestamp)
RETURNS decimal
IMMUTABLE
AS $$
select cast(datediff(day, date_trunc('mon', $1), last_day($1) + 1) as decimal)
$$ LANGUAGE sql;