From a4917bef0ead2424bf0c2eeb05dfb681dff33574 Mon Sep 17 00:00:00 2001 From: Tom Lane <tgl@sss.pgh.pa.us> Date: Tue, 11 Nov 2008 02:42:33 +0000 Subject: [PATCH] Add support for input and output of interval values formatted per ISO 8601; specifically, we can input either the "format with designators" or the "alternative format", and we can output the former when IntervalStyle is set to iso_8601. Ron Mayer --- doc/src/sgml/config.sgml | 5 +- doc/src/sgml/datatype.sgml | 132 ++++++++- src/backend/utils/adt/datetime.c | 370 ++++++++++++++++++++++++- src/backend/utils/adt/nabstime.c | 8 +- src/backend/utils/adt/timestamp.c | 11 +- src/backend/utils/misc/guc.c | 3 +- src/bin/psql/tab-complete.c | 4 +- src/include/miscadmin.h | 10 +- src/include/utils/datetime.h | 10 +- src/test/regress/expected/interval.out | 51 ++++ src/test/regress/sql/interval.sql | 35 +++ 11 files changed, 605 insertions(+), 34 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 715eb44e010..7931ea87377 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1,4 +1,4 @@ -<!-- $PostgreSQL: pgsql/doc/src/sgml/config.sgml,v 1.194 2008/11/09 00:28:34 tgl Exp $ --> +<!-- $PostgreSQL: pgsql/doc/src/sgml/config.sgml,v 1.195 2008/11/11 02:42:31 tgl Exp $ --> <chapter Id="runtime-config"> <title>Server Configuration</title> @@ -4032,6 +4032,9 @@ SET XML OPTION { DOCUMENT | CONTENT }; matching <productname>PostgreSQL</> releases prior to 8.4 when the <varname>DateStyle</> parameter was set to non-<literal>ISO</> output. + The value <literal>iso_8601</> will produce output matching the time + interval <quote>format with designators</> defined in section + 4.4.3.2 of ISO 8601. </para> <para> The <varname>IntervalStyle</> parameter also affects the diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml index d26fdc5fde6..c9669b49512 100644 --- a/doc/src/sgml/datatype.sgml +++ b/doc/src/sgml/datatype.sgml @@ -1,4 +1,4 @@ -<!-- $PostgreSQL: pgsql/doc/src/sgml/datatype.sgml,v 1.233 2008/11/09 17:09:48 tgl Exp $ --> +<!-- $PostgreSQL: pgsql/doc/src/sgml/datatype.sgml,v 1.234 2008/11/11 02:42:31 tgl Exp $ --> <chapter id="datatype"> <title id="datatype-title">Data Types</title> @@ -2353,9 +2353,9 @@ January 8 04:05:06 1999 PST <type>interval</type> values can be written with the following verbose syntax: -<programlisting> +<synopsis> <optional>@</> <replaceable>quantity</> <replaceable>unit</> <optional><replaceable>quantity</> <replaceable>unit</>...</> <optional><replaceable>direction</></optional> -</programlisting> +</synopsis> where <replaceable>quantity</> is a number (possibly signed); <replaceable>unit</> is <literal>microsecond</literal>, @@ -2384,6 +2384,76 @@ January 8 04:05:06 1999 PST <varname>IntervalStyle</> is set to <literal>sql_standard</literal>.) </para> + <para> + Interval values can also be written as ISO 8601 time intervals, using + either the <quote>format with designators</> of the standard's section + 4.4.3.2 or the <quote>alternative format</> of section 4.4.3.3. The + format with designators looks like this: +<synopsis> +P <replaceable>quantity</> <replaceable>unit</> <optional> <replaceable>quantity</> <replaceable>unit</> ...</optional> <optional> T <optional> <replaceable>quantity</> <replaceable>unit</> ...</optional></optional> +</synopsis> + The string must start with a <literal>P</>, and may include a + <literal>T</> that introduces the time-of-day units. The + available unit abbreviations are given in <xref + linkend="datatype-interval-iso8601-units">. Units may be + omitted, and may be specified in any order, but units smaller than + a day must appear after <literal>T</>. In particular, the meaning of + <literal>M</> depends on whether it is before or after + <literal>T</>. + </para> + + <table id="datatype-interval-iso8601-units"> + <title>ISO 8601 interval unit abbreviations</title> + <tgroup cols="2"> + <thead> + <row> + <entry>Abbreviation</entry> + <entry>Meaning</entry> + </row> + </thead> + <tbody> + <row> + <entry>Y</entry> + <entry>Years</entry> + </row> + <row> + <entry>M</entry> + <entry>Months (in the date part)</entry> + </row> + <row> + <entry>W</entry> + <entry>Weeks</entry> + </row> + <row> + <entry>D</entry> + <entry>Days</entry> + </row> + <row> + <entry>H</entry> + <entry>Hours</entry> + </row> + <row> + <entry>M</entry> + <entry>Minutes (in the time part)</entry> + </row> + <row> + <entry>S</entry> + <entry>Seconds</entry> + </row> + </tbody> + </tgroup> + </table> + + <para> + In the alternative format: +<synopsis> +P <optional> <replaceable>years</>-<replaceable>months</>-<replaceable>days</> </optional> <optional> T <replaceable>hours</>:<replaceable>minutes</>:<replaceable>seconds</> </optional> +</synopsis> + the string must begin with <literal>P</literal>, and a + <literal>T</> separates the date and time parts of the interval. + The values are given as numbers similar to ISO 8601 dates. + </para> + <para> When writing an interval constant with a <replaceable>fields</> specification, or when assigning to an interval column that was defined @@ -2433,6 +2503,46 @@ January 8 04:05:06 1999 PST For example, <literal>'1.5 month'</> becomes 1 month and 15 days. Only seconds will ever be shown as fractional on output. </para> + + <para> + <xref linkend="datatype-interval-input-examples"> shows some examples + of valid <type>interval</> input. + </para> + + <table id="datatype-interval-input-examples"> + <title>Interval Input</title> + <tgroup cols="2"> + <thead> + <row> + <entry>Example</entry> + <entry>Description</entry> + </row> + </thead> + <tbody> + <row> + <entry>1-2</entry> + <entry>SQL standard format: 1 year 2 months</entry> + </row> + <row> + <entry>3 4:05:06</entry> + <entry>SQL standard format: 3 days 4 hours 5 minutes 6 seconds</entry> + </row> + <row> + <entry>1 year 2 months 3 days 4 hours 5 minutes 6 seconds</entry> + <entry>Traditional Postgres format: 1 year 2 months 3 days 4 hours 5 minutes 6 seconds</entry> + </row> + <row> + <entry>P1Y2M3DT4H5M6S</entry> + <entry>ISO 8601 <quote>format with designators</>: same meaning as above</entry> + </row> + <row> + <entry>P0001-02-03T04:05:06</entry> + <entry>ISO 8601 <quote>alternative format</>: same meaning as above</entry> + </row> + </tbody> + </tgroup> + </table> + </sect2> <sect2 id="datatype-interval-output"> @@ -2446,8 +2556,8 @@ January 8 04:05:06 1999 PST <para> The output format of the interval type can be set to one of the - three styles <literal>sql_standard</>, - <literal>postgres</>, or <literal>postgres_verbose</>, + four styles <literal>sql_standard</>, <literal>postgres</>, + <literal>postgres_verbose</>, or <literal>iso_8601</>, using the command <literal>SET intervalstyle</literal>. The default is the <literal>postgres</> format. <xref linkend="interval-style-output-table"> shows examples of each @@ -2476,6 +2586,12 @@ January 8 04:05:06 1999 PST <varname>DateStyle</> parameter was set to non-<literal>ISO</> output. </para> + <para> + The output of the <literal>iso_8601</> style matches the <quote>format + with designators</> described in section 4.4.3.2 of the + ISO 8601 standard. + </para> + <table id="interval-style-output-table"> <title>Interval Output Style Examples</title> <tgroup cols="4"> @@ -2506,6 +2622,12 @@ January 8 04:05:06 1999 PST <entry>@ 3 days 4 hours 5 mins 6 secs</entry> <entry>@ 1 year 2 mons -3 days 4 hours 5 mins 6 secs ago</entry> </row> + <row> + <entry><literal>iso_8601</></entry> + <entry>P1Y2M</entry> + <entry>P3DT4H5M6S</entry> + <entry>P-1Y-2M3DT-4H-5M-6S</entry> + </row> </tbody> </tgroup> </table> diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index e91c470304f..ef61b3eb560 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.197 2008/11/09 00:28:34 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/datetime.c,v 1.198 2008/11/11 02:42:32 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -2726,6 +2726,7 @@ DecodeSpecial(int field, char *lowtoken, int *val) /* DecodeInterval() * Interpret previously parsed fields for general time interval. * Returns 0 if successful, DTERR code if bogus input detected. + * dtype, tm, fsec are output parameters. * * Allow "date" field DTK_DATE since this could be just * an unsigned floating point number. - thomas 1997-11-16 @@ -3188,6 +3189,307 @@ 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. + * Returns 0 or DTERR code. + */ +static int +ParseISO8601Number(char *str, char **endptr, int *ipart, double *fpart) +{ + double val; + + if (!(isdigit((unsigned char) *str) || *str == '-' || *str == '.')) + return DTERR_BAD_FORMAT; + errno = 0; + val = strtod(str, endptr); + /* did we not see anything that looks like a double? */ + if (*endptr == str || errno != 0) + return DTERR_BAD_FORMAT; + /* watch out for overflow */ + if (val < INT_MIN || val > INT_MAX) + return DTERR_FIELD_OVERFLOW; + /* be very sure we truncate towards zero (cf dtrunc()) */ + if (val >= 0) + *ipart = (int) floor(val); + else + *ipart = (int) -floor(-val); + *fpart = val - *ipart; + return 0; +} + +/* + * Determine number of integral digits in a valid ISO 8601 number field + * (we should ignore sign and any fraction part) + */ +static int +ISO8601IntegerWidth(char *fieldstart) +{ + /* We might have had a leading '-' */ + if (*fieldstart == '-') + fieldstart++; + return strspn(fieldstart, "0123456789"); +} + +/* + * Multiply frac by scale (to produce seconds) and add to *tm & *fsec. + * We assume the input frac is less than 1 so overflow is not an issue. + */ +static void +AdjustFractionalSeconds(double frac, struct pg_tm * tm, fsec_t *fsec, + int scale) +{ + int sec; + + if (frac == 0) + return; + frac *= scale; + sec = (int) frac; + tm->tm_sec += sec; + frac -= sec; +#ifdef HAVE_INT64_TIMESTAMP + *fsec += rint(frac * 1000000); +#else + *fsec += frac; +#endif +} + +/* As above, but initial scale produces days */ +static void +AdjustFractionalDays(double frac, struct pg_tm * tm, fsec_t *fsec, int scale) +{ + int extra_days; + + if (frac == 0) + return; + frac *= scale; + extra_days = (int) frac; + tm->tm_mday += extra_days; + frac -= extra_days; + AdjustFractionalSeconds(frac, tm, fsec, SECS_PER_DAY); +} + + +/* DecodeISO8601Interval() + * Decode an ISO 8601 time interval of the "format with designators" + * (section 4.4.3.2) or "alternative format" (section 4.4.3.3) + * Examples: P1D for 1 day + * PT1H for 1 hour + * P2Y6M7DT1H30M for 2 years, 6 months, 7 days 1 hour 30 min + * P0002-06-07T01:30:00 the same value in alternative format + * + * Returns 0 if successful, DTERR code if bogus input detected. + * Note: error code should be DTERR_BAD_FORMAT if input doesn't look like + * ISO8601, otherwise this could cause unexpected error messages. + * dtype, tm, fsec are output parameters. + * + * A couple exceptions from the spec: + * - a week field ('W') may coexist with other units + * - allows decimals in fields other than the least significant unit. + */ +int +DecodeISO8601Interval(char *str, + int *dtype, struct pg_tm * tm, fsec_t *fsec) +{ + bool datepart = true; + bool havefield = false; + + *dtype = DTK_DELTA; + + tm->tm_year = 0; + tm->tm_mon = 0; + tm->tm_mday = 0; + tm->tm_hour = 0; + tm->tm_min = 0; + tm->tm_sec = 0; + *fsec = 0; + + if (strlen(str) < 2 || str[0] != 'P') + return DTERR_BAD_FORMAT; + + str++; + while (*str) + { + char *fieldstart; + int val; + double fval; + char unit; + int dterr; + + if (*str == 'T') /* T indicates the beginning of the time part */ + { + datepart = false; + havefield = false; + str++; + continue; + } + + fieldstart = str; + dterr = ParseISO8601Number(str, &str, &val, &fval); + if (dterr) + return dterr; + + /* + * Note: we could step off the end of the string here. Code below + * *must* exit the loop if unit == '\0'. + */ + unit = *str++; + + if (datepart) + { + switch (unit) /* before T: Y M W D */ + { + case 'Y': + tm->tm_year += val; + tm->tm_mon += (fval * 12); + break; + case 'M': + tm->tm_mon += val; + AdjustFractionalDays(fval, tm, fsec, DAYS_PER_MONTH); + break; + case 'W': + tm->tm_mday += val * 7; + AdjustFractionalDays(fval, tm, fsec, 7); + break; + case 'D': + tm->tm_mday += val; + AdjustFractionalSeconds(fval, tm, fsec, SECS_PER_DAY); + break; + case 'T': /* ISO 8601 4.4.3.3 Alternative Format / Basic */ + case '\0': + if (ISO8601IntegerWidth(fieldstart) == 8 && !havefield) + { + tm->tm_year += val / 10000; + tm->tm_mon += (val / 100) % 100; + tm->tm_mday += val % 100; + AdjustFractionalSeconds(fval, tm, fsec, SECS_PER_DAY); + if (unit == '\0') + return 0; + datepart = false; + havefield = false; + continue; + } + /* Else fall through to extended alternative format */ + case '-': /* ISO 8601 4.4.3.3 Alternative Format, Extended */ + if (havefield) + return DTERR_BAD_FORMAT; + + tm->tm_year += val; + tm->tm_mon += (fval * 12); + if (unit == '\0') + return 0; + if (unit == 'T') + { + datepart = false; + havefield = false; + continue; + } + + dterr = ParseISO8601Number(str, &str, &val, &fval); + if (dterr) + return dterr; + tm->tm_mon += val; + AdjustFractionalDays(fval, tm, fsec, DAYS_PER_MONTH); + if (*str == '\0') + return 0; + if (*str == 'T') + { + datepart = false; + havefield = false; + continue; + } + if (*str != '-') + return DTERR_BAD_FORMAT; + str++; + + dterr = ParseISO8601Number(str, &str, &val, &fval); + if (dterr) + return dterr; + tm->tm_mday += val; + AdjustFractionalSeconds(fval, tm, fsec, SECS_PER_DAY); + if (*str == '\0') + return 0; + if (*str == 'T') + { + datepart = false; + havefield = false; + continue; + } + return DTERR_BAD_FORMAT; + default: + /* not a valid date unit suffix */ + return DTERR_BAD_FORMAT; + } + } + else + { + switch (unit) /* after T: H M S */ + { + case 'H': + tm->tm_hour += val; + AdjustFractionalSeconds(fval, tm, fsec, SECS_PER_HOUR); + break; + case 'M': + tm->tm_min += val; + AdjustFractionalSeconds(fval, tm, fsec, SECS_PER_MINUTE); + break; + case 'S': + tm->tm_sec += val; + AdjustFractionalSeconds(fval, tm, fsec, 1); + break; + case '\0': /* ISO 8601 4.4.3.3 Alternative Format */ + if (ISO8601IntegerWidth(fieldstart) == 6 && !havefield) + { + tm->tm_hour += val / 10000; + tm->tm_min += (val / 100) % 100; + tm->tm_sec += val % 100; + AdjustFractionalSeconds(fval, tm, fsec, 1); + return 0; + } + /* Else fall through to extended alternative format */ + case ':': /* ISO 8601 4.4.3.3 Alternative Format, Extended */ + if (havefield) + return DTERR_BAD_FORMAT; + + tm->tm_hour += val; + AdjustFractionalSeconds(fval, tm, fsec, SECS_PER_HOUR); + if (unit == '\0') + return 0; + + dterr = ParseISO8601Number(str, &str, &val, &fval); + if (dterr) + return dterr; + tm->tm_min += val; + AdjustFractionalSeconds(fval, tm, fsec, SECS_PER_MINUTE); + if (*str == '\0') + return 0; + if (*str != ':') + return DTERR_BAD_FORMAT; + str++; + + dterr = ParseISO8601Number(str, &str, &val, &fval); + if (dterr) + return dterr; + tm->tm_sec += val; + AdjustFractionalSeconds(fval, tm, fsec, 1); + if (*str == '\0') + return 0; + return DTERR_BAD_FORMAT; + + default: + /* not a valid time unit suffix */ + return DTERR_BAD_FORMAT; + } + } + + havefield = true; + } + + return 0; +} + + /* DecodeUnits() * Decode text string using lookup table. * This routine supports time interval decoding @@ -3662,27 +3964,39 @@ EncodeDateTime(struct pg_tm * tm, fsec_t fsec, int *tzp, char **tzn, int style, /* - * Helper function to avoid duplicated code in EncodeInterval below. + * Helper functions to avoid duplicated code in EncodeInterval. + * + * Append sections and fractional seconds (if any) at *cp. * Note that any sign is stripped from the input seconds values. */ static void -AppendSeconds(char *cp, int sec, fsec_t fsec) +AppendSeconds(char *cp, int sec, fsec_t fsec, bool fillzeros) { if (fsec == 0) { - sprintf(cp, ":%02d", abs(sec)); + sprintf(cp, fillzeros ? "%02d" : "%d", abs(sec)); } else { #ifdef HAVE_INT64_TIMESTAMP - sprintf(cp, ":%02d.%06d", abs(sec), Abs(fsec)); + sprintf(cp, fillzeros ? "%02d.%06d" : "%d.%06d", abs(sec), Abs(fsec)); #else - sprintf(cp, ":%012.9f", fabs(sec + fsec)); + sprintf(cp, fillzeros ? "%012.9f" : "%.9f", fabs(sec + fsec)); #endif TrimTrailingZeros(cp); } } +/* Append an ISO8601 field, but only if value isn't zero */ +static char * +AddISO8601IntervalPart(char *cp, int value, char units) +{ + if (value == 0) + return cp; + sprintf(cp, "%d%c", value, units); + return cp + strlen(cp); +} + /* EncodeInterval() * Interpret time structure as a delta time and convert to string. @@ -3772,12 +4086,12 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str) char day_sign = (mday < 0) ? '-' : '+'; char sec_sign = (hour < 0 || min < 0 || sec < 0 || fsec < 0) ? '-' : '+'; - sprintf(cp, "%c%d-%d %c%d %c%d:%02d", + sprintf(cp, "%c%d-%d %c%d %c%d:%02d:", year_sign, abs(year), abs(mon), day_sign, abs(mday), sec_sign, abs(hour), abs(min)); cp += strlen(cp); - AppendSeconds(cp, sec, fsec); + AppendSeconds(cp, sec, fsec, true); } else if (has_year_month) { @@ -3785,19 +4099,47 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str) } else if (has_day) { - sprintf(cp, "%d %d:%02d", mday, hour, min); + sprintf(cp, "%d %d:%02d:", mday, hour, min); cp += strlen(cp); - AppendSeconds(cp, sec, fsec); + AppendSeconds(cp, sec, fsec, true); } else { - sprintf(cp, "%d:%02d", hour, min); + sprintf(cp, "%d:%02d:", hour, min); cp += strlen(cp); - AppendSeconds(cp, sec, fsec); + AppendSeconds(cp, sec, fsec, true); } } break; + /* ISO 8601 "time-intervals by duration only" */ + case INTSTYLE_ISO_8601: + /* special-case zero to avoid printing nothing */ + if (year == 0 && mon == 0 && mday == 0 && + hour == 0 && min == 0 && sec == 0 && fsec == 0) + { + sprintf(cp, "PT0S"); + break; + } + *cp++ = 'P'; + cp = AddISO8601IntervalPart(cp, year, 'Y'); + cp = AddISO8601IntervalPart(cp, mon , 'M'); + cp = AddISO8601IntervalPart(cp, mday, 'D'); + if (hour != 0 || min != 0 || sec != 0 || fsec != 0) + *cp++ = 'T'; + cp = AddISO8601IntervalPart(cp, hour, 'H'); + cp = AddISO8601IntervalPart(cp, min , 'M'); + if (sec != 0 || fsec != 0) + { + if (sec < 0 || fsec < 0) + *cp++ = '-'; + AppendSeconds(cp, sec, fsec, false); + cp += strlen(cp); + *cp++ = 'S'; + *cp++ = '\0'; + } + break; + /* Compatible with postgresql < 8.4 when DateStyle = 'iso' */ case INTSTYLE_POSTGRES: if (tm->tm_year != 0) @@ -3835,11 +4177,11 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str) int minus = (tm->tm_hour < 0 || tm->tm_min < 0 || tm->tm_sec < 0 || fsec < 0); - sprintf(cp, "%s%s%02d:%02d", is_nonzero ? " " : "", + sprintf(cp, "%s%s%02d:%02d:", is_nonzero ? " " : "", (minus ? "-" : (is_before ? "+" : "")), abs(tm->tm_hour), abs(tm->tm_min)); cp += strlen(cp); - AppendSeconds(cp, tm->tm_sec, fsec); + AppendSeconds(cp, tm->tm_sec, fsec, true); cp += strlen(cp); is_nonzero = TRUE; } diff --git a/src/backend/utils/adt/nabstime.c b/src/backend/utils/adt/nabstime.c index 6744818e412..0dd8ab5e5a6 100644 --- a/src/backend/utils/adt/nabstime.c +++ b/src/backend/utils/adt/nabstime.c @@ -10,7 +10,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/adt/nabstime.c,v 1.157 2008/11/09 00:28:35 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/nabstime.c,v 1.158 2008/11/11 02:42:32 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -634,6 +634,12 @@ reltimein(PG_FUNCTION_ARGS) if (dterr == 0) dterr = DecodeInterval(field, ftype, nf, INTERVAL_FULL_RANGE, &dtype, tm, &fsec); + + /* if those functions think it's a bad format, try ISO8601 style */ + if (dterr == DTERR_BAD_FORMAT) + dterr = DecodeISO8601Interval(str, + &dtype, tm, &fsec); + if (dterr != 0) { if (dterr == DTERR_FIELD_OVERFLOW) diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index ce633c7a4fd..d0d9afc9586 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.194 2008/11/09 00:28:35 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/timestamp.c,v 1.195 2008/11/11 02:42:32 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -626,7 +626,14 @@ interval_in(PG_FUNCTION_ARGS) dterr = ParseDateTime(str, workbuf, sizeof(workbuf), field, ftype, MAXDATEFIELDS, &nf); if (dterr == 0) - dterr = DecodeInterval(field, ftype, nf, range, &dtype, tm, &fsec); + dterr = DecodeInterval(field, ftype, nf, range, + &dtype, tm, &fsec); + + /* if those functions think it's a bad format, try ISO8601 style */ + if (dterr == DTERR_BAD_FORMAT) + dterr = DecodeISO8601Interval(str, + &dtype, tm, &fsec); + if (dterr != 0) { if (dterr == DTERR_FIELD_OVERFLOW) diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 6a5faa725da..143003f3844 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -10,7 +10,7 @@ * Written by Peter Eisentraut <peter_e@gmx.net>. * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/misc/guc.c,v 1.476 2008/11/09 00:28:35 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/misc/guc.c,v 1.477 2008/11/11 02:42:32 tgl Exp $ * *-------------------------------------------------------------------- */ @@ -217,6 +217,7 @@ static const struct config_enum_entry intervalstyle_options[] = { {"postgres", INTSTYLE_POSTGRES, false}, {"postgres_verbose", INTSTYLE_POSTGRES_VERBOSE, false}, {"sql_standard", INTSTYLE_SQL_STANDARD, false}, + {"iso_8601", INTSTYLE_ISO_8601, false}, {NULL, 0, false} }; diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index 8c38aaf95bd..d262f21771e 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -3,7 +3,7 @@ * * Copyright (c) 2000-2008, PostgreSQL Global Development Group * - * $PostgreSQL: pgsql/src/bin/psql/tab-complete.c,v 1.175 2008/11/09 00:28:35 tgl Exp $ + * $PostgreSQL: pgsql/src/bin/psql/tab-complete.c,v 1.176 2008/11/11 02:42:32 tgl Exp $ */ /*---------------------------------------------------------------------- @@ -1959,7 +1959,7 @@ psql_completion(char *text, int start, int end) else if (pg_strcasecmp(prev2_wd, "IntervalStyle") == 0) { static const char *const my_list[] = - {"postgres", "postgres_verbose", "sql_standard", NULL}; + {"postgres", "postgres_verbose", "sql_standard", "iso_8601", NULL}; COMPLETE_WITH_LIST(my_list); } diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 9348a527aa6..3a3f3830991 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -13,7 +13,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/miscadmin.h,v 1.204 2008/11/09 00:28:35 tgl Exp $ + * $PostgreSQL: pgsql/src/include/miscadmin.h,v 1.205 2008/11/11 02:42:32 tgl Exp $ * * NOTES * some of the information in this file should be moved to other files. @@ -197,10 +197,12 @@ extern int DateOrder; * INTSTYLE_POSTGRES Like Postgres < 8.4 when DateStyle = 'iso' * INTSTYLE_POSTGRES_VERBOSE Like Postgres < 8.4 when DateStyle != 'iso' * INTSTYLE_SQL_STANDARD SQL standard interval literals + * INTSTYLE_ISO_8601 ISO-8601-basic formatted intervals */ -#define INTSTYLE_POSTGRES 0 -#define INTSTYLE_POSTGRES_VERBOSE 1 -#define INTSTYLE_SQL_STANDARD 2 +#define INTSTYLE_POSTGRES 0 +#define INTSTYLE_POSTGRES_VERBOSE 1 +#define INTSTYLE_SQL_STANDARD 2 +#define INTSTYLE_ISO_8601 3 extern int IntervalStyle; diff --git a/src/include/utils/datetime.h b/src/include/utils/datetime.h index 439e9779d20..9f5d979bcf1 100644 --- a/src/include/utils/datetime.h +++ b/src/include/utils/datetime.h @@ -9,7 +9,7 @@ * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/utils/datetime.h,v 1.70 2008/09/10 18:29:41 tgl Exp $ + * $PostgreSQL: pgsql/src/include/utils/datetime.h,v 1.71 2008/11/11 02:42:32 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -289,9 +289,11 @@ extern int DecodeDateTime(char **field, int *ftype, extern int DecodeTimeOnly(char **field, int *ftype, int nf, int *dtype, struct pg_tm * tm, fsec_t *fsec, int *tzp); -extern int DecodeInterval(char **field, int *ftype, - int nf, int range, int *dtype, - struct pg_tm * tm, fsec_t *fsec); +extern int DecodeInterval(char **field, int *ftype, int nf, int range, + int *dtype, struct pg_tm * tm, fsec_t *fsec); +extern int DecodeISO8601Interval(char *str, + int *dtype, struct pg_tm * tm, fsec_t *fsec); + extern void DateTimeParseError(int dterr, const char *str, const char *datatype); diff --git a/src/test/regress/expected/interval.out b/src/test/regress/expected/interval.out index e8fee7a38e9..94a4275404a 100644 --- a/src/test/regress/expected/interval.out +++ b/src/test/regress/expected/interval.out @@ -646,3 +646,54 @@ SELECT interval '1 day -1 hours', +0-0 +1 -1:00:00 | +0-0 -1 +1:00:00 | +1-2 -3 +4:05:06.789 | -1-2 +3 -4:05:06.789 (1 row) +-- test outputting iso8601 intervals +SET IntervalStyle to iso_8601; +select interval '0' AS "zero", + interval '1-2' AS "a year 2 months", + interval '1 2:03:04' AS "a bit over a day", + interval '2:03:04.45679' AS "a bit over 2 hours", + (interval '1-2' + interval '3 4:05:06.7') AS "all fields", + (interval '1-2' - interval '3 4:05:06.7') AS "mixed sign", + (- interval '1-2' + interval '3 4:05:06.7') AS "negative"; + zero | a year 2 months | a bit over a day | a bit over 2 hours | all fields | mixed sign | negative +------+-----------------+------------------+--------------------+-------------------+-----------------------+--------------------- + PT0S | P1Y2M | P1DT2H3M4S | PT2H3M4.45679S | P1Y2M3DT4H5M6.70S | P1Y2M-3DT-4H-5M-6.70S | P-1Y-2M3DT4H5M6.70S +(1 row) + +-- test inputting ISO 8601 4.4.2.1 "Format With Time Unit Designators" +SET IntervalStyle to sql_standard; +select interval 'P0Y' AS "zero", + interval 'P1Y2M' AS "a year 2 months", + interval 'P1W' AS "a week", + interval 'P1DT2H3M4S' AS "a bit over a day", + interval 'P1Y2M3DT4H5M6.7S' AS "all fields", + interval 'P-1Y-2M-3DT-4H-5M-6.7S' AS "negative", + interval 'PT-0.1S' AS "fractional second"; + zero | a year 2 months | a week | a bit over a day | all fields | negative | fractional second +------+-----------------+-----------+------------------+---------------------+---------------------+------------------- + 0 | 1-2 | 7 0:00:00 | 1 2:03:04 | +1-2 +3 +4:05:06.70 | -1-2 -3 -4:05:06.70 | -0:00:00.10 +(1 row) + +-- test inputting ISO 8601 4.4.2.2 "Alternative Format" +SET IntervalStyle to postgres; +select interval 'P00021015T103020' AS "ISO8601 Basic Format", + interval 'P0002-10-15T10:30:20' AS "ISO8601 Extended Format"; + ISO8601 Basic Format | ISO8601 Extended Format +----------------------------------+---------------------------------- + 2 years 10 mons 15 days 10:30:20 | 2 years 10 mons 15 days 10:30:20 +(1 row) + +-- Make sure optional ISO8601 alternative format fields are optional. +select interval 'P0002' AS "year only", + interval 'P0002-10' AS "year month", + interval 'P0002-10-15' AS "year month day", + interval 'P0002T1S' AS "year only plus time", + interval 'P0002-10T1S' AS "year month plus time", + interval 'P0002-10-15T1S' AS "year month day plus time", + interval 'PT10' AS "hour only", + interval 'PT10:30' AS "hour minute"; + year only | year month | year month day | year only plus time | year month plus time | year month day plus time | hour only | hour minute +-----------+-----------------+-------------------------+---------------------+--------------------------+----------------------------------+-----------+------------- + 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) + diff --git a/src/test/regress/sql/interval.sql b/src/test/regress/sql/interval.sql index 9b32dd6f3b3..ce9560b0b09 100644 --- a/src/test/regress/sql/interval.sql +++ b/src/test/regress/sql/interval.sql @@ -200,3 +200,38 @@ SELECT interval '1 day -1 hours', interval '-1 days +1 hours', interval '1 years 2 months -3 days 4 hours 5 minutes 6.789 seconds', - interval '1 years 2 months -3 days 4 hours 5 minutes 6.789 seconds'; + +-- test outputting iso8601 intervals +SET IntervalStyle to iso_8601; +select interval '0' AS "zero", + interval '1-2' AS "a year 2 months", + interval '1 2:03:04' AS "a bit over a day", + interval '2:03:04.45679' AS "a bit over 2 hours", + (interval '1-2' + interval '3 4:05:06.7') AS "all fields", + (interval '1-2' - interval '3 4:05:06.7') AS "mixed sign", + (- interval '1-2' + interval '3 4:05:06.7') AS "negative"; + +-- test inputting ISO 8601 4.4.2.1 "Format With Time Unit Designators" +SET IntervalStyle to sql_standard; +select interval 'P0Y' AS "zero", + interval 'P1Y2M' AS "a year 2 months", + interval 'P1W' AS "a week", + interval 'P1DT2H3M4S' AS "a bit over a day", + interval 'P1Y2M3DT4H5M6.7S' AS "all fields", + interval 'P-1Y-2M-3DT-4H-5M-6.7S' AS "negative", + interval 'PT-0.1S' AS "fractional second"; + +-- test inputting ISO 8601 4.4.2.2 "Alternative Format" +SET IntervalStyle to postgres; +select interval 'P00021015T103020' AS "ISO8601 Basic Format", + interval 'P0002-10-15T10:30:20' AS "ISO8601 Extended Format"; + +-- Make sure optional ISO8601 alternative format fields are optional. +select interval 'P0002' AS "year only", + interval 'P0002-10' AS "year month", + interval 'P0002-10-15' AS "year month day", + interval 'P0002T1S' AS "year only plus time", + interval 'P0002-10T1S' AS "year month plus time", + interval 'P0002-10-15T1S' AS "year month day plus time", + interval 'PT10' AS "hour only", + interval 'PT10:30' AS "hour minute"; -- GitLab