diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index 462f2ed7a8..4c12c4d663 100644 --- a/src/backend/utils/adt/datetime.c +++ b/src/backend/utils/adt/datetime.c @@ -668,19 +668,50 @@ AdjustYears(int64 val, int scale, } -/* Fetch a fractional-second value with suitable error checking */ +/* + * Parse the fractional part of a number (decimal point and optional digits, + * followed by end of string). Returns the fractional value into *frac. + * + * Returns 0 if successful, DTERR code if bogus input detected. + */ +static int +ParseFraction(char *cp, double *frac) +{ + /* Caller should always pass the start of the fraction part */ + Assert(*cp == '.'); + + /* + * We want to allow just "." with no digits, but some versions of strtod + * will report EINVAL for that, so special-case it. + */ + if (cp[1] == '\0') + { + *frac = 0; + } + else + { + errno = 0; + *frac = strtod(cp, &cp); + /* check for parse failure */ + if (*cp != '\0' || errno != 0) + return DTERR_BAD_FORMAT; + } + return 0; +} + +/* + * Fetch a fractional-second value with suitable error checking. + * Same as ParseFraction except we convert the result to integer microseconds. + */ static int ParseFractionalSecond(char *cp, fsec_t *fsec) { double frac; + int dterr; - /* Caller should always pass the start of the fraction part */ - Assert(*cp == '.'); - errno = 0; - frac = strtod(cp, &cp); - /* check for parse failure */ - if (*cp != '\0' || errno != 0) - return DTERR_BAD_FORMAT; + dterr = ParseFraction(cp, &frac); + if (dterr) + return dterr; *fsec = rint(frac * 1000000); return 0; } @@ -1248,10 +1279,9 @@ DecodeDateTime(char **field, int *ftype, int nf, { double time; - errno = 0; - time = strtod(cp, &cp); - if (*cp != '\0' || errno != 0) - return DTERR_BAD_FORMAT; + dterr = ParseFraction(cp, &time); + if (dterr) + return dterr; time *= USECS_PER_DAY; dt2time(time, &tm->tm_hour, &tm->tm_min, @@ -2146,10 +2176,9 @@ DecodeTimeOnly(char **field, int *ftype, int nf, { double time; - errno = 0; - time = strtod(cp, &cp); - if (*cp != '\0' || errno != 0) - return DTERR_BAD_FORMAT; + dterr = ParseFraction(cp, &time); + if (dterr) + return dterr; time *= USECS_PER_DAY; dt2time(time, &tm->tm_hour, &tm->tm_min, @@ -3035,13 +3064,21 @@ DecodeNumberField(int len, char *str, int fmask, * Can we use ParseFractionalSecond here? Not clear whether trailing * junk should be rejected ... */ - double frac; + if (cp[1] == '\0') + { + /* avoid assuming that strtod will accept "." */ + *fsec = 0; + } + else + { + double frac; - errno = 0; - frac = strtod(cp, NULL); - if (errno != 0) - return DTERR_BAD_FORMAT; - *fsec = rint(frac * 1000000); + errno = 0; + frac = strtod(cp, NULL); + if (errno != 0) + return DTERR_BAD_FORMAT; + *fsec = rint(frac * 1000000); + } /* Now truncate off the fraction for further processing */ *cp = '\0'; len = strlen(str); @@ -3467,11 +3504,9 @@ DecodeInterval(char **field, int *ftype, int nf, int range, } else if (*cp == '.') { - errno = 0; - fval = strtod(cp, &cp); - if (*cp != '\0' || errno != 0) - return DTERR_BAD_FORMAT; - + dterr = ParseFraction(cp, &fval); + if (dterr) + return dterr; if (*field[i] == '-') fval = -fval; } @@ -3650,6 +3685,7 @@ DecodeInterval(char **field, int *ftype, int nf, int range, * Helper functions to avoid duplicated code in DecodeISO8601Interval. * * Parse a decimal value and break it into integer and fractional parts. + * Set *endptr to end+1 of the parsed substring. * Returns 0 or DTERR code. */ static int @@ -3676,7 +3712,20 @@ ParseISO8601Number(char *str, char **endptr, int64 *ipart, double *fpart) /* Parse fractional part if there is any */ if (**endptr == '.') - *fpart = strtod(*endptr, endptr) * sign; + { + /* + * As in ParseFraction, some versions of strtod insist on seeing some + * digits after '.', but some don't. We want to allow zero digits + * after '.' as long as there were some before it. + */ + if (isdigit((unsigned char) *(*endptr + 1))) + *fpart = strtod(*endptr, endptr) * sign; + else + { + (*endptr)++; /* advance over '.' */ + str++; /* so next test will fail if no digits */ + } + } /* did we not see anything that looks like a number? */ if (*endptr == str || errno != 0) diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index 843b07d7d2..97a4544ffc 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -4134,11 +4134,13 @@ timestamp_to_char(PG_FUNCTION_ARGS) ereport(ERROR, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); - COPY_tm(tm, &tt); - thisdate = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday); - tm->tm_wday = (thisdate + 1) % 7; - tm->tm_yday = thisdate - date2j(tm->tm_year, 1, 1) + 1; + /* calculate wday and yday, because timestamp2tm doesn't */ + thisdate = date2j(tt.tm_year, tt.tm_mon, tt.tm_mday); + tt.tm_wday = (thisdate + 1) % 7; + tt.tm_yday = thisdate - date2j(tt.tm_year, 1, 1) + 1; + + COPY_tm(tm, &tt); if (!(res = datetime_to_char_body(&tmtc, fmt, false, PG_GET_COLLATION()))) PG_RETURN_NULL(); @@ -4168,11 +4170,13 @@ timestamptz_to_char(PG_FUNCTION_ARGS) ereport(ERROR, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); - COPY_tm(tm, &tt); - thisdate = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday); - tm->tm_wday = (thisdate + 1) % 7; - tm->tm_yday = thisdate - date2j(tm->tm_year, 1, 1) + 1; + /* calculate wday and yday, because timestamp2tm doesn't */ + thisdate = date2j(tt.tm_year, tt.tm_mon, tt.tm_mday); + tt.tm_wday = (thisdate + 1) % 7; + tt.tm_yday = thisdate - date2j(tt.tm_year, 1, 1) + 1; + + COPY_tm(tm, &tt); if (!(res = datetime_to_char_body(&tmtc, fmt, false, PG_GET_COLLATION()))) PG_RETURN_NULL(); diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out index 86c8d4bc99..03f77c01dc 100644 --- a/src/test/regress/expected/interval.out +++ b/src/test/regress/expected/interval.out @@ -908,6 +908,41 @@ select interval 'P0002' AS "year only", 2 years | 2 years 10 mons | 2 years 10 mons 15 days | 2 years 00:00:01 | 2 years 10 mons 00:00:01 | 2 years 10 mons 15 days 00:00:01 | 10:00:00 | 10:30:00 (1 row) +-- Check handling of fractional fields in ISO8601 format. +select interval 'P1Y0M3DT4H5M6S'; + interval +------------------------ + 1 year 3 days 04:05:06 +(1 row) + +select interval 'P1.0Y0M3DT4H5M6S'; + interval +------------------------ + 1 year 3 days 04:05:06 +(1 row) + +select interval 'P1.1Y0M3DT4H5M6S'; + interval +------------------------------ + 1 year 1 mon 3 days 04:05:06 +(1 row) + +select interval 'P1.Y0M3DT4H5M6S'; + interval +------------------------ + 1 year 3 days 04:05:06 +(1 row) + +select interval 'P.1Y0M3DT4H5M6S'; + interval +----------------------- + 1 mon 3 days 04:05:06 +(1 row) + +select interval 'P.Y0M3DT4H5M6S'; -- error +ERROR: invalid input syntax for type interval: "P.Y0M3DT4H5M6S" +LINE 1: select interval 'P.Y0M3DT4H5M6S'; + ^ -- test a couple rounding cases that changed since 8.3 w/ HAVE_INT64_TIMESTAMP. SET IntervalStyle to postgres_verbose; select interval '-10 mons -3 days +03:55:06.70'; diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql index f05055e03a..97d33a1323 100644 --- a/src/test/regress/sql/interval.sql +++ b/src/test/regress/sql/interval.sql @@ -312,6 +312,14 @@ select interval 'P0002' AS "year only", interval 'PT10' AS "hour only", interval 'PT10:30' AS "hour minute"; +-- Check handling of fractional fields in ISO8601 format. +select interval 'P1Y0M3DT4H5M6S'; +select interval 'P1.0Y0M3DT4H5M6S'; +select interval 'P1.1Y0M3DT4H5M6S'; +select interval 'P1.Y0M3DT4H5M6S'; +select interval 'P.1Y0M3DT4H5M6S'; +select interval 'P.Y0M3DT4H5M6S'; -- error + -- test a couple rounding cases that changed since 8.3 w/ HAVE_INT64_TIMESTAMP. SET IntervalStyle to postgres_verbose; select interval '-10 mons -3 days +03:55:06.70';