Fix INTERVAL output when year/month has different sign as day/hour etc.

Previously, all fields were unsigned, with only a trailing "ago" to
 indicate negative intervals. Now, ISO format does not use "ago", and
 and the traditional PostgreSQL format has the first numeric field unsigned
 with "ago" supporting that field. So "1 month - 2 days ago" is two days
 less than a month in the past.
Fix interval arithmetic across daylight savings time boundaries.
 Previously, most math across boundaries introduced a one hour offset.
Allow some date/time functions to return NULL if called with NULL args.
Implement functions for AT TIME ZONE support.
Support "SAT" as an Australian time zone if USE_AUSTRALIAN_RULES
 is defined.
This commit is contained in:
Thomas G. Lockhart 2000-11-06 15:57:00 +00:00
parent df9462ac05
commit 2cf1642461
2 changed files with 212 additions and 110 deletions

View File

@ -8,7 +8,7 @@
*
*
* IDENTIFICATION
* $Header: /cvsroot/pgsql/src/backend/utils/adt/datetime.c,v 1.54 2000/10/29 13:17:33 petere Exp $
* $Header: /cvsroot/pgsql/src/backend/utils/adt/datetime.c,v 1.55 2000/11/06 15:57:00 thomas Exp $
*
*-------------------------------------------------------------------------
*/
@ -199,7 +199,11 @@ static datetkn datetktbl[] = {
{"pst", TZ, NEG(48)}, /* Pacific Standard Time */
{"sadt", DTZ, 63}, /* S. Australian Dayl. Time */
{"sast", TZ, 57}, /* South Australian Std Time */
#if USE_AUSTRALIAN_RULES
{"sat", TZ, 57},
#else
{"sat", DOW, 6},
#endif
{"saturday", DOW, 6},
{"sep", MONTH, 9},
{"sept", MONTH, 9},
@ -218,8 +222,7 @@ static datetkn datetktbl[] = {
{"tue", DOW, 2},
{"tues", DOW, 2},
{"tuesday", DOW, 2},
{"undefined", RESERV, DTK_INVALID}, /* "undefined" pre-v6.1 invalid
* time */
{"undefined", RESERV, DTK_INVALID}, /* pre-v6.1 invalid time */
{"ut", TZ, 0},
{"utc", TZ, 0},
{"wadt", DTZ, 48}, /* West Australian DST */
@ -235,10 +238,10 @@ static datetkn datetktbl[] = {
{"ydt", DTZ, NEG(48)}, /* Yukon Daylight Time */
{YESTERDAY, RESERV, DTK_YESTERDAY}, /* yesterday midnight */
{"yst", TZ, NEG(54)}, /* Yukon Standard Time */
{"z", RESERV, DTK_ZULU}, /* 00:00:00 */
{"zp4", TZ, NEG(24)}, /* GMT +4 hours. */
{"zp5", TZ, NEG(30)}, /* GMT +5 hours. */
{"zp6", TZ, NEG(36)}, /* GMT +6 hours. */
{"z", RESERV, DTK_ZULU}, /* 00:00:00 */
{ZULU, RESERV, DTK_ZULU}, /* 00:00:00 */
};
@ -466,25 +469,6 @@ ParseDateTime(char *timestr, char *lowstr,
*/
if ((*cp == '-') || (*cp == '/') || (*cp == '.'))
{
#if 0
/*
* special case of Posix timezone "GMT-0800" Note that
* other sign (e.g. "GMT+0800" is recognized as two
* separate fields and handled later. XXX There is no room
* for a delimiter between the "GMT" and the "-0800", so
* we are going to just swallow the "GMT". But this leads
* to other troubles with the definition of signs, so we
* have to flip - thomas 2000-02-06
*/
if ((*cp == '-') && isdigit(*(cp + 1))
&& (strncmp(field[nf], "gmt", 3) == 0))
{
*cp = '+';
continue;
}
#endif
ftype[nf] = DTK_DATE;
while (isdigit((int) *cp) || (*cp == '-') || (*cp == '/') || (*cp == '.'))
*lp++ = tolower(*cp++);
@ -1667,8 +1651,7 @@ DecodeDateDelta(char **field, int *ftype, int nf, int *dtype, struct tm * tm, do
tmask,
type;
int i;
int flen,
val;
int val;
double fval;
double sec;
@ -1695,14 +1678,40 @@ DecodeDateDelta(char **field, int *ftype, int nf, int *dtype, struct tm * tm, do
break;
case DTK_TZ:
/*
* Timezone is a token with a leading sign character and
* otherwise the same as a non-signed numeric field
* otherwise the same as a non-signed time field
*/
Assert((*field[i] == '-') || (*field[i] == '+'));
/* A single signed number ends up here, but will be rejected by DecodeTime().
* So, work this out to drop through to DTK_NUMBER, which *can* tolerate this.
*/
cp = field[i]+1;
while ((*cp != '\0') && (*cp != ':'))
cp++;
if ((*cp == ':')
&& (DecodeTime((field[i]+1), fmask, &tmask, tm, fsec) == 0)) {
if (*field[i] == '-') {
/* flip the sign on all fields */
tm->tm_hour = -tm->tm_hour;
tm->tm_min = -tm->tm_min;
tm->tm_sec = -tm->tm_sec;
*fsec = -(*fsec);
}
/* Set the next type to be a day, if units are not specified.
* This handles the case of '1 +02:03' since we are reading right to left.
*/
type = DTK_DAY;
tmask = DTK_M(TZ);
break;
}
/* DROP THROUGH */
case DTK_DATE:
case DTK_NUMBER:
val = strtol(field[i], &cp, 10);
if (*cp == '.')
{
fval = strtod(cp, &cp);
@ -1717,7 +1726,6 @@ DecodeDateDelta(char **field, int *ftype, int nf, int *dtype, struct tm * tm, do
else
return -1;
flen = strlen(field[i]);
tmask = 0; /* DTK_M(type); */
switch (type)
@ -2193,98 +2201,126 @@ EncodeTimeSpan(struct tm * tm, double fsec, int style, char *str)
int 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.
*/
switch (style)
{
/* compatible with ISO date formats */
case USE_ISO_DATES:
break;
if (tm->tm_year != 0)
{
sprintf(cp, "%d year%s",
tm->tm_year, ((tm->tm_year != 1) ? "s" : ""));
cp += strlen(cp);
is_nonzero = TRUE;
}
default:
strcpy(cp, "@ ");
cp += strlen(cp);
break;
}
if (tm->tm_mon != 0)
{
sprintf(cp, "%s%d mon%s", (is_nonzero ? " " : ""),
tm->tm_mon, ((tm->tm_mon != 1) ? "s" : ""));
cp += strlen(cp);
is_nonzero = TRUE;
}
if (tm->tm_year != 0)
{
is_before |= (tm->tm_year < 0);
sprintf(cp, "%d year%s",
abs(tm->tm_year), ((abs(tm->tm_year) != 1) ? "s" : ""));
cp += strlen(cp);
is_nonzero = TRUE;
}
if (tm->tm_mon != 0)
{
is_before |= (tm->tm_mon < 0);
sprintf(cp, "%s%d mon%s", (is_nonzero ? " " : ""),
abs(tm->tm_mon), ((abs(tm->tm_mon) != 1) ? "s" : ""));
cp += strlen(cp);
is_nonzero = TRUE;
}
switch (style)
{
/* compatible with ISO date formats */
case USE_ISO_DATES:
if (tm->tm_mday != 0)
{
is_before |= (tm->tm_mday < 0);
sprintf(cp, "%s%d", (is_nonzero ? " " : ""), abs(tm->tm_mday));
sprintf(cp, "%s%d", (is_nonzero ? " " : ""), tm->tm_mday);
cp += strlen(cp);
is_nonzero = TRUE;
}
is_before |= ((tm->tm_hour < 0) || (tm->tm_min < 0));
sprintf(cp, "%s%02d:%02d", (is_nonzero ? " " : ""),
abs(tm->tm_hour), abs(tm->tm_min));
cp += strlen(cp);
/* Mark as "non-zero" since the fields are now filled in */
is_nonzero = TRUE;
/* fractional seconds? */
if (fsec != 0)
{
fsec += tm->tm_sec;
is_before |= (fsec < 0);
sprintf(cp, ":%05.2f", fabs(fsec));
int minus = ((tm->tm_hour < 0) || (tm->tm_min < 0)
|| (tm->tm_sec < 0) || (fsec < 0));
sprintf(cp, "%s%s%02d:%02d", (is_nonzero ? " " : ""),
(minus ? "-" : "+"),
abs(tm->tm_hour), abs(tm->tm_min));
cp += strlen(cp);
/* Mark as "non-zero" since the fields are now filled in */
is_nonzero = TRUE;
/* otherwise, integer seconds only? */
}
else if (tm->tm_sec != 0)
{
is_before |= (tm->tm_sec < 0);
sprintf(cp, ":%02d", abs(tm->tm_sec));
cp += strlen(cp);
is_nonzero = TRUE;
/* fractional seconds? */
if (fsec != 0)
{
fsec += tm->tm_sec;
sprintf(cp, ":%05.2f", fabs(fsec));
cp += strlen(cp);
is_nonzero = TRUE;
/* otherwise, integer seconds only? */
}
else if (tm->tm_sec != 0)
{
sprintf(cp, ":%02d", abs(tm->tm_sec));
cp += strlen(cp);
is_nonzero = TRUE;
}
}
break;
case USE_POSTGRES_DATES:
default:
strcpy(cp, "@ ");
cp += strlen(cp);
if (tm->tm_year != 0)
{
is_before = (tm->tm_year < 0);
if (is_before)
tm->tm_year = -tm->tm_year;
sprintf(cp, "%d year%s",
tm->tm_year, ((tm->tm_year != 1) ? "s" : ""));
cp += strlen(cp);
is_nonzero = TRUE;
}
if (tm->tm_mon != 0)
{
if (! is_nonzero)
is_before = (tm->tm_mon < 0);
if (is_before)
tm->tm_mon = -tm->tm_mon;
sprintf(cp, "%s%d mon%s", (is_nonzero ? " " : ""),
tm->tm_mon, ((tm->tm_mon != 1) ? "s" : ""));
cp += strlen(cp);
is_nonzero = TRUE;
}
if (tm->tm_mday != 0)
{
is_before |= (tm->tm_mday < 0);
if (! is_nonzero)
is_before = (tm->tm_mday < 0);
if (is_before)
tm->tm_mday = -tm->tm_mday;
sprintf(cp, "%s%d day%s", (is_nonzero ? " " : ""),
abs(tm->tm_mday), ((abs(tm->tm_mday) != 1) ? "s" : ""));
tm->tm_mday, ((tm->tm_mday != 1) ? "s" : ""));
cp += strlen(cp);
is_nonzero = TRUE;
}
if (tm->tm_hour != 0)
{
is_before |= (tm->tm_hour < 0);
if (! is_nonzero)
is_before = (tm->tm_hour < 0);
if (is_before)
tm->tm_hour = -tm->tm_hour;
sprintf(cp, "%s%d hour%s", (is_nonzero ? " " : ""),
abs(tm->tm_hour), ((abs(tm->tm_hour) != 1) ? "s" : ""));
tm->tm_hour, ((tm->tm_hour != 1) ? "s" : ""));
cp += strlen(cp);
is_nonzero = TRUE;
}
if (tm->tm_min != 0)
{
is_before |= (tm->tm_min < 0);
if (! is_nonzero)
is_before = (tm->tm_min < 0);
if (is_before)
tm->tm_min = -tm->tm_min;
sprintf(cp, "%s%d min%s", (is_nonzero ? " " : ""),
abs(tm->tm_min), ((abs(tm->tm_min) != 1) ? "s" : ""));
tm->tm_min, ((tm->tm_min != 1) ? "s" : ""));
cp += strlen(cp);
is_nonzero = TRUE;
}
@ -2293,8 +2329,11 @@ EncodeTimeSpan(struct tm * tm, double fsec, int style, char *str)
if (fsec != 0)
{
fsec += tm->tm_sec;
is_before |= (fsec < 0);
sprintf(cp, "%s%.2f secs", (is_nonzero ? " " : ""), fabs(fsec));
if (! is_nonzero)
is_before = (fsec < 0);
if (is_before)
fsec = -fsec;
sprintf(cp, "%s%.2f secs", (is_nonzero ? " " : ""), fsec);
cp += strlen(cp);
is_nonzero = TRUE;
@ -2302,9 +2341,12 @@ EncodeTimeSpan(struct tm * tm, double fsec, int style, char *str)
}
else if (tm->tm_sec != 0)
{
is_before |= (tm->tm_sec < 0);
if (! is_nonzero)
is_before = (tm->tm_sec < 0);
if (is_before)
tm->tm_sec = -tm->tm_sec;
sprintf(cp, "%s%d sec%s", (is_nonzero ? " " : ""),
abs(tm->tm_sec), ((abs(tm->tm_sec) != 1) ? "s" : ""));
tm->tm_sec, ((tm->tm_sec != 1) ? "s" : ""));
cp += strlen(cp);
is_nonzero = TRUE;
}
@ -2312,7 +2354,7 @@ EncodeTimeSpan(struct tm * tm, double fsec, int style, char *str)
}
/* identically zero? then put in a unitless zero... */
if (!is_nonzero)
if (! is_nonzero)
{
strcat(cp, "0");
cp += strlen(cp);

View File

@ -8,7 +8,7 @@
*
*
* IDENTIFICATION
* $Header: /cvsroot/pgsql/src/backend/utils/adt/timestamp.c,v 1.36 2000/10/29 13:17:34 petere Exp $
* $Header: /cvsroot/pgsql/src/backend/utils/adt/timestamp.c,v 1.37 2000/11/06 15:57:00 thomas Exp $
*
*-------------------------------------------------------------------------
*/
@ -1000,6 +1000,39 @@ timestamp_pl_span(PG_FUNCTION_ARGS)
if (tm->tm_mday > day_tab[isleap(tm->tm_year)][tm->tm_mon - 1])
tm->tm_mday = (day_tab[isleap(tm->tm_year)][tm->tm_mon - 1]);
if (IS_VALID_UTIME(tm->tm_year, tm->tm_mon, tm->tm_mday))
{
#if defined(HAVE_TM_ZONE) || defined(HAVE_INT_TIMEZONE)
tm->tm_isdst = -1;
tm->tm_year -= 1900;
tm->tm_mon -= 1;
tm->tm_isdst = -1;
mktime(tm);
tm->tm_year += 1900;
tm->tm_mon += 1;
# if defined(HAVE_TM_ZONE)
tz = -(tm->tm_gmtoff); /* tm_gmtoff is Sun/DEC-ism */
# elif defined(HAVE_INT_TIMEZONE)
# ifdef __CYGWIN__
tz = (tm->tm_isdst ? (_timezone - 3600) : _timezone);
# else
tz = (tm->tm_isdst ? (timezone - 3600) : timezone);
# endif
# endif
#else /* not (HAVE_TM_ZONE || HAVE_INT_TIMEZONE) */
tz = CTimeZone;
#endif
}
else
{
tm->tm_isdst = 0;
tz = 0;
}
if (tm2timestamp(tm, fsec, &tz, &dt) != 0)
elog(ERROR, "Unable to add timestamp and interval");
@ -1571,12 +1604,7 @@ timestamp_trunc(PG_FUNCTION_ARGS)
if (TIMESTAMP_NOT_FINITE(timestamp))
{
#if NOT_USED
/* should return null but Postgres doesn't like that currently. - tgl 97/06/12 */
elog(ERROR, "Timestamp is not finite");
#endif
result = 0;
PG_RETURN_NULL();
}
else
{
@ -1902,10 +1930,6 @@ timestamp_part(PG_FUNCTION_ARGS)
if (TIMESTAMP_NOT_FINITE(timestamp))
{
#if NOT_USED
/* should return null but Postgres doesn't like that currently. - tgl 97/06/12 */
elog(ERROR, "Timestamp is not finite", NULL);
#endif
PG_RETURN_NULL();
}
else
@ -2197,15 +2221,7 @@ timestamp_zone(PG_FUNCTION_ARGS)
if (TIMESTAMP_NOT_FINITE(timestamp))
{
/*
* could return null but Postgres doesn't like that currently. -
* tgl 97/06/12
*
* Could do it now if you wanted ... the other tgl 2000/06/08
*/
elog(ERROR, "Timestamp is not finite");
result = NULL;
PG_RETURN_NULL();
}
else if ((type == TZ) || (type == DTZ))
{
@ -2241,4 +2257,48 @@ timestamp_zone(PG_FUNCTION_ARGS)
}
PG_RETURN_TEXT_P(result);
}
} /* timestamp_zone() */
/* timestamp_izone()
* Encode timestamp type with specified time interval as time zone.
* Require ISO-formatted result, since character-string time zone is not available.
*/
Datum
timestamp_izone(PG_FUNCTION_ARGS)
{
Interval *zone = PG_GETARG_INTERVAL_P(0);
Timestamp timestamp = PG_GETARG_TIMESTAMP(1);
text *result;
Timestamp dt;
int tz;
char *tzn = "";
double fsec;
struct tm tt,
*tm = &tt;
char buf[MAXDATELEN + 1];
int len;
if (TIMESTAMP_NOT_FINITE(timestamp))
PG_RETURN_NULL();
if (zone->month != 0)
elog(ERROR, "INTERVAL time zone not legal (month specified)");
tm->tm_isdst = -1;
tz = -(zone->time);
dt = (TIMESTAMP_IS_RELATIVE(timestamp) ? SetTimestamp(timestamp) : timestamp);
dt = dt2local(dt, tz);
if (timestamp2tm(dt, NULL, tm, &fsec, NULL) != 0)
elog(ERROR, "Timestamp not legal");
EncodeDateTime(tm, fsec, &tz, &tzn, USE_ISO_DATES, buf);
len = (strlen(buf) + VARHDRSZ);
result = palloc(len);
VARATT_SIZEP(result) = len;
memmove(VARDATA(result), buf, (len - VARHDRSZ));
PG_RETURN_TEXT_P(result);
} /* timestamp_izone() */