From 05506fc4afe95eca768853a9da35eb10e2126721 Mon Sep 17 00:00:00 2001 From: Tom Lane <tgl@sss.pgh.pa.us> Date: Mon, 25 Feb 2008 23:21:01 +0000 Subject: [PATCH] Fix datetime input to behave correctly for Feb 29 in years BC. Formerly, DecodeDate attempted to verify the day-of-the-month exactly, but it was under the misapprehension that it would know whether we were looking at a BC year or not. In reality this check can't be made until the calling function (eg DecodeDateTime) has processed all the fields. So, split the BC adjustment and validity checks out into a new function ValidateDate that is called only after processing all the fields. In passing, this patch makes DecodeTimeOnly work for BC inputs, which it never did before. (The historical veracity of all this is nonexistent, of course, but if we're going to say we support proleptic Gregorian calendar then we should do it correctly. In any case the unpatched code is broken because it could emit dates that it would then reject on re-inputting.) Per report from Bernd Helmle. Back-patch as far as 8.0; in 7.x we were not using our own calendar support and so this seems a bit too risky to put into 7.4. --- src/backend/utils/adt/datetime.c | 192 +++++++++++++++---------------- 1 file changed, 93 insertions(+), 99 deletions(-) diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index 0bfdd0ec2d6..d8fbe0a3077 100644 --- a/src/backend/utils/adt/datetime.c +++ b/src/backend/utils/adt/datetime.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/adt/datetime.c,v 1.185 2008/02/17 02:09:28 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/datetime.c,v 1.186 2008/02/25 23:21:01 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -40,7 +40,10 @@ static int DecodeTime(char *str, int fmask, int *tmask, struct pg_tm * tm, fsec_t *fsec); static int DecodeTimezone(char *str, int *tzp); static const datetkn *datebsearch(const char *key, const datetkn *base, int nel); -static int DecodeDate(char *str, int fmask, int *tmask, struct pg_tm * tm); +static int DecodeDate(char *str, int fmask, int *tmask, bool *is2digits, + struct pg_tm * tm); +static int ValidateDate(int fmask, bool is2digits, bool bc, + struct pg_tm * tm); static void TrimTrailingZeros(char *str); @@ -805,7 +808,8 @@ DecodeDateTime(char **field, int *ftype, int nf, } else { - dterr = DecodeDate(field[i], fmask, &tmask, tm); + dterr = DecodeDate(field[i], fmask, + &tmask, &is2digits, tm); if (dterr) return dterr; } @@ -1000,7 +1004,8 @@ DecodeDateTime(char **field, int *ftype, int nf, /* Embedded decimal and no date yet? */ if (cp != NULL && !(fmask & DTK_DATE_M)) { - dterr = DecodeDate(field[i], fmask, &tmask, tm); + dterr = DecodeDate(field[i], fmask, + &tmask, &is2digits, tm); if (dterr) return dterr; } @@ -1234,51 +1239,14 @@ DecodeDateTime(char **field, int *ftype, int nf, if (tmask & fmask) return DTERR_BAD_FORMAT; fmask |= tmask; - } - - if (fmask & DTK_M(YEAR)) - { - /* there is no year zero in AD/BC notation; i.e. "1 BC" == year 0 */ - if (bc) - { - if (tm->tm_year > 0) - tm->tm_year = -(tm->tm_year - 1); - else - ereport(ERROR, - (errcode(ERRCODE_INVALID_DATETIME_FORMAT), - errmsg("inconsistent use of year %04d and \"BC\"", - tm->tm_year))); - } - else if (is2digits) - { - if (tm->tm_year < 70) - tm->tm_year += 2000; - else if (tm->tm_year < 100) - tm->tm_year += 1900; - } - } - - /* now that we have correct year, decode DOY */ - if (fmask & DTK_M(DOY)) - { - j2date(date2j(tm->tm_year, 1, 1) + tm->tm_yday - 1, - &tm->tm_year, &tm->tm_mon, &tm->tm_mday); - } - - /* check for valid month */ - if (fmask & DTK_M(MONTH)) - { - if (tm->tm_mon < 1 || tm->tm_mon > MONTHS_PER_YEAR) - return DTERR_MD_FIELD_OVERFLOW; - } + } /* end loop over fields */ - /* minimal check for valid day */ - if (fmask & DTK_M(DAY)) - { - if (tm->tm_mday < 1 || tm->tm_mday > 31) - return DTERR_MD_FIELD_OVERFLOW; - } + /* do final checking/adjustment of Y/M/D fields */ + dterr = ValidateDate(fmask, is2digits, bc, tm); + if (dterr) + return dterr; + /* handle AM/PM */ if (mer != HR24 && tm->tm_hour > 12) return DTERR_FIELD_OVERFLOW; if (mer == AM && tm->tm_hour == 12) @@ -1296,14 +1264,6 @@ DecodeDateTime(char **field, int *ftype, int nf, return DTERR_BAD_FORMAT; } - /* - * Check for valid day of month, now that we know for sure the month - * and year. Note we don't use MD_FIELD_OVERFLOW here, since it seems - * unlikely that "Feb 29" is a YMD-order error. - */ - if (tm->tm_mday > day_tab[isleap(tm->tm_year)][tm->tm_mon - 1]) - return DTERR_FIELD_OVERFLOW; - /* * If we had a full timezone spec, compute the offset (we could not do * it before, because we need the date to resolve DST status). @@ -1486,6 +1446,7 @@ DecodeTimeOnly(char **field, int *ftype, int nf, int val; int dterr; bool is2digits = FALSE; + bool bc = FALSE; int mer = HR24; pg_tz *namedTz = NULL; @@ -1517,7 +1478,8 @@ DecodeTimeOnly(char **field, int *ftype, int nf, if (i == 0 && nf >= 2 && (ftype[nf - 1] == DTK_DATE || ftype[1] == DTK_TIME)) { - dterr = DecodeDate(field[i], fmask, &tmask, tm); + dterr = DecodeDate(field[i], fmask, + &tmask, &is2digits, tm); if (dterr) return dterr; } @@ -1783,7 +1745,8 @@ DecodeTimeOnly(char **field, int *ftype, int nf, */ if (i == 0 && nf >= 2 && ftype[nf - 1] == DTK_DATE) { - dterr = DecodeDate(field[i], fmask, &tmask, tm); + dterr = DecodeDate(field[i], fmask, + &tmask, &is2digits, tm); if (dterr) return dterr; } @@ -1912,6 +1875,10 @@ DecodeTimeOnly(char **field, int *ftype, int nf, mer = val; break; + case ADBC: + bc = (val == BC); + break; + case UNITS: tmask = 0; ptype = val; @@ -1960,8 +1927,14 @@ DecodeTimeOnly(char **field, int *ftype, int nf, if (tmask & fmask) return DTERR_BAD_FORMAT; fmask |= tmask; - } + } /* end loop over fields */ + /* do final checking/adjustment of Y/M/D fields */ + dterr = ValidateDate(fmask, is2digits, bc, tm); + if (dterr) + return dterr; + + /* handle AM/PM */ if (mer != HR24 && tm->tm_hour > 12) return DTERR_FIELD_OVERFLOW; if (mer == AM && tm->tm_hour == 12) @@ -2047,10 +2020,15 @@ DecodeTimeOnly(char **field, int *ftype, int nf, * Decode date string which includes delimiters. * Return 0 if okay, a DTERR code if not. * - * Insist on a complete set of fields. + * str: field to be parsed + * fmask: bitmask for field types already seen + * *tmask: receives bitmask for fields found here + * *is2digits: set to TRUE if we find 2-digit year + * *tm: field values are stored into appropriate members of this struct */ static int -DecodeDate(char *str, int fmask, int *tmask, struct pg_tm * tm) +DecodeDate(char *str, int fmask, int *tmask, bool *is2digits, + struct pg_tm * tm) { fsec_t fsec; int nf = 0; @@ -2058,13 +2036,13 @@ DecodeDate(char *str, int fmask, int *tmask, struct pg_tm * tm) len; int dterr; bool haveTextMonth = FALSE; - bool bc = FALSE; - bool is2digits = FALSE; int type, val, dmask = 0; char *field[MAXDATEFIELDS]; + *tmask = 0; + /* parse this string... */ while (*str != '\0' && nf < MAXDATEFIELDS) { @@ -2090,14 +2068,6 @@ DecodeDate(char *str, int fmask, int *tmask, struct pg_tm * tm) nf++; } -#if 0 - /* don't allow too many fields */ - if (nf > 3) - return DTERR_BAD_FORMAT; -#endif - - *tmask = 0; - /* look first for text fields, since that will be unambiguous month */ for (i = 0; i < nf; i++) { @@ -2115,10 +2085,6 @@ DecodeDate(char *str, int fmask, int *tmask, struct pg_tm * tm) haveTextMonth = TRUE; break; - case ADBC: - bc = (val == BC); - break; - default: return DTERR_BAD_FORMAT; } @@ -2144,7 +2110,7 @@ DecodeDate(char *str, int fmask, int *tmask, struct pg_tm * tm) dterr = DecodeNumber(len, field[i], haveTextMonth, fmask, &dmask, tm, - &fsec, &is2digits); + &fsec, is2digits); if (dterr) return dterr; @@ -2158,23 +2124,38 @@ DecodeDate(char *str, int fmask, int *tmask, struct pg_tm * tm) if ((fmask & ~(DTK_M(DOY) | DTK_M(TZ))) != DTK_DATE_M) return DTERR_BAD_FORMAT; - /* there is no year zero in AD/BC notation; i.e. "1 BC" == year 0 */ - if (bc) - { - if (tm->tm_year > 0) - tm->tm_year = -(tm->tm_year - 1); - else - ereport(ERROR, - (errcode(ERRCODE_INVALID_DATETIME_FORMAT), - errmsg("inconsistent use of year %04d and \"BC\"", - tm->tm_year))); - } - else if (is2digits) + /* validation of the field values must wait until ValidateDate() */ + + return 0; +} + +/* ValidateDate() + * Check valid year/month/day values, handle BC and DOY cases + * Return 0 if okay, a DTERR code if not. + */ +static int +ValidateDate(int fmask, bool is2digits, bool bc, struct pg_tm * tm) +{ + if (fmask & DTK_M(YEAR)) { - if (tm->tm_year < 70) - tm->tm_year += 2000; - else if (tm->tm_year < 100) - tm->tm_year += 1900; + /* there is no year zero in AD/BC notation; i.e. "1 BC" == year 0 */ + if (bc) + { + if (tm->tm_year > 0) + tm->tm_year = -(tm->tm_year - 1); + else + ereport(ERROR, + (errcode(ERRCODE_INVALID_DATETIME_FORMAT), + errmsg("inconsistent use of year %04d and \"BC\"", + tm->tm_year))); + } + else if (is2digits) + { + if (tm->tm_year < 70) + tm->tm_year += 2000; + else if (tm->tm_year < 100) + tm->tm_year += 1900; + } } /* now that we have correct year, decode DOY */ @@ -2185,16 +2166,29 @@ DecodeDate(char *str, int fmask, int *tmask, struct pg_tm * tm) } /* check for valid month */ - if (tm->tm_mon < 1 || tm->tm_mon > MONTHS_PER_YEAR) - return DTERR_MD_FIELD_OVERFLOW; + if (fmask & DTK_M(MONTH)) + { + if (tm->tm_mon < 1 || tm->tm_mon > MONTHS_PER_YEAR) + return DTERR_MD_FIELD_OVERFLOW; + } - /* check for valid day */ - if (tm->tm_mday < 1 || tm->tm_mday > 31) - return DTERR_MD_FIELD_OVERFLOW; + /* minimal check for valid day */ + if (fmask & DTK_M(DAY)) + { + if (tm->tm_mday < 1 || tm->tm_mday > 31) + return DTERR_MD_FIELD_OVERFLOW; + } - /* We don't want to hint about DateStyle for Feb 29 */ - if (tm->tm_mday > day_tab[isleap(tm->tm_year)][tm->tm_mon - 1]) - return DTERR_FIELD_OVERFLOW; + if ((fmask & DTK_DATE_M) == DTK_DATE_M) + { + /* + * Check for valid day of month, now that we know for sure the month + * and year. Note we don't use MD_FIELD_OVERFLOW here, since it seems + * unlikely that "Feb 29" is a YMD-order error. + */ + if (tm->tm_mday > day_tab[isleap(tm->tm_year)][tm->tm_mon - 1]) + return DTERR_FIELD_OVERFLOW; + } return 0; } -- GitLab