aboutsummaryrefslogtreecommitdiff
path: root/src/backend/utils/adt/datetime.c
diff options
context:
space:
mode:
authorTom Lane <tgl@sss.pgh.pa.us>2008-11-09 00:28:35 +0000
committerTom Lane <tgl@sss.pgh.pa.us>2008-11-09 00:28:35 +0000
commitdf7641e25ab4da6f3a48222cbda0e121ccb32ad5 (patch)
tree29c33ccd2ff2a7b3c2abcd52cede74c79c4c56f7 /src/backend/utils/adt/datetime.c
parenteec501c4f773add9f30789efc28f8ce3bf2e5de9 (diff)
downloadpostgresql-df7641e25ab4da6f3a48222cbda0e121ccb32ad5.tar.gz
postgresql-df7641e25ab4da6f3a48222cbda0e121ccb32ad5.zip
Add a new GUC variable called "IntervalStyle" that decouples interval output
from DateStyle, and create a new interval style that produces output matching the SQL standard (at least for interval values that fall within the standard's restrictions). IntervalStyle is also used to resolve the conflict between the standard and traditional Postgres rules for interpreting negative interval input. Ron Mayer
Diffstat (limited to 'src/backend/utils/adt/datetime.c')
-rw-r--r--src/backend/utils/adt/datetime.c244
1 files changed, 200 insertions, 44 deletions
diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c
index 8efe2b22f2c..e91c470304f 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.196 2008/11/08 20:51:49 tgl Exp $
+ * $PostgreSQL: pgsql/src/backend/utils/adt/datetime.c,v 1.197 2008/11/09 00:28:34 tgl Exp $
*
*-------------------------------------------------------------------------
*/
@@ -3101,6 +3101,11 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
fmask |= tmask;
}
+ /* ensure that at least one time field has been found */
+ if (fmask == 0)
+ return DTERR_BAD_FORMAT;
+
+ /* ensure fractional seconds are fractional */
if (*fsec != 0)
{
int sec;
@@ -3114,6 +3119,60 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
tm->tm_sec += sec;
}
+ /*----------
+ * The SQL standard defines the interval literal
+ * '-1 1:00:00'
+ * to mean "negative 1 days and negative 1 hours", while Postgres
+ * traditionally treats this as meaning "negative 1 days and positive
+ * 1 hours". In SQL_STANDARD intervalstyle, we apply the leading sign
+ * to all fields if there are no other explicit signs.
+ *
+ * We leave the signs alone if there are additional explicit signs.
+ * This protects us against misinterpreting postgres-style dump output,
+ * since the postgres-style output code has always put an explicit sign on
+ * all fields following a negative field. But note that SQL-spec output
+ * is ambiguous and can be misinterpreted on load! (So it's best practice
+ * to dump in postgres style, not SQL style.)
+ *----------
+ */
+ if (IntervalStyle == INTSTYLE_SQL_STANDARD && *field[0] == '-')
+ {
+ /* Check for additional explicit signs */
+ bool more_signs = false;
+
+ for (i = 1; i < nf; i++)
+ {
+ if (*field[i] == '-' || *field[i] == '+')
+ {
+ more_signs = true;
+ break;
+ }
+ }
+
+ if (!more_signs)
+ {
+ /*
+ * Rather than re-determining which field was field[0], just
+ * force 'em all negative.
+ */
+ if (*fsec > 0)
+ *fsec = -(*fsec);
+ if (tm->tm_sec > 0)
+ tm->tm_sec = -tm->tm_sec;
+ if (tm->tm_min > 0)
+ tm->tm_min = -tm->tm_min;
+ if (tm->tm_hour > 0)
+ tm->tm_hour = -tm->tm_hour;
+ if (tm->tm_mday > 0)
+ tm->tm_mday = -tm->tm_mday;
+ if (tm->tm_mon > 0)
+ tm->tm_mon = -tm->tm_mon;
+ if (tm->tm_year > 0)
+ tm->tm_year = -tm->tm_year;
+ }
+ }
+
+ /* finally, AGO negates everything */
if (is_before)
{
*fsec = -(*fsec);
@@ -3125,10 +3184,6 @@ DecodeInterval(char **field, int *ftype, int nf, int range,
tm->tm_year = -tm->tm_year;
}
- /* ensure that at least one time field has been found */
- if (fmask == 0)
- return DTERR_BAD_FORMAT;
-
return 0;
}
@@ -3606,6 +3661,29 @@ EncodeDateTime(struct pg_tm * tm, fsec_t fsec, int *tzp, char **tzn, int style,
}
+/*
+ * Helper function to avoid duplicated code in EncodeInterval below.
+ * Note that any sign is stripped from the input seconds values.
+ */
+static void
+AppendSeconds(char *cp, int sec, fsec_t fsec)
+{
+ if (fsec == 0)
+ {
+ sprintf(cp, ":%02d", abs(sec));
+ }
+ else
+ {
+#ifdef HAVE_INT64_TIMESTAMP
+ sprintf(cp, ":%02d.%06d", abs(sec), Abs(fsec));
+#else
+ sprintf(cp, ":%012.9f", fabs(sec + fsec));
+#endif
+ TrimTrailingZeros(cp);
+ }
+}
+
+
/* EncodeInterval()
* Interpret time structure as a delta time and convert to string.
*
@@ -3613,23 +3691,115 @@ EncodeDateTime(struct pg_tm * tm, fsec_t fsec, int *tzp, char **tzn, int style,
* Actually, afaik ISO does not address time interval formatting,
* but this looks similar to the spec for absolute date/time.
* - thomas 1998-04-30
+ *
+ * Actually, afaik, ISO 8601 does specify formats for "time
+ * intervals...[of the]...format with time-unit designators", which
+ * are pretty ugly. The format looks something like
+ * P1Y1M1DT1H1M1.12345S
+ * but useful for exchanging data with computers instead of humans.
+ * - ron 2003-07-14
+ *
+ * And ISO's SQL 2008 standard specifies standards for
+ * "year-month literal"s (that look like '2-3') and
+ * "day-time literal"s (that look like ('4 5:6:7')
*/
int
EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str)
{
+ char *cp = str;
+ int year = tm->tm_year;
+ int mon = tm->tm_mon;
+ int mday = tm->tm_mday;
+ int hour = tm->tm_hour;
+ int min = tm->tm_min;
+ int sec = tm->tm_sec;
bool is_before = FALSE;
bool is_nonzero = FALSE;
- char *cp = str;
/*
* The sign of year and month are guaranteed to match, since they are
* stored internally as "month". But we'll need to check for is_before and
- * is_nonzero when determining the signs of hour/minute/seconds fields.
+ * is_nonzero when determining the signs of day and hour/minute/seconds
+ * fields.
*/
switch (style)
{
- /* compatible with ISO date formats */
- case USE_ISO_DATES:
+ /* SQL Standard interval format */
+ case INTSTYLE_SQL_STANDARD:
+ {
+ bool has_negative = year < 0 || mon < 0 ||
+ mday < 0 || hour < 0 ||
+ min < 0 || sec < 0 || fsec < 0;
+ bool has_positive = year > 0 || mon > 0 ||
+ mday > 0 || hour > 0 ||
+ min > 0 || sec > 0 || fsec > 0;
+ bool has_year_month = year != 0 || mon != 0;
+ bool has_day_time = mday != 0 || hour != 0 ||
+ min != 0 || sec != 0 || fsec != 0;
+ bool has_day = mday != 0;
+ bool sql_standard_value = !(has_negative && has_positive) &&
+ !(has_year_month && has_day_time);
+
+ /*
+ * SQL Standard wants only 1 "<sign>" preceding the whole
+ * interval ... but can't do that if mixed signs.
+ */
+ if (has_negative && sql_standard_value)
+ {
+ *cp++ = '-';
+ year = -year;
+ mon = -mon;
+ mday = -mday;
+ hour = -hour;
+ min = -min;
+ sec = -sec;
+ fsec = -fsec;
+ }
+
+ if (!has_negative && !has_positive)
+ {
+ sprintf(cp, "0");
+ }
+ else if (!sql_standard_value)
+ {
+ /*
+ * For non sql-standard interval values,
+ * force outputting the signs to avoid
+ * ambiguities with intervals with mixed
+ * sign components.
+ */
+ char year_sign = (year < 0 || mon < 0) ? '-' : '+';
+ 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",
+ year_sign, abs(year), abs(mon),
+ day_sign, abs(mday),
+ sec_sign, abs(hour), abs(min));
+ cp += strlen(cp);
+ AppendSeconds(cp, sec, fsec);
+ }
+ else if (has_year_month)
+ {
+ sprintf(cp, "%d-%d", year, mon);
+ }
+ else if (has_day)
+ {
+ sprintf(cp, "%d %d:%02d", mday, hour, min);
+ cp += strlen(cp);
+ AppendSeconds(cp, sec, fsec);
+ }
+ else
+ {
+ sprintf(cp, "%d:%02d", hour, min);
+ cp += strlen(cp);
+ AppendSeconds(cp, sec, fsec);
+ }
+ }
+ break;
+
+ /* Compatible with postgresql < 8.4 when DateStyle = 'iso' */
+ case INTSTYLE_POSTGRES:
if (tm->tm_year != 0)
{
sprintf(cp, "%d year%s",
@@ -3669,32 +3839,20 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str)
(minus ? "-" : (is_before ? "+" : "")),
abs(tm->tm_hour), abs(tm->tm_min));
cp += strlen(cp);
- /* Mark as "non-zero" since the fields are now filled in */
+ AppendSeconds(cp, tm->tm_sec, fsec);
+ cp += strlen(cp);
is_nonzero = TRUE;
-
- /* need fractional seconds? */
- if (fsec != 0)
- {
-#ifdef HAVE_INT64_TIMESTAMP
- sprintf(cp, ":%02d", abs(tm->tm_sec));
- cp += strlen(cp);
- sprintf(cp, ".%06d", Abs(fsec));
-#else
- fsec += tm->tm_sec;
- sprintf(cp, ":%012.9f", fabs(fsec));
-#endif
- TrimTrailingZeros(cp);
- cp += strlen(cp);
- }
- else
- {
- sprintf(cp, ":%02d", abs(tm->tm_sec));
- cp += strlen(cp);
- }
+ }
+ /* identically zero? then put in a unitless zero... */
+ if (!is_nonzero)
+ {
+ strcat(cp, "0");
+ cp += strlen(cp);
}
break;
- case USE_POSTGRES_DATES:
+ /* Compatible with postgresql < 8.4 when DateStyle != 'iso' */
+ case INTSTYLE_POSTGRES_VERBOSE:
default:
strcpy(cp, "@ ");
cp += strlen(cp);
@@ -3821,22 +3979,20 @@ EncodeInterval(struct pg_tm * tm, fsec_t fsec, int style, char *str)
is_before = (tm->tm_sec < 0);
is_nonzero = TRUE;
}
+ /* identically zero? then put in a unitless zero... */
+ if (!is_nonzero)
+ {
+ strcat(cp, "0");
+ cp += strlen(cp);
+ }
+ if (is_before)
+ {
+ strcat(cp, " ago");
+ cp += strlen(cp);
+ }
break;
}
- /* identically zero? then put in a unitless zero... */
- if (!is_nonzero)
- {
- strcat(cp, "0");
- cp += strlen(cp);
- }
-
- if (is_before && (style != USE_ISO_DATES))
- {
- strcat(cp, " ago");
- cp += strlen(cp);
- }
-
return 0;
} /* EncodeInterval() */