From: Tom Lane Date: Mon, 1 Nov 2004 21:34:44 +0000 (+0000) Subject: Invent pg_next_dst_boundary() and rewrite DetermineLocalTimeZone() to X-Git-Tag: REL8_0_0BETA5~169 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=5ba04cd9f17ac91cce7460310a2a6cbd714b7e8a;p=postgresql Invent pg_next_dst_boundary() and rewrite DetermineLocalTimeZone() to use it, as per my proposal of yesterday. This gives us a means of determining the zone offset to impute to an unlabeled timestamp that is both efficient and reliable, unlike all our previous tries involving mktime() and localtime(). The behavior for invalid or ambiguous times at a DST transition is fixed to be really and truly "assume standard time", fixing a bug that has come and gone repeatedly but was back again in 7.4. (There is some ongoing discussion about whether we should raise an error instead, but for the moment I'll make it do what it was previously intended to do.) --- diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index 4459d286c9..e47edb35b9 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.134 2004/08/30 02:54:39 momjian Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/datetime.c,v 1.135 2004/11/01 21:34:38 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -1576,24 +1576,25 @@ DecodeDateTime(char **field, int *ftype, int nf, * tm_isdst field accordingly, and return the actual timezone offset. * * Note: it might seem that we should use mktime() for this, but bitter - * experience teaches otherwise. In particular, mktime() is generally - * incapable of coping reasonably with "impossible" times within a - * spring-forward DST transition. Typical implementations of mktime() - * turn out to be loops around localtime() anyway, so they're not even - * any faster than this code. + * experience teaches otherwise. This code is much faster than most versions + * of mktime(), anyway. */ int DetermineLocalTimeZone(struct pg_tm * tm) { - int tz; - int date , + int date, sec; pg_time_t day, - mysec, - locsec, - delta1, - delta2; - struct pg_tm *tx; + mytime, + prevtime, + boundary, + beforetime, + aftertime; + long int before_gmtoff, + after_gmtoff; + int before_isdst, + after_isdst; + int res; if (HasCTZSet) { @@ -1615,82 +1616,71 @@ DetermineLocalTimeZone(struct pg_tm * tm) if (day / 86400 != date) goto overflow; sec = tm->tm_sec + (tm->tm_min + tm->tm_hour * 60) * 60; - mysec = day + sec; - /* since sec >= 0, overflow could only be from +day to -mysec */ - if (mysec < 0 && day > 0) + mytime = day + sec; + /* since sec >= 0, overflow could only be from +day to -mytime */ + if (mytime < 0 && day > 0) goto overflow; /* - * Use pg_localtime to convert that pg_time_t to broken-down time, and - * reassemble to get a representation of local time. (We could get - * overflow of a few hours in the result, but the delta calculation - * should still work.) + * Find the DST time boundary just before or following the target time. + * We assume that all zones have GMT offsets less than 24 hours, and + * that DST boundaries can't be closer together than 48 hours, so + * backing up 24 hours and finding the "next" boundary will work. */ - tx = pg_localtime(&mysec); - if (!tx) - goto overflow; /* probably can't happen */ - day = date2j(tx->tm_year + 1900, tx->tm_mon + 1, tx->tm_mday) - - UNIX_EPOCH_JDATE; - locsec = tx->tm_sec + (tx->tm_min + (day * 24 + tx->tm_hour) * 60) * 60; + prevtime = mytime - (24 * 60 * 60); + if (mytime < 0 && prevtime > 0) + goto overflow; - /* - * The local time offset corresponding to that GMT time is now - * computable as mysec - locsec. - */ - delta1 = mysec - locsec; + res = pg_next_dst_boundary(&prevtime, + &before_gmtoff, &before_isdst, + &boundary, + &after_gmtoff, &after_isdst); + if (res < 0) + goto overflow; /* failure? */ + + if (res == 0) + { + /* Non-DST zone, life is simple */ + tm->tm_isdst = before_isdst; + return - (int) before_gmtoff; + } /* - * However, if that GMT time and the local time we are actually - * interested in are on opposite sides of a daylight-savings-time - * transition, then this is not the time offset we want. So, adjust - * the pg_time_t to be what we think the GMT time corresponding to our - * target local time is, and repeat the pg_localtime() call and delta - * calculation. - * - * We have to watch out for overflow while adjusting the pg_time_t. + * Form the candidate pg_time_t values with local-time adjustment */ - if ((delta1 < 0) ? (mysec < 0 && (mysec + delta1) > 0) : - (mysec > 0 && (mysec + delta1) < 0)) + beforetime = mytime - before_gmtoff; + if ((before_gmtoff > 0) ? (mytime < 0 && beforetime > 0) : + (mytime > 0 && beforetime < 0)) + goto overflow; + aftertime = mytime - after_gmtoff; + if ((after_gmtoff > 0) ? (mytime < 0 && aftertime > 0) : + (mytime > 0 && aftertime < 0)) goto overflow; - mysec += delta1; - tx = pg_localtime(&mysec); - if (!tx) - goto overflow; /* probably can't happen */ - day = date2j(tx->tm_year + 1900, tx->tm_mon + 1, tx->tm_mday) - - UNIX_EPOCH_JDATE; - locsec = tx->tm_sec + (tx->tm_min + (day * 24 + tx->tm_hour) * 60) * 60; - delta2 = mysec - locsec; /* - * We may have to do it again to get the correct delta. - * - * It might seem we should just loop until we get the same delta twice in - * a row, but if we've been given an "impossible" local time (in the - * gap during a spring-forward transition) we'd never get out of the - * loop. The behavior we want is that "impossible" times are taken as - * standard time, and also that ambiguous times (during a fall-back - * transition) are taken as standard time. Therefore, we bias the code - * to prefer the standard-time solution. + * If both before or both after the boundary time, we know what to do */ - if (delta2 != delta1 && tx->tm_isdst != 0) + if (beforetime <= boundary && aftertime < boundary) { - delta2 -= delta1; - if ((delta2 < 0) ? (mysec < 0 && (mysec + delta2) > 0) : - (mysec > 0 && (mysec + delta2) < 0)) - goto overflow; - mysec += delta2; - tx = pg_localtime(&mysec); - if (!tx) - goto overflow; /* probably can't happen */ - day = date2j(tx->tm_year + 1900, tx->tm_mon + 1, tx->tm_mday) - - UNIX_EPOCH_JDATE; - locsec = tx->tm_sec + (tx->tm_min + (day * 24 + tx->tm_hour) * 60) * 60; - delta2 = mysec - locsec; + tm->tm_isdst = before_isdst; + return - (int) before_gmtoff; } - tm->tm_isdst = tx->tm_isdst; - tz = (int) delta2; - - return tz; + if (beforetime > boundary && aftertime >= boundary) + { + tm->tm_isdst = after_isdst; + return - (int) after_gmtoff; + } + /* + * It's an invalid or ambiguous time due to timezone transition. + * Prefer the standard-time interpretation. + */ + if (after_isdst == 0) + { + tm->tm_isdst = after_isdst; + return - (int) after_gmtoff; + } + tm->tm_isdst = before_isdst; + return - (int) before_gmtoff; overflow: /* Given date is out of range, so assume UTC */ diff --git a/src/include/pgtime.h b/src/include/pgtime.h index 1c66a63d2f..8a1370cc8d 100644 --- a/src/include/pgtime.h +++ b/src/include/pgtime.h @@ -6,7 +6,7 @@ * Portions Copyright (c) 1996-2004, PostgreSQL Global Development Group * * IDENTIFICATION - * $PostgreSQL: pgsql/src/include/pgtime.h,v 1.4 2004/08/29 05:06:55 momjian Exp $ + * $PostgreSQL: pgsql/src/include/pgtime.h,v 1.5 2004/11/01 21:34:41 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -37,12 +37,19 @@ struct pg_tm const char *tm_zone; }; -extern struct pg_tm *pg_localtime(const pg_time_t *); -extern struct pg_tm *pg_gmtime(const pg_time_t *); -extern bool pg_tzset(const char *tzname); +extern struct pg_tm *pg_localtime(const pg_time_t *timep); +extern struct pg_tm *pg_gmtime(const pg_time_t *timep); +extern int pg_next_dst_boundary(const pg_time_t *timep, + long int *before_gmtoff, + int *before_isdst, + pg_time_t *boundary, + long int *after_gmtoff, + int *after_isdst); extern size_t pg_strftime(char *s, size_t max, const char *format, const struct pg_tm * tm); + extern void pg_timezone_initialize(void); +extern bool pg_tzset(const char *tzname); extern bool tz_acceptable(void); extern const char *select_default_timezone(void); extern const char *pg_get_current_timezone(void); diff --git a/src/timezone/localtime.c b/src/timezone/localtime.c index 5cd0b41ed2..8e64f065b4 100644 --- a/src/timezone/localtime.c +++ b/src/timezone/localtime.c @@ -3,7 +3,7 @@ * 1996-06-05 by Arthur David Olson (arthur_david_olson@nih.gov). * * IDENTIFICATION - * $PostgreSQL: pgsql/src/timezone/localtime.c,v 1.8 2004/08/29 05:07:02 momjian Exp $ + * $PostgreSQL: pgsql/src/timezone/localtime.c,v 1.9 2004/11/01 21:34:44 tgl Exp $ */ /* @@ -1059,6 +1059,100 @@ timesub(const pg_time_t *timep, const long offset, tmp->tm_gmtoff = offset; } +/* + * Find the next DST transition time at or after the given time + * + * *timep is the input value, the other parameters are output values. + * + * When the function result is 1, *boundary is set to the time_t + * representation of the next DST transition time at or after *timep, + * *before_gmtoff and *before_isdst are set to the GMT offset and isdst + * state prevailing just before that boundary, and *after_gmtoff and + * *after_isdst are set to the state prevailing just after that boundary. + * + * When the function result is 0, there is no known DST transition at or + * after *timep, but *before_gmtoff and *before_isdst indicate the GMT + * offset and isdst state prevailing at *timep. (This would occur in + * DST-less time zones, for example.) + * + * A function result of -1 indicates failure (this case does not actually + * occur in our current implementation). + */ +int +pg_next_dst_boundary(const pg_time_t *timep, + long int *before_gmtoff, + int *before_isdst, + pg_time_t *boundary, + long int *after_gmtoff, + int *after_isdst) +{ + register struct state *sp; + register const struct ttinfo *ttisp; + int i; + int j; + const pg_time_t t = *timep; + + sp = lclptr; + if (sp->timecnt == 0) + { + /* non-DST zone, use lowest-numbered standard type */ + i = 0; + while (sp->ttis[i].tt_isdst) + if (++i >= sp->typecnt) + { + i = 0; + break; + } + ttisp = &sp->ttis[i]; + *before_gmtoff = ttisp->tt_gmtoff; + *before_isdst = ttisp->tt_isdst; + return 0; + } + if (t > sp->ats[sp->timecnt - 1]) + { + /* No known transition >= t, so use last known segment's type */ + i = sp->types[sp->timecnt - 1]; + ttisp = &sp->ttis[i]; + *before_gmtoff = ttisp->tt_gmtoff; + *before_isdst = ttisp->tt_isdst; + return 0; + } + if (t <= sp->ats[0]) + { + /* For "before", use lowest-numbered standard type */ + i = 0; + while (sp->ttis[i].tt_isdst) + if (++i >= sp->typecnt) + { + i = 0; + break; + } + ttisp = &sp->ttis[i]; + *before_gmtoff = ttisp->tt_gmtoff; + *before_isdst = ttisp->tt_isdst; + *boundary = sp->ats[0]; + /* And for "after", use the first segment's type */ + i = sp->types[0]; + ttisp = &sp->ttis[i]; + *after_gmtoff = ttisp->tt_gmtoff; + *after_isdst = ttisp->tt_isdst; + return 1; + } + /* Else search to find the containing segment */ + for (i = 1; i < sp->timecnt; ++i) + if (t <= sp->ats[i]) + break; + j = sp->types[i - 1]; + ttisp = &sp->ttis[j]; + *before_gmtoff = ttisp->tt_gmtoff; + *before_isdst = ttisp->tt_isdst; + *boundary = sp->ats[i]; + j = sp->types[i]; + ttisp = &sp->ttis[j]; + *after_gmtoff = ttisp->tt_gmtoff; + *after_isdst = ttisp->tt_isdst; + return 1; +} /* * Return the name of the current timezone