5

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

paulf
  • 93
  • 1
  • 7
  • 2
    Replacing the `now()` with the timestamp that'd reproduce the issue would be helpful for people who're not seeing this on the same day you wrote it. – Craig Ringer Nov 04 '14 at 04:12

3 Answers3

7

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.

Richard Huxton
  • 21,516
  • 3
  • 39
  • 51
1

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
MBO
  • 30,379
  • 5
  • 50
  • 52
  • seems rather arbitrary though that '1 year' gets me 360 days. 5+ days seems like quite a difference – paulf Nov 03 '14 at 23:50
  • From my understanding year is dynamic interval - it changes it's meaning depending on year you add it to. Without date attached maybe it's some value, let's say 360 days? I don't know, for me it's easier to check if 1 year from now is earlier than some other date instead of checking if difference between 2 dates is less than 1 year (was it leap year? or maybe it was around Julian calendar reform?). 1 year has no meaning when you don't know year you are talking about. – MBO Nov 04 '14 at 00:01
  • maybe - tho no year i know of is 360 days, they are either 365 or 366 in what i would expect to be the default calendar (maybe different for lunar calendars, etc). also, it's not a random year, the example was explicitly 1 year subtracted from November 3, 2014. – paulf Nov 04 '14 at 03:28
  • @paulf But when you substract timestamp from other timestamp, you get interval without context. It's the same as with months: Mar 1st - Feb 1st = 1 month, Jul 1st - Jun 1st = 1 month. Are they the same amount of days? Richard was better digging why year equals ta 360 days though. – MBO Nov 04 '14 at 08:10
1

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       |
pozs
  • 34,608
  • 5
  • 57
  • 63