PostgreSQL age() function: different/unexpected results when landing in dfferent month

穿精又带淫゛_ 提交于 2019-12-04 15:00:23

age is calculated by the timestamptz_age function in src/backend/utils/adt/timestamp.c. The comment says:

/* timestamptz_age()
 * Calculate time difference while retaining year/month fields.
 * Note that this does not result in an accurate absolute time span
 *  since year and month are out of context once the arithmetic
 *  is done.
 */

The code first converts the arguments to struct pg_tm variables tm1 and tm2 (struct pg_tm is similar to the C library's struct tm, but has additional time zone fields) and then calculates the difference tm per field.

In the case of age('2018-07-01','2018-05-20'), the relevant fields of that difference would look like this:

tm_mday = -19
tm_mon  =   2
tm_year =   0

Now negative fields are adjusted. for tm_mday, the code looks like this:

while (tm->tm_mday < 0)
{
    if (dt1 < dt2)
    {
        tm->tm_mday += day_tab[isleap(tm1->tm_year)][tm1->tm_mon - 1];
        tm->tm_mon--;
    }
    else
    {
        tm->tm_mday += day_tab[isleap(tm2->tm_year)][tm2->tm_mon - 1];
        tm->tm_mon--;
    }
}

Since dt1 > dt2, the else branch is taken, and the code adds the number of days in May (31) and reduces the month by 1, ending up with

tm_mday = 12
tm_mon  =  1
tm_year =  0

That is the result you get.

Now at first glance it seems that tm2->tm_mon isn't the right month to choose, and it would have been better to take the previous month of the left argument:

day_tab[isleap(tm1->tm_year)][(tm1->tm_mon + 10) % 12]

But I cannot say if that choice would be better in all cases, and in any event the comment indemnifies the function, so I'd hesitate to call it a bug.

You might want to take it up with the hackers mailing list.

The above unexpected behaviour is not because of age() . But because of interval data type which will allows calculations. Below link contains the necessary explanation.

Odd month arithmetic

In your first one since two times are successive you don't see unexpected. But it second it is not. This tends to above odd month arithmetic behaviour

For those interested: I think I've found a workaround for the problem, using a function which gives me the desired result. It works according to my own tests, even for leap years, but unfortunately, I cannot guarantee that it will always work. It also seems a little bit hacky.

CREATE OR REPLACE FUNCTION age_forward ("endDate" date,"startDate" date) 
     RETURNS interval AS $$

     /*

     Basic approach: actually do a culculation like this:
     SELECT age('2018-07-01','2018-06-01') + ((30 - 20) + 1||' days')::interval;

     So, basically:
     (1) truncate start and end to month level, so always FIRST of month
     (2) add one month to the start month
     (3) calculate the days
     (4) add the days as string and build the interval

     The crucial part is 3: calculate the days

     We do it like this:
     - get the number of days for the month in question. The month in question is the month BEFORE the end month. For our example it is JUNE
     - subtract the start date day number from the number of days (here 20)
     - add the end date day number (here 1)

     */

     SELECT CASE 

        /* First step: Check if the startDate day number is lower or equal the endDate day number.
           If this is the case: Do vanilla age(). Works perfectly here
        */

        WHEN (date_part('day', "startDate" )::integer) <= date_part('day', "endDate" )::integer

        THEN age("endDate","startDate")

        /* Special case to treat here: startDate day number is greater than endDate day number. Do the algorithm described above */

        ELSE  age
               (

                  date_trunc('month', "endDate"::date), /* Go just till month level, always using '1' as day */

                  date_trunc('month', "startDate"::date)
                  + '1 mons'::interval
                  /* Add one month so that interval to look for will become actually shorter for now. */
                ) 
              + 
                (
                   (

                     /* Calculate the last day of the month previous to the end month. See https://stackoverflow.com/questions/28186014/how-to-get-the-last-day-of-month-in-postgres  */
                     (date_part('day',(date_trunc('month', (date_trunc('month', "endDate"::date) - '1 mons'::interval)  ) + interval '1 month' - interval '1 day')::date))::integer

                     - 

                     /* endDate day number subtracted */
                     date_part('day', "startDate" )::integer
                   )

                   /* endDate day number added */
                   + date_part('day', "endDate" )::integer||' days'

                )::interval
        END

$$ LANGUAGE sql;
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!