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