Commit a4917bef authored by Tom Lane's avatar Tom Lane

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
parent a44564b4
<!-- $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"> <chapter Id="runtime-config">
<title>Server Configuration</title> <title>Server Configuration</title>
...@@ -4032,6 +4032,9 @@ SET XML OPTION { DOCUMENT | CONTENT }; ...@@ -4032,6 +4032,9 @@ SET XML OPTION { DOCUMENT | CONTENT };
matching <productname>PostgreSQL</> releases prior to 8.4 matching <productname>PostgreSQL</> releases prior to 8.4
when the <varname>DateStyle</> when the <varname>DateStyle</>
parameter was set to non-<literal>ISO</> output. 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>
<para> <para>
The <varname>IntervalStyle</> parameter also affects the The <varname>IntervalStyle</> parameter also affects the
......
<!-- $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"> <chapter id="datatype">
<title id="datatype-title">Data Types</title> <title id="datatype-title">Data Types</title>
...@@ -2353,9 +2353,9 @@ January 8 04:05:06 1999 PST ...@@ -2353,9 +2353,9 @@ January 8 04:05:06 1999 PST
<type>interval</type> values can be written with the following <type>interval</type> values can be written with the following
verbose syntax: verbose syntax:
<programlisting> <synopsis>
<optional>@</> <replaceable>quantity</> <replaceable>unit</> <optional><replaceable>quantity</> <replaceable>unit</>...</> <optional><replaceable>direction</></optional> <optional>@</> <replaceable>quantity</> <replaceable>unit</> <optional><replaceable>quantity</> <replaceable>unit</>...</> <optional><replaceable>direction</></optional>
</programlisting> </synopsis>
where <replaceable>quantity</> is a number (possibly signed); where <replaceable>quantity</> is a number (possibly signed);
<replaceable>unit</> is <literal>microsecond</literal>, <replaceable>unit</> is <literal>microsecond</literal>,
...@@ -2384,6 +2384,76 @@ January 8 04:05:06 1999 PST ...@@ -2384,6 +2384,76 @@ January 8 04:05:06 1999 PST
<varname>IntervalStyle</> is set to <literal>sql_standard</literal>.) <varname>IntervalStyle</> is set to <literal>sql_standard</literal>.)
</para> </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> <para>
When writing an interval constant with a <replaceable>fields</> When writing an interval constant with a <replaceable>fields</>
specification, or when assigning to an interval column that was defined specification, or when assigning to an interval column that was defined
...@@ -2433,6 +2503,46 @@ January 8 04:05:06 1999 PST ...@@ -2433,6 +2503,46 @@ January 8 04:05:06 1999 PST
For example, <literal>'1.5 month'</> becomes 1 month and 15 days. For example, <literal>'1.5 month'</> becomes 1 month and 15 days.
Only seconds will ever be shown as fractional on output. Only seconds will ever be shown as fractional on output.
</para> </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>
<sect2 id="datatype-interval-output"> <sect2 id="datatype-interval-output">
...@@ -2446,8 +2556,8 @@ January 8 04:05:06 1999 PST ...@@ -2446,8 +2556,8 @@ January 8 04:05:06 1999 PST
<para> <para>
The output format of the interval type can be set to one of the The output format of the interval type can be set to one of the
three styles <literal>sql_standard</>, four styles <literal>sql_standard</>, <literal>postgres</>,
<literal>postgres</>, or <literal>postgres_verbose</>, <literal>postgres_verbose</>, or <literal>iso_8601</>,
using the command <literal>SET intervalstyle</literal>. using the command <literal>SET intervalstyle</literal>.
The default is the <literal>postgres</> format. The default is the <literal>postgres</> format.
<xref linkend="interval-style-output-table"> shows examples of each <xref linkend="interval-style-output-table"> shows examples of each
...@@ -2476,6 +2586,12 @@ January 8 04:05:06 1999 PST ...@@ -2476,6 +2586,12 @@ January 8 04:05:06 1999 PST
<varname>DateStyle</> parameter was set to non-<literal>ISO</> output. <varname>DateStyle</> parameter was set to non-<literal>ISO</> output.
</para> </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"> <table id="interval-style-output-table">
<title>Interval Output Style Examples</title> <title>Interval Output Style Examples</title>
<tgroup cols="4"> <tgroup cols="4">
...@@ -2506,6 +2622,12 @@ January 8 04:05:06 1999 PST ...@@ -2506,6 +2622,12 @@ January 8 04:05:06 1999 PST
<entry>@ 3 days 4 hours 5 mins 6 secs</entry> <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> <entry>@ 1 year 2 mons -3 days 4 hours 5 mins 6 secs ago</entry>
</row> </row>
<row>
<entry><literal>iso_8601</></entry>
<entry>P1Y2M</entry>
<entry>P3DT4H5M6S</entry>
<entry>P-1Y-2M3DT-4H-5M-6S</entry>
</row>
</tbody> </tbody>
</tgroup> </tgroup>
</table> </table>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
* *
* *
* IDENTIFICATION * 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) ...@@ -2726,6 +2726,7 @@ DecodeSpecial(int field, char *lowtoken, int *val)
/* DecodeInterval() /* DecodeInterval()
* Interpret previously parsed fields for general time interval. * Interpret previously parsed fields for general time interval.
* Returns 0 if successful, DTERR code if bogus input detected. * 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 * Allow "date" field DTK_DATE since this could be just
* an unsigned floating point number. - thomas 1997-11-16 * an unsigned floating point number. - thomas 1997-11-16
...@@ -3188,6 +3189,307 @@ DecodeInterval(char **field, int *ftype, int nf, int range, ...@@ -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() /* DecodeUnits()
* Decode text string using lookup table. * Decode text string using lookup table.
* This routine supports time interval decoding * This routine supports time interval decoding
...@@ -3662,27 +3964,39 @@ EncodeDateTime(struct pg_tm * tm, fsec_t fsec, int *tzp, char **tzn, int style, ...@@ -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. * Note that any sign is stripped from the input seconds values.
*/ */
static void static void
AppendSeconds(char *cp, int sec, fsec_t fsec) AppendSeconds(char *cp, int sec, fsec_t fsec, bool fillzeros)
{ {
if (fsec == 0) if (fsec == 0)
{ {
sprintf(cp, ":%02d", abs(sec)); sprintf(cp, fillzeros ? "%02d" : "%d", abs(sec));
} }
else else
{ {
#ifdef HAVE_INT64_TIMESTAMP #ifdef HAVE_INT64_TIMESTAMP
sprintf(cp, ":%02d.%06d", abs(sec), Abs(fsec)); sprintf(cp, fillzeros ? "%02d.%06d" : "%d.%06d", abs(sec), Abs(fsec));
#else #else
sprintf(cp, ":%012.9f", fabs(sec + fsec)); sprintf(cp, fillzeros ? "%012.9f" : "%.9f", fabs(sec + fsec));
#endif #endif
TrimTrailingZeros(cp); 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() /* EncodeInterval()
* Interpret time structure as a delta time and convert to string. * 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) ...@@ -3772,12 +4086,12 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str)
char day_sign = (mday < 0) ? '-' : '+'; char day_sign = (mday < 0) ? '-' : '+';
char sec_sign = (hour < 0 || min < 0 || sec < 0 || fsec < 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), year_sign, abs(year), abs(mon),
day_sign, abs(mday), day_sign, abs(mday),
sec_sign, abs(hour), abs(min)); sec_sign, abs(hour), abs(min));
cp += strlen(cp); cp += strlen(cp);
AppendSeconds(cp, sec, fsec); AppendSeconds(cp, sec, fsec, true);
} }
else if (has_year_month) else if (has_year_month)
{ {
...@@ -3785,19 +4099,47 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str) ...@@ -3785,19 +4099,47 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str)
} }
else if (has_day) else if (has_day)
{ {
sprintf(cp, "%d %d:%02d", mday, hour, min); sprintf(cp, "%d %d:%02d:", mday, hour, min);
cp += strlen(cp); cp += strlen(cp);
AppendSeconds(cp, sec, fsec); AppendSeconds(cp, sec, fsec, true);
} }
else else
{ {
sprintf(cp, "%d:%02d", hour, min); sprintf(cp, "%d:%02d:", hour, min);
cp += strlen(cp); cp += strlen(cp);
AppendSeconds(cp, sec, fsec); AppendSeconds(cp, sec, fsec, true);
} }
} }
break; 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' */ /* Compatible with postgresql < 8.4 when DateStyle = 'iso' */
case INTSTYLE_POSTGRES: case INTSTYLE_POSTGRES:
if (tm->tm_year != 0) if (tm->tm_year != 0)
...@@ -3835,11 +4177,11 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str) ...@@ -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 || int minus = (tm->tm_hour < 0 || tm->tm_min < 0 ||
tm->tm_sec < 0 || fsec < 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 ? "+" : "")), (minus ? "-" : (is_before ? "+" : "")),
abs(tm->tm_hour), abs(tm->tm_min)); abs(tm->tm_hour), abs(tm->tm_min));
cp += strlen(cp); cp += strlen(cp);
AppendSeconds(cp, tm->tm_sec, fsec); AppendSeconds(cp, tm->tm_sec, fsec, true);
cp += strlen(cp); cp += strlen(cp);
is_nonzero = TRUE; is_nonzero = TRUE;
} }
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
* *
* *
* IDENTIFICATION * 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) ...@@ -634,6 +634,12 @@ reltimein(PG_FUNCTION_ARGS)
if (dterr == 0) if (dterr == 0)
dterr = DecodeInterval(field, ftype, nf, INTERVAL_FULL_RANGE, dterr = DecodeInterval(field, ftype, nf, INTERVAL_FULL_RANGE,
&dtype, tm, &fsec); &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 != 0)
{ {
if (dterr == DTERR_FIELD_OVERFLOW) if (dterr == DTERR_FIELD_OVERFLOW)
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
* *
* *
* IDENTIFICATION * 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) ...@@ -626,7 +626,14 @@ interval_in(PG_FUNCTION_ARGS)
dterr = ParseDateTime(str, workbuf, sizeof(workbuf), field, dterr = ParseDateTime(str, workbuf, sizeof(workbuf), field,
ftype, MAXDATEFIELDS, &nf); ftype, MAXDATEFIELDS, &nf);
if (dterr == 0) 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 != 0)
{ {
if (dterr == DTERR_FIELD_OVERFLOW) if (dterr == DTERR_FIELD_OVERFLOW)
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
* Written by Peter Eisentraut <peter_e@gmx.net>. * Written by Peter Eisentraut <peter_e@gmx.net>.
* *
* IDENTIFICATION * 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[] = { ...@@ -217,6 +217,7 @@ static const struct config_enum_entry intervalstyle_options[] = {
{"postgres", INTSTYLE_POSTGRES, false}, {"postgres", INTSTYLE_POSTGRES, false},
{"postgres_verbose", INTSTYLE_POSTGRES_VERBOSE, false}, {"postgres_verbose", INTSTYLE_POSTGRES_VERBOSE, false},
{"sql_standard", INTSTYLE_SQL_STANDARD, false}, {"sql_standard", INTSTYLE_SQL_STANDARD, false},
{"iso_8601", INTSTYLE_ISO_8601, false},
{NULL, 0, false} {NULL, 0, false}
}; };
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* *
* Copyright (c) 2000-2008, PostgreSQL Global Development Group * 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) ...@@ -1959,7 +1959,7 @@ psql_completion(char *text, int start, int end)
else if (pg_strcasecmp(prev2_wd, "IntervalStyle") == 0) else if (pg_strcasecmp(prev2_wd, "IntervalStyle") == 0)
{ {
static const char *const my_list[] = 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); COMPLETE_WITH_LIST(my_list);
} }
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
* Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California * 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 * NOTES
* some of the information in this file should be moved to other files. * some of the information in this file should be moved to other files.
...@@ -197,10 +197,12 @@ extern int DateOrder; ...@@ -197,10 +197,12 @@ extern int DateOrder;
* INTSTYLE_POSTGRES Like Postgres < 8.4 when DateStyle = 'iso' * INTSTYLE_POSTGRES Like Postgres < 8.4 when DateStyle = 'iso'
* INTSTYLE_POSTGRES_VERBOSE 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_SQL_STANDARD SQL standard interval literals
* INTSTYLE_ISO_8601 ISO-8601-basic formatted intervals
*/ */
#define INTSTYLE_POSTGRES 0 #define INTSTYLE_POSTGRES 0
#define INTSTYLE_POSTGRES_VERBOSE 1 #define INTSTYLE_POSTGRES_VERBOSE 1
#define INTSTYLE_SQL_STANDARD 2 #define INTSTYLE_SQL_STANDARD 2
#define INTSTYLE_ISO_8601 3
extern int IntervalStyle; extern int IntervalStyle;
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
* Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group * Portions Copyright (c) 1996-2008, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California * 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, ...@@ -289,9 +289,11 @@ extern int DecodeDateTime(char **field, int *ftype,
extern int DecodeTimeOnly(char **field, int *ftype, extern int DecodeTimeOnly(char **field, int *ftype,
int nf, int *dtype, int nf, int *dtype,
struct pg_tm * tm, fsec_t *fsec, int *tzp); struct pg_tm * tm, fsec_t *fsec, int *tzp);
extern int DecodeInterval(char **field, int *ftype, extern int DecodeInterval(char **field, int *ftype, int nf, int range,
int nf, int range, int *dtype, int *dtype, struct pg_tm * tm, fsec_t *fsec);
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, extern void DateTimeParseError(int dterr, const char *str,
const char *datatype); const char *datatype);
......
...@@ -646,3 +646,54 @@ SELECT interval '1 day -1 hours', ...@@ -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 +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) (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)
...@@ -200,3 +200,38 @@ SELECT interval '1 day -1 hours', ...@@ -200,3 +200,38 @@ SELECT interval '1 day -1 hours',
interval '-1 days +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',
- 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";
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment