[PATCH 1/8] * tzfile.5: Fix prefix/suffix typo (thanks to Ian Abbott).

--- tzfile.5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tzfile.5 b/tzfile.5 index b4952d5..ec78689 100644 --- a/tzfile.5 +++ b/tzfile.5 @@ -134,7 +134,7 @@ bytes that represent time zone designations, which are null-terminated byte strings, each indexed by the .B tt_desigidx values mentioned above. -The byte strings can overlap if one is a prefix of the other. +The byte strings can overlap if one is a suffix of the other. The encoding of these strings is not specified. .IP * .B tzh_leapcnt -- 2.27.0

* zdump.c (hunt): Fix arithmetic overflow bug when hunting near timestamp extrema; HIT - LOT can overflow. The old code attempted to work around this by assuming wraparound arithmetic, but that assumption is not safe (the C standard says behavior is undefined) and the workarounds weren’t even correct anyway for large negative times. --- zdump.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/zdump.c b/zdump.c index dcad817..464a334 100644 --- a/zdump.c +++ b/zdump.c @@ -654,7 +654,6 @@ hunt(timezone_t tz, char *name, time_t lot, time_t hit) static char * loab; static size_t loabsize; char const * ab; - time_t t; struct tm lotm; struct tm tm; bool lotm_ok = my_localtime_rz(tz, &lot, &lotm) != NULL; @@ -663,15 +662,16 @@ hunt(timezone_t tz, char *name, time_t lot, time_t hit) if (lotm_ok) ab = saveabbr(&loab, &loabsize, &lotm); for ( ; ; ) { - time_t diff = hit - lot; - if (diff < 2) + /* T = average of LOT and HIT, rounding down. + Avoid overflow, even on oddball C89 platforms + where / rounds down and TIME_T_MIN == -TIME_T_MAX + so lot / 2 + hit / 2 might overflow. */ + time_t t = (lot / 2 + - ((lot % 2 + hit % 2) < 0) + + ((lot % 2 + hit % 2) == 2) + + hit / 2); + if (t == lot) break; - t = lot; - t += diff / 2; - if (t <= lot) - ++t; - else if (t >= hit) - --t; tm_ok = my_localtime_rz(tz, &t, &tm) != NULL; if (lotm_ok & tm_ok ? (delta(&tm, &lotm) == t - lot -- 2.27.0

* zdump.c (delta_nonneg): Speed up code by using division rather than repeated addition. Based on a suggestion from Arthur David Olson; this version avoids int overflow. --- zdump.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/zdump.c b/zdump.c index 464a334..b006229 100644 --- a/zdump.c +++ b/zdump.c @@ -693,11 +693,11 @@ hunt(timezone_t tz, char *name, time_t lot, time_t hit) static intmax_t delta_nonneg(struct tm *newp, struct tm *oldp) { - register intmax_t result; - register int tmy; - - result = 0; - for (tmy = oldp->tm_year; tmy < newp->tm_year; ++tmy) + intmax_t oldy = oldp->tm_year; + int cycles = (newp->tm_year - oldy) / YEARSPERREPEAT; + intmax_t sec = SECSPERREPEAT, result = cycles * sec; + int tmy = oldp->tm_year + cycles * YEARSPERREPEAT; + for ( ; tmy < newp->tm_year; ++tmy) result += DAYSPERNYEAR + isleap_sum(tmy, TM_YEAR_BASE); result += newp->tm_yday - oldp->tm_yday; result *= HOURSPERDAY; -- 2.27.0

* private.h (DAYSPERREPEAT): New macro. (SECSPERREPEAT, AVGSECSPERYEAR): Use it. Rearrange REPEATish macros in a more-logical order. * zic.c (rpytime): Avoid some conditional branches and don’t treat years in the range 0..1969 as a special case. The code for negative years was dubious anyway. (LDAYSPERWEEK): Remove. All uses removed. --- private.h | 18 ++++++------------ zic.c | 46 +++++++++++++++++----------------------------- 2 files changed, 23 insertions(+), 41 deletions(-) diff --git a/private.h b/private.h index a919e4f..e38a4f3 100644 --- a/private.h +++ b/private.h @@ -710,8 +710,6 @@ char *ctime_r(time_t const *, char *); /* Handy macros that are independent of tzfile implementation. */ -#define YEARSPERREPEAT 400 /* years before a Gregorian repeat */ - #define SECSPERMIN 60 #define MINSPERHOUR 60 #define HOURSPERDAY 24 @@ -722,6 +720,12 @@ char *ctime_r(time_t const *, char *); #define SECSPERDAY ((int_fast32_t) SECSPERHOUR * HOURSPERDAY) #define MONSPERYEAR 12 +#define YEARSPERREPEAT 400 /* years before a Gregorian repeat */ +#define DAYSPERREPEAT ((int_fast32_t) 400 * 365 + 100 - 4 + 1) +#define SECSPERREPEAT ((int_fast64_t) DAYSPERREPEAT * SECSPERDAY) +#define SECSPERREPEAT_BITS 34 /* ceil(log2(SECSPERREPEAT)) */ +#define AVGSECSPERYEAR (SECSPERREPEAT / YEARSPERREPEAT) + #define TM_SUNDAY 0 #define TM_MONDAY 1 #define TM_TUESDAY 2 @@ -764,14 +768,4 @@ char *ctime_r(time_t const *, char *); #define isleap_sum(a, b) isleap((a) % 400 + (b) % 400) - -/* -** The Gregorian year averages 365.2425 days, which is 31556952 seconds. -*/ - -#define AVGSECSPERYEAR 31556952L -#define SECSPERREPEAT \ - ((int_fast64_t) YEARSPERREPEAT * (int_fast64_t) AVGSECSPERYEAR) -#define SECSPERREPEAT_BITS 34 /* ceil(log2(SECSPERREPEAT)) */ - #endif /* !defined PRIVATE_H */ diff --git a/zic.c b/zic.c index f5d813b..6c38bd7 100644 --- a/zic.c +++ b/zic.c @@ -3420,6 +3420,7 @@ rpytime(const struct rule *rp, zic_t wantedy) register int m, i; register zic_t dayoff; /* with a nod to Margaret O. */ register zic_t t, y; + int yrem; if (wantedy == ZIC_MIN) return min_time; @@ -3428,24 +3429,20 @@ rpytime(const struct rule *rp, zic_t wantedy) dayoff = 0; m = TM_JANUARY; y = EPOCH_YEAR; - if (y < wantedy) { - wantedy -= y; - dayoff = (wantedy / YEARSPERREPEAT) * (SECSPERREPEAT / SECSPERDAY); - wantedy %= YEARSPERREPEAT; - wantedy += y; - } else if (wantedy < 0) { - dayoff = (wantedy / YEARSPERREPEAT) * (SECSPERREPEAT / SECSPERDAY); - wantedy %= YEARSPERREPEAT; - } + + /* dayoff = floor((wantedy - y) / YEARSPERREPEAT) * DAYSPERREPEAT, + sans overflow. */ + yrem = wantedy % YEARSPERREPEAT - y % YEARSPERREPEAT; + dayoff = ((wantedy / YEARSPERREPEAT - y / YEARSPERREPEAT + + yrem / YEARSPERREPEAT - (yrem % YEARSPERREPEAT < 0)) + * DAYSPERREPEAT); + /* wantedy = y + ((wantedy - y) mod YEARSPERREPEAT), sans overflow. */ + wantedy = y + (yrem + 2 * YEARSPERREPEAT) % YEARSPERREPEAT; + while (wantedy != y) { - if (wantedy > y) { - i = len_years[isleap(y)]; - ++y; - } else { - --y; - i = -len_years[isleap(y)]; - } + i = len_years[isleap(y)]; dayoff = oadd(dayoff, i); + y++; } while (m != rp->r_month) { i = len_months[isleap(y)][m]; @@ -3464,30 +3461,21 @@ rpytime(const struct rule *rp, zic_t wantedy) --i; dayoff = oadd(dayoff, i); if (rp->r_dycode == DC_DOWGEQ || rp->r_dycode == DC_DOWLEQ) { - register zic_t wday; - -#define LDAYSPERWEEK ((zic_t) DAYSPERWEEK) - wday = EPOCH_WDAY; /* ** Don't trust mod of negative numbers. */ - if (dayoff >= 0) - wday = (wday + dayoff) % LDAYSPERWEEK; - else { - wday -= ((-dayoff) % LDAYSPERWEEK); - if (wday < 0) - wday += LDAYSPERWEEK; - } + zic_t wday = ((EPOCH_WDAY + dayoff % DAYSPERWEEK + DAYSPERWEEK) + % DAYSPERWEEK); while (wday != rp->r_wday) if (rp->r_dycode == DC_DOWGEQ) { dayoff = oadd(dayoff, 1); - if (++wday >= LDAYSPERWEEK) + if (++wday >= DAYSPERWEEK) wday = 0; ++i; } else { dayoff = oadd(dayoff, -1); if (--wday < 0) - wday = LDAYSPERWEEK - 1; + wday = DAYSPERWEEK - 1; --i; } if (i < 0 || i >= len_months[isleap(y)][m]) { -- 2.27.0

* localtime.c (tzparse): Replace arg PARSELB with arg BASEP, and do the parselb calculations ourselves rather than relying on the caller. All callers changed. Use a different bound for timezone transitions than for leap seconds, since we need the former to decide how to merge a TZif file's TZ string transitions, and we also need the latter to know when to stop extending the transition table. This fixes a bug when the last leap second is after the last explicit timezone transition. --- localtime.c | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/localtime.c b/localtime.c index a18d130..f37280e 100644 --- a/localtime.c +++ b/localtime.c @@ -155,7 +155,7 @@ static bool normalize_overflow32(int_fast32_t *, int *, int); static struct tm *timesub(time_t const *, int_fast32_t, struct state const *, struct tm *); static bool typesequiv(struct state const *, int, int); -static bool tzparse(char const *, struct state *, time_t); +static bool tzparse(char const *, struct state *, struct state *); #ifdef ALL_STATE static struct state * lclptr; @@ -596,15 +596,9 @@ tzloadbody(char const *name, struct state *sp, bool doextend, up->buf[0] == '\n' && up->buf[nread - 1] == '\n' && sp->typecnt + 2 <= TZ_MAX_TYPES) { struct state *ts = &lsp->u.st; - time_t parselb = TIME_T_MIN; - if (0 < sp->timecnt) - parselb = sp->ats[sp->timecnt - 1]; - if (0 < sp->leapcnt - && parselb < sp->lsis[sp->leapcnt - 1].ls_trans) - parselb = sp->lsis[sp->leapcnt - 1].ls_trans; up->buf[nread - 1] = '\0'; - if (tzparse(&up->buf[1], ts, parselb)) { + if (tzparse(&up->buf[1], ts, sp)) { /* Attempt to reuse existing abbreviations. Without this, America/Anchorage would be right on @@ -1070,7 +1064,7 @@ transtime(const int year, register const struct rule *const rulep, */ static bool -tzparse(const char *name, struct state *sp, time_t parselb) +tzparse(const char *name, struct state *sp, struct state *basep) { const char * stdname; const char * dstname; @@ -1081,6 +1075,7 @@ tzparse(const char *name, struct state *sp, time_t parselb) int_fast32_t dstoffset; register char * cp; register bool load_ok; + time_t atlo = TIME_T_MIN, leaplo = TIME_T_MIN; stdname = name; if (*name == '<') { @@ -1103,9 +1098,19 @@ tzparse(const char *name, struct state *sp, time_t parselb) charcnt = stdlen + 1; if (sizeof sp->chars < charcnt) return false; - load_ok = tzload(TZDEFRULES, sp, false) == 0; - if (!load_ok) - sp->leapcnt = 0; /* so, we're off a little */ + if (basep) { + if (0 < basep->timecnt) + atlo = basep->ats[basep->timecnt - 1]; + load_ok = false; + sp->leapcnt = basep->leapcnt; + memcpy(sp->lsis, basep->lsis, sp->leapcnt * sizeof *sp->lsis); + } else { + load_ok = tzload(TZDEFRULES, sp, false) == 0; + if (!load_ok) + sp->leapcnt = 0; /* So, we're off a little. */ + } + if (0 < sp->leapcnt) + leaplo = sp->lsis[sp->leapcnt - 1].ls_trans; if (*name != '\0') { if (*name == '<') { dstname = ++name; @@ -1168,7 +1173,7 @@ tzparse(const char *name, struct state *sp, time_t parselb) janoffset = -yearsecs; break; } - } while (parselb < janfirst + } while (atlo < janfirst && EPOCH_YEAR - YEARSPERREPEAT / 2 < yearbeg); while (true) { @@ -1178,7 +1183,7 @@ tzparse(const char *name, struct state *sp, time_t parselb) time_t janfirst1 = janfirst; if (increment_overflow_time(&janfirst1, yearsecs) || increment_overflow(&yearbeg1, 1) - || parselb <= janfirst1) + || atlo <= janfirst1) break; yearbeg = yearbeg1; janfirst = janfirst1; @@ -1209,16 +1214,22 @@ tzparse(const char *name, struct state *sp, time_t parselb) if (! increment_overflow_time (&sp->ats[timecnt], janoffset + starttime) - && parselb <= sp->ats[timecnt]) + && atlo <= sp->ats[timecnt]) sp->types[timecnt++] = !reversed; sp->ats[timecnt] = janfirst; if (! increment_overflow_time (&sp->ats[timecnt], janoffset + endtime) - && parselb <= sp->ats[timecnt]) { + && atlo <= sp->ats[timecnt]) { sp->types[timecnt++] = reversed; } } + if (endtime < leaplo) { + yearlim = year; + if (increment_overflow(&yearlim, + YEARSPERREPEAT + 1)) + yearlim = INT_MAX; + } if (increment_overflow_time (&janfirst, janoffset + yearsecs)) break; @@ -1335,7 +1346,7 @@ static void gmtload(struct state *const sp) { if (tzload(gmt, sp, true) != 0) - tzparse("GMT0", sp, TIME_T_MIN); + tzparse("GMT0", sp, NULL); } /* Initialize *SP to a value appropriate for the TZ setting NAME. @@ -1358,7 +1369,7 @@ zoneinit(struct state *sp, char const *name) return 0; } else { int err = tzload(name, sp, true); - if (err != 0 && name && name[0] != ':' && tzparse(name, sp, TIME_T_MIN)) + if (err != 0 && name && name[0] != ':' && tzparse(name, sp, NULL)) err = 0; if (err == 0) scrub_abbrs(sp); -- 2.27.0

* localtime.c (leaps_thru_end_of_nonneg, leaps_thru_end_of): Args and result are now of type time_t not int, so as to not mishandle arguments that do not fit in int. All callers changed to not pass negative arguments unless time_t is signed. (timesub): Prefer int_fast32_t to int_fast64_t when either will do. Use int_fast32_t instead of int when values might not fit into 16-bit range (the C standard still allows 16-bit ints). Use time_t instead of int when values might not fit into even 64-bit int (at least in theory, time_t could unsigned and wider than 64 bits), taking care to avoid negative values when time_t is unsigned. Fix glitches in range testing by simply testing whether the year is in range before assigning it to tm_year. Avoid potential overflow in subtraction, by computing with TM_WDAY_BASE and tm_year rather than EPOCH_WDAY and (y - EPOCH_YEAR). * private.h (TM_WDAY_BASE): New macro. --- localtime.c | 111 ++++++++++++++++++++++++---------------------------- private.h | 1 + 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/localtime.c b/localtime.c index f37280e..b0a03bb 100644 --- a/localtime.c +++ b/localtime.c @@ -1655,14 +1655,14 @@ offtime(const time_t *timep, long offset) ** where, to make the math easy, the answer for year zero is defined as zero. */ -static int -leaps_thru_end_of_nonneg(int y) +static time_t +leaps_thru_end_of_nonneg(time_t y) { return y / 4 - y / 100 + y / 400; } -static int -leaps_thru_end_of(register const int y) +static time_t +leaps_thru_end_of(time_t y) { return (y < 0 ? -1 - leaps_thru_end_of_nonneg(-1 - y) @@ -1675,13 +1675,12 @@ timesub(const time_t *timep, int_fast32_t offset, { register const struct lsinfo * lp; register time_t tdays; - register int idays; /* unsigned would be so 2003 */ - register int_fast64_t rem; - int y; register const int * ip; register int_fast32_t corr; register bool hit; register int i; + int_fast32_t idays, rem, dayoff, dayrem; + time_t y; corr = 0; hit = false; @@ -1695,67 +1694,63 @@ timesub(const time_t *timep, int_fast32_t offset, break; } } - y = EPOCH_YEAR; + + /* Calculate the year, avoiding integer overflow even if + time_t is unsigned. */ tdays = *timep / SECSPERDAY; rem = *timep % SECSPERDAY; - while (tdays < 0 || tdays >= year_lengths[isleap(y)]) { - int newy; - register time_t tdelta; - register int idelta; + rem += offset % SECSPERDAY - corr % SECSPERDAY + 3 * SECSPERDAY; + dayoff = offset / SECSPERDAY - corr / SECSPERDAY + rem / SECSPERDAY - 3; + rem %= SECSPERDAY; + /* y = (EPOCH_YEAR + + floor((tdays + dayoff) / DAYSPERREPEAT) * YEARSPERREPEAT), + sans overflow. But calculate against 1570 (EPOCH_YEAR - + YEARSPERREPEAT) instead of against 1970 so that things work + for localtime values before 1970 when time_t is unsigned. */ + dayrem = tdays % DAYSPERREPEAT; + dayrem += dayoff % DAYSPERREPEAT; + y = (EPOCH_YEAR - YEARSPERREPEAT + + ((1 + dayoff / DAYSPERREPEAT + dayrem / DAYSPERREPEAT + - ((dayrem % DAYSPERREPEAT) < 0) + + tdays / DAYSPERREPEAT) + * YEARSPERREPEAT)); + /* idays = (tdays + dayoff) mod DAYSPERREPEAT, sans overflow. */ + idays = tdays % DAYSPERREPEAT; + idays += dayoff % DAYSPERREPEAT + 2 * DAYSPERREPEAT; + idays %= DAYSPERREPEAT; + /* Increase Y and decrease IDAYS until IDAYS is in range for Y. */ + while (year_lengths[isleap(y)] <= idays) { + int tdelta = idays / DAYSPERLYEAR; + int_fast32_t ydelta = tdelta + !tdelta; + time_t newy = y + ydelta; register int leapdays; - - tdelta = tdays / DAYSPERLYEAR; - if (! ((! TYPE_SIGNED(time_t) || INT_MIN <= tdelta) - && tdelta <= INT_MAX)) - goto out_of_range; - idelta = tdelta; - if (idelta == 0) - idelta = (tdays < 0) ? -1 : 1; - newy = y; - if (increment_overflow(&newy, idelta)) - goto out_of_range; leapdays = leaps_thru_end_of(newy - 1) - leaps_thru_end_of(y - 1); - tdays -= ((time_t) newy - y) * DAYSPERNYEAR; - tdays -= leapdays; + idays -= ydelta * DAYSPERNYEAR; + idays -= leapdays; y = newy; } - /* - ** Given the range, we can now fearlessly cast... - */ - idays = tdays; - rem += offset - corr; - while (rem < 0) { - rem += SECSPERDAY; - --idays; - } - while (rem >= SECSPERDAY) { - rem -= SECSPERDAY; - ++idays; - } - while (idays < 0) { - if (increment_overflow(&y, -1)) - goto out_of_range; - idays += year_lengths[isleap(y)]; - } - while (idays >= year_lengths[isleap(y)]) { - idays -= year_lengths[isleap(y)]; - if (increment_overflow(&y, 1)) - goto out_of_range; + + if (!TYPE_SIGNED(time_t) && y < TM_YEAR_BASE) { + int signed_y = y; + tmp->tm_year = signed_y - TM_YEAR_BASE; + } else if ((!TYPE_SIGNED(time_t) || INT_MIN + TM_YEAR_BASE <= y) + && y - TM_YEAR_BASE <= INT_MAX) + tmp->tm_year = y - TM_YEAR_BASE; + else { + errno = EOVERFLOW; + return NULL; } - tmp->tm_year = y; - if (increment_overflow(&tmp->tm_year, -TM_YEAR_BASE)) - goto out_of_range; tmp->tm_yday = idays; /* ** The "extra" mods below avoid overflow problems. */ - tmp->tm_wday = EPOCH_WDAY + - ((y - EPOCH_YEAR) % DAYSPERWEEK) * - (DAYSPERNYEAR % DAYSPERWEEK) + - leaps_thru_end_of(y - 1) - - leaps_thru_end_of(EPOCH_YEAR - 1) + - idays; + tmp->tm_wday = (TM_WDAY_BASE + + ((tmp->tm_year % DAYSPERWEEK) + * (DAYSPERNYEAR % DAYSPERWEEK)) + + leaps_thru_end_of(y - 1) + - leaps_thru_end_of(TM_YEAR_BASE - 1) + + idays); tmp->tm_wday %= DAYSPERWEEK; if (tmp->tm_wday < 0) tmp->tm_wday += DAYSPERWEEK; @@ -1776,10 +1771,6 @@ timesub(const time_t *timep, int_fast32_t offset, tmp->TM_GMTOFF = offset; #endif /* defined TM_GMTOFF */ return tmp; - - out_of_range: - errno = EOVERFLOW; - return NULL; } char * diff --git a/private.h b/private.h index e38a4f3..7e587e4 100644 --- a/private.h +++ b/private.h @@ -748,6 +748,7 @@ char *ctime_r(time_t const *, char *); #define TM_DECEMBER 11 #define TM_YEAR_BASE 1900 +#define TM_WDAY_BASE TM_MONDAY #define EPOCH_YEAR 1970 #define EPOCH_WDAY TM_THURSDAY -- 2.27.0

From: Arthur David Olson <arthurdavidolson@gmail.com> * zdump.c (main): Show transitions around cutlotime and cuthitime if they are between out-of-range and in-range years. --- zdump.c | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/zdump.c b/zdump.c index b006229..fbe53cd 100644 --- a/zdump.c +++ b/zdump.c @@ -549,6 +549,14 @@ main(int argc, char *argv[]) show(tz, argv[i], t, true); t += SECSPERDAY; show(tz, argv[i], t, true); + if (my_localtime_rz(tz, &t, &tm) == NULL && t < cutlotime) { + time_t newt = cutlotime; + if (my_localtime_rz(tz, &newt, &newtm) != NULL) { + newt = hunt(tz, argv[i], t, newt); + show(tz, argv[i], newt - 1, true); + show(tz, argv[i], newt, true); + } + } } if (t + 1 < cutlotime) t = cutlotime - 1; @@ -591,6 +599,16 @@ main(int argc, char *argv[]) } } if (! (iflag | Vflag)) { + time_t newt = absolute_max_time; + newt -= SECSPERDAY; + t = cuthitime; + if (t < newt && + my_localtime_rz(tz, &t, &tm) != NULL && + my_localtime_rz(tz, &newt, &newtm) == NULL) { + newt = hunt(tz, argv[i], t, newt); + show(tz, argv[i], newt - 1, true); + show(tz, argv[i], newt, true); + } t = absolute_max_time; t -= SECSPERDAY; show(tz, argv[i], t, true); @@ -693,10 +711,16 @@ hunt(timezone_t tz, char *name, time_t lot, time_t hit) static intmax_t delta_nonneg(struct tm *newp, struct tm *oldp) { - intmax_t oldy = oldp->tm_year; - int cycles = (newp->tm_year - oldy) / YEARSPERREPEAT; - intmax_t sec = SECSPERREPEAT, result = cycles * sec; - int tmy = oldp->tm_year + cycles * YEARSPERREPEAT; + register intmax_t result; + register int tmy; + + result = 0; + tmy = oldp->tm_year; + if (newp->tm_year - tmy > YEARSPERREPEAT) { + intmax_t cycles = (newp->tm_year - tmy) / YEARSPERREPEAT; + result = cycles * (YEARSPERREPEAT * DAYSPERNYEAR + 100 - 4 + 1); + tmy += cycles * YEARSPERREPEAT; + } for ( ; tmy < newp->tm_year; ++tmy) result += DAYSPERNYEAR + isleap_sum(tmy, TM_YEAR_BASE); result += newp->tm_yday - oldp->tm_yday; -- 2.27.0

* NEWS, zdump.8: Document the new behavior. * zdump.c (main): Let showextrema do the work. Don’t bother with the one-hour-away timestamp. (hunt): New args LOTMP and ONLY_OK. All callers changed. (showextrema): New function. Don’t assume there’s just a single transition near the boundary; there could be more if a timezone or leap-second transition is nearby. --- NEWS | 5 +++ zdump.8 | 17 +++++---- zdump.c | 112 ++++++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 96 insertions(+), 38 deletions(-) diff --git a/NEWS b/NEWS index 20b789c..593c3fa 100644 --- a/NEWS +++ b/NEWS @@ -90,6 +90,11 @@ Unreleased, experimental changes seconds on the rare platforms where time_t counts leap seconds, fixing a bug introduced in 2014g. + zdump -v now outputs timestamps at boundaries of what localtime + and gmtime can represent, instead of the less-useful timestamps + one hour after the minimum and one hour before the maximum. + (Thanks to Arthur David Olson for prototype code.) + zdump's -c and -t options are now consistently inclusive for the lower time bound and exclusive for the upper. Formerly they were inconsistent. (Confusion noted by Martin Burnicki.) diff --git a/zdump.8 b/zdump.8 index 17c0200..a5e2042 100644 --- a/zdump.8 +++ b/zdump.8 @@ -44,12 +44,14 @@ Output a verbose description of time intervals. For each .I timezone on the command line, -print the time at the lowest possible time value, -the time one day after the lowest possible time value, +print the times at the two extreme time values, +the times (if present) at and just beyond the boundaries of years that +.BR localtime (3) +and +.BR gmtime (3) +can represent, and the times both one second before and exactly at -each detected time discontinuity, -the time at one day less than the highest possible time value, -and the time at the highest possible time value. +each detected time discontinuity. Each line is followed by .BI isdst= D where @@ -66,7 +68,7 @@ seconds east of Greenwich. .B \*-V Like .BR \*-v , -except omit the times relative to the extreme time values. +except omit output concerning extreme time and year values. This generates output that is easier to compare to that of implementations with different time representations. .TP @@ -200,7 +202,8 @@ This time zone is east of UT, so its UT offsets are positive. Also, many of its time zone abbreviations are omitted since they duplicate the text of the UT offset. .SH LIMITATIONS -Time discontinuities are found by sampling the results returned by localtime +Time discontinuities are found by sampling the results returned by +.BR localtime (3) at twelve-hour intervals. This works in all real-world cases; one can construct artificial time zones for which this fails. diff --git a/zdump.c b/zdump.c index fbe53cd..e1db329 100644 --- a/zdump.c +++ b/zdump.c @@ -91,8 +91,9 @@ static bool errout; static char const *abbr(struct tm const *); static intmax_t delta(struct tm *, struct tm *) ATTRIBUTE_PURE; static void dumptime(struct tm const *); -static time_t hunt(timezone_t, char *, time_t, time_t); +static time_t hunt(timezone_t, char *, time_t, struct tm *, time_t, bool); static void show(timezone_t, char *, time_t, bool); +static void showextrema(timezone_t, char *, time_t, struct tm *, time_t); static void showtrans(char const *, struct tm const *, time_t, char const *, char const *); static const char *tformat(void); @@ -533,6 +534,7 @@ main(int argc, char *argv[]) char const *ab; time_t t; struct tm tm, newtm; + struct tm *tmp; bool tm_ok; if (!tz) { perror(argv[i]); @@ -547,21 +549,19 @@ main(int argc, char *argv[]) t = absolute_min_time; if (! (iflag | Vflag)) { show(tz, argv[i], t, true); - t += SECSPERDAY; - show(tz, argv[i], t, true); - if (my_localtime_rz(tz, &t, &tm) == NULL && t < cutlotime) { + if (my_localtime_rz(tz, &t, &tm) == NULL + && t < cutlotime) { time_t newt = cutlotime; - if (my_localtime_rz(tz, &newt, &newtm) != NULL) { - newt = hunt(tz, argv[i], t, newt); - show(tz, argv[i], newt - 1, true); - show(tz, argv[i], newt, true); - } + if (my_localtime_rz(tz, &newt, &newtm) + != NULL) + showextrema(tz, argv[i], t, NULL, newt); } } if (t + 1 < cutlotime) t = cutlotime - 1; INITIALIZE (ab); - tm_ok = my_localtime_rz(tz, &t, &tm) != NULL; + tmp = my_localtime_rz(tz, &t, &tm); + tm_ok = tmp != NULL; if (tm_ok) { ab = saveabbr(&abbrev, &abbrevsize, &tm); if (iflag) { @@ -580,7 +580,7 @@ main(int argc, char *argv[]) || (tm_ok && (delta(&newtm, &tm) != newt - t || newtm.tm_isdst != tm.tm_isdst || strcmp(abbr(&newtm), ab) != 0))) { - newt = hunt(tz, argv[i], t, newt); + newt = hunt(tz, argv[i], t, tmp, newt, false); newtmp = localtime_rz(tz, &newt, &newtm); newtm_ok = newtmp != NULL; if (iflag) @@ -600,20 +600,14 @@ main(int argc, char *argv[]) } if (! (iflag | Vflag)) { time_t newt = absolute_max_time; - newt -= SECSPERDAY; t = cuthitime; - if (t < newt && - my_localtime_rz(tz, &t, &tm) != NULL && - my_localtime_rz(tz, &newt, &newtm) == NULL) { - newt = hunt(tz, argv[i], t, newt); - show(tz, argv[i], newt - 1, true); - show(tz, argv[i], newt, true); + if (t < newt) { + tmp = my_localtime_rz(tz, &t, &tm); + if (tmp != NULL + && my_localtime_rz(tz, &newt, &newtm) == NULL) + showextrema(tz, argv[i], t, tmp, newt); } - t = absolute_max_time; - t -= SECSPERDAY; - show(tz, argv[i], t, true); - t += SECSPERDAY; - show(tz, argv[i], t, true); + show(tz, argv[i], absolute_max_time, true); } tzfree(tz); } @@ -666,19 +660,30 @@ yeartot(intmax_t y) return t; } +/* Search for a discontinuity in timezone TZ with name NAME, in the + timestamps ranging from LOT (with broken-down time LOTMP if + nonnull) through HIT. LOT and HIT disagree about some aspect of + timezone. If ONLY_OK, search only for definedness changes, i.e., + localtime succeeds on one side of the transition but fails on the + other side. Return the timestamp just before the transition from + LOT's settings. */ + static time_t -hunt(timezone_t tz, char *name, time_t lot, time_t hit) +hunt(timezone_t tz, char *name, time_t lot, struct tm *lotmp, time_t hit, + bool only_ok) { static char * loab; static size_t loabsize; char const * ab; struct tm lotm; struct tm tm; - bool lotm_ok = my_localtime_rz(tz, &lot, &lotm) != NULL; + bool lotm_ok = lotmp != NULL; bool tm_ok; - if (lotm_ok) + if (lotm_ok) { + lotm = *lotmp; ab = saveabbr(&loab, &loabsize, &lotm); + } for ( ; ; ) { /* T = average of LOT and HIT, rounding down. Avoid overflow, even on oddball C89 platforms @@ -691,11 +696,11 @@ hunt(timezone_t tz, char *name, time_t lot, time_t hit) if (t == lot) break; tm_ok = my_localtime_rz(tz, &t, &tm) != NULL; - if (lotm_ok & tm_ok - ? (delta(&tm, &lotm) == t - lot - && tm.tm_isdst == lotm.tm_isdst - && strcmp(abbr(&tm), ab) == 0) - : lotm_ok == tm_ok) { + if (lotm_ok == tm_ok + && (only_ok + || (lotm_ok && tm.tm_isdst == lotm.tm_isdst + && delta(&tm, &lotm) == t - lot + && strcmp(abbr(&tm), ab) == 0))) { lot = t; if (tm_ok) lotm = tm; @@ -819,6 +824,51 @@ show(timezone_t tz, char *zone, time_t t, bool v) abbrok(abbr(tmp), zone); } +/* Show timestamps just before and just after a transition between + defined and undefined (or vice versa) in either localtime or + gmtime. These transitions are for timezone TZ with name ZONE, in + the range from LO (with broken-down time LOTMP if that is nonnull) + through HI. LO and HI disagree on definedness. */ + +static void +showextrema(timezone_t tz, char *zone, time_t lo, struct tm *lotmp, time_t hi) +{ + struct tm localtm[2], gmtm[2]; + time_t t, boundary = hunt(tz, zone, lo, lotmp, hi, true); + bool old = false; + hi = (SECSPERDAY < hi - boundary + ? boundary + SECSPERDAY + : hi + (hi < TIME_T_MAX)); + if (SECSPERDAY < boundary - lo) { + lo = boundary - SECSPERDAY; + lotmp = my_localtime_rz(tz, &lo, &localtm[old]); + } + if (lotmp) + localtm[old] = *lotmp; + else + localtm[old].tm_sec = -1; + if (! my_gmtime_r(&lo, &gmtm[old])) + gmtm[old].tm_sec = -1; + + /* Search sequentially for definedness transitions. Although this + could be sped up by refining 'hunt' to search for either + localtime or gmtime definedness transitions, it hardly seems + worth the trouble. */ + for (t = lo + 1; t < hi; t++) { + bool new = !old; + if (! my_localtime_rz(tz, &t, &localtm[new])) + localtm[new].tm_sec = -1; + if (! my_gmtime_r(&t, &gmtm[new])) + gmtm[new].tm_sec = -1; + if (((localtm[old].tm_sec < 0) != (localtm[new].tm_sec < 0)) + | ((gmtm[old].tm_sec < 0) != (gmtm[new].tm_sec < 0))) { + show(tz, zone, t - 1, true); + show(tz, zone, t, true); + } + old = new; + } +} + #if HAVE_SNPRINTF # define my_snprintf snprintf #else -- 2.27.0

The prior behavior was offset from absolute_{min,max}_time by SECSPERDAY; the attached fixes this description of the prior behavior in NEWS. -- Tim Parenti On Wed, 24 Mar 2021 at 00:01, Paul Eggert via tz <tz@iana.org> wrote:
* NEWS, zdump.8: Document the new behavior. * zdump.c (main): Let showextrema do the work. Don’t bother with the one-hour-away timestamp. (hunt): New args LOTMP and ONLY_OK. All callers changed. (showextrema): New function. Don’t assume there’s just a single transition near the boundary; there could be more if a timezone or leap-second transition is nearby. --- NEWS | 5 +++ zdump.8 | 17 +++++---- zdump.c | 112 ++++++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 96 insertions(+), 38 deletions(-)
diff --git a/NEWS b/NEWS index 20b789c..593c3fa 100644 --- a/NEWS +++ b/NEWS @@ -90,6 +90,11 @@ Unreleased, experimental changes seconds on the rare platforms where time_t counts leap seconds, fixing a bug introduced in 2014g.
+ zdump -v now outputs timestamps at boundaries of what localtime + and gmtime can represent, instead of the less-useful timestamps + one hour after the minimum and one hour before the maximum. + (Thanks to Arthur David Olson for prototype code.) + zdump's -c and -t options are now consistently inclusive for the lower time bound and exclusive for the upper. Formerly they were inconsistent. (Confusion noted by Martin Burnicki.) diff --git a/zdump.8 b/zdump.8 index 17c0200..a5e2042 100644 --- a/zdump.8 +++ b/zdump.8 @@ -44,12 +44,14 @@ Output a verbose description of time intervals. For each .I timezone on the command line, -print the time at the lowest possible time value, -the time one day after the lowest possible time value, +print the times at the two extreme time values, +the times (if present) at and just beyond the boundaries of years that +.BR localtime (3) +and +.BR gmtime (3) +can represent, and the times both one second before and exactly at -each detected time discontinuity, -the time at one day less than the highest possible time value, -and the time at the highest possible time value. +each detected time discontinuity. Each line is followed by .BI isdst= D where @@ -66,7 +68,7 @@ seconds east of Greenwich. .B \*-V Like .BR \*-v , -except omit the times relative to the extreme time values. +except omit output concerning extreme time and year values. This generates output that is easier to compare to that of implementations with different time representations. .TP @@ -200,7 +202,8 @@ This time zone is east of UT, so its UT offsets are positive. Also, many of its time zone abbreviations are omitted since they duplicate the text of the UT offset. .SH LIMITATIONS -Time discontinuities are found by sampling the results returned by localtime +Time discontinuities are found by sampling the results returned by +.BR localtime (3) at twelve-hour intervals. This works in all real-world cases; one can construct artificial time zones for which this fails. diff --git a/zdump.c b/zdump.c index fbe53cd..e1db329 100644 --- a/zdump.c +++ b/zdump.c @@ -91,8 +91,9 @@ static bool errout; static char const *abbr(struct tm const *); static intmax_t delta(struct tm *, struct tm *) ATTRIBUTE_PURE; static void dumptime(struct tm const *); -static time_t hunt(timezone_t, char *, time_t, time_t); +static time_t hunt(timezone_t, char *, time_t, struct tm *, time_t, bool); static void show(timezone_t, char *, time_t, bool); +static void showextrema(timezone_t, char *, time_t, struct tm *, time_t); static void showtrans(char const *, struct tm const *, time_t, char const *, char const *); static const char *tformat(void); @@ -533,6 +534,7 @@ main(int argc, char *argv[]) char const *ab; time_t t; struct tm tm, newtm; + struct tm *tmp; bool tm_ok; if (!tz) { perror(argv[i]); @@ -547,21 +549,19 @@ main(int argc, char *argv[]) t = absolute_min_time; if (! (iflag | Vflag)) { show(tz, argv[i], t, true); - t += SECSPERDAY; - show(tz, argv[i], t, true); - if (my_localtime_rz(tz, &t, &tm) == NULL && t < cutlotime) { + if (my_localtime_rz(tz, &t, &tm) == NULL + && t < cutlotime) { time_t newt = cutlotime; - if (my_localtime_rz(tz, &newt, &newtm) != NULL) { - newt = hunt(tz, argv[i], t, newt); - show(tz, argv[i], newt - 1, true); - show(tz, argv[i], newt, true); - } + if (my_localtime_rz(tz, &newt, &newtm) + != NULL) + showextrema(tz, argv[i], t, NULL, newt); } } if (t + 1 < cutlotime) t = cutlotime - 1; INITIALIZE (ab); - tm_ok = my_localtime_rz(tz, &t, &tm) != NULL; + tmp = my_localtime_rz(tz, &t, &tm); + tm_ok = tmp != NULL; if (tm_ok) { ab = saveabbr(&abbrev, &abbrevsize, &tm); if (iflag) { @@ -580,7 +580,7 @@ main(int argc, char *argv[]) || (tm_ok && (delta(&newtm, &tm) != newt - t || newtm.tm_isdst != tm.tm_isdst || strcmp(abbr(&newtm), ab) != 0))) { - newt = hunt(tz, argv[i], t, newt); + newt = hunt(tz, argv[i], t, tmp, newt, false); newtmp = localtime_rz(tz, &newt, &newtm); newtm_ok = newtmp != NULL; if (iflag) @@ -600,20 +600,14 @@ main(int argc, char *argv[]) } if (! (iflag | Vflag)) { time_t newt = absolute_max_time; - newt -= SECSPERDAY; t = cuthitime; - if (t < newt && - my_localtime_rz(tz, &t, &tm) != NULL && - my_localtime_rz(tz, &newt, &newtm) == NULL) { - newt = hunt(tz, argv[i], t, newt); - show(tz, argv[i], newt - 1, true); - show(tz, argv[i], newt, true); + if (t < newt) { + tmp = my_localtime_rz(tz, &t, &tm); + if (tmp != NULL + && my_localtime_rz(tz, &newt, &newtm) == NULL) + showextrema(tz, argv[i], t, tmp, newt); } - t = absolute_max_time; - t -= SECSPERDAY; - show(tz, argv[i], t, true); - t += SECSPERDAY; - show(tz, argv[i], t, true); + show(tz, argv[i], absolute_max_time, true); } tzfree(tz); } @@ -666,19 +660,30 @@ yeartot(intmax_t y) return t; }
+/* Search for a discontinuity in timezone TZ with name NAME, in the + timestamps ranging from LOT (with broken-down time LOTMP if + nonnull) through HIT. LOT and HIT disagree about some aspect of + timezone. If ONLY_OK, search only for definedness changes, i.e., + localtime succeeds on one side of the transition but fails on the + other side. Return the timestamp just before the transition from + LOT's settings. */ + static time_t -hunt(timezone_t tz, char *name, time_t lot, time_t hit) +hunt(timezone_t tz, char *name, time_t lot, struct tm *lotmp, time_t hit, + bool only_ok) { static char * loab; static size_t loabsize; char const * ab; struct tm lotm; struct tm tm; - bool lotm_ok = my_localtime_rz(tz, &lot, &lotm) != NULL; + bool lotm_ok = lotmp != NULL; bool tm_ok;
- if (lotm_ok) + if (lotm_ok) { + lotm = *lotmp; ab = saveabbr(&loab, &loabsize, &lotm); + } for ( ; ; ) { /* T = average of LOT and HIT, rounding down. Avoid overflow, even on oddball C89 platforms @@ -691,11 +696,11 @@ hunt(timezone_t tz, char *name, time_t lot, time_t hit) if (t == lot) break; tm_ok = my_localtime_rz(tz, &t, &tm) != NULL; - if (lotm_ok & tm_ok - ? (delta(&tm, &lotm) == t - lot - && tm.tm_isdst == lotm.tm_isdst - && strcmp(abbr(&tm), ab) == 0) - : lotm_ok == tm_ok) { + if (lotm_ok == tm_ok + && (only_ok + || (lotm_ok && tm.tm_isdst == lotm.tm_isdst + && delta(&tm, &lotm) == t - lot + && strcmp(abbr(&tm), ab) == 0))) { lot = t; if (tm_ok) lotm = tm; @@ -819,6 +824,51 @@ show(timezone_t tz, char *zone, time_t t, bool v) abbrok(abbr(tmp), zone); }
+/* Show timestamps just before and just after a transition between + defined and undefined (or vice versa) in either localtime or + gmtime. These transitions are for timezone TZ with name ZONE, in + the range from LO (with broken-down time LOTMP if that is nonnull) + through HI. LO and HI disagree on definedness. */ + +static void +showextrema(timezone_t tz, char *zone, time_t lo, struct tm *lotmp, time_t hi) +{ + struct tm localtm[2], gmtm[2]; + time_t t, boundary = hunt(tz, zone, lo, lotmp, hi, true); + bool old = false; + hi = (SECSPERDAY < hi - boundary + ? boundary + SECSPERDAY + : hi + (hi < TIME_T_MAX)); + if (SECSPERDAY < boundary - lo) { + lo = boundary - SECSPERDAY; + lotmp = my_localtime_rz(tz, &lo, &localtm[old]); + } + if (lotmp) + localtm[old] = *lotmp; + else + localtm[old].tm_sec = -1; + if (! my_gmtime_r(&lo, &gmtm[old])) + gmtm[old].tm_sec = -1; + + /* Search sequentially for definedness transitions. Although this + could be sped up by refining 'hunt' to search for either + localtime or gmtime definedness transitions, it hardly seems + worth the trouble. */ + for (t = lo + 1; t < hi; t++) { + bool new = !old; + if (! my_localtime_rz(tz, &t, &localtm[new])) + localtm[new].tm_sec = -1; + if (! my_gmtime_r(&t, &gmtm[new])) + gmtm[new].tm_sec = -1; + if (((localtm[old].tm_sec < 0) != (localtm[new].tm_sec < 0)) + | ((gmtm[old].tm_sec < 0) != (gmtm[new].tm_sec < 0))) { + show(tz, zone, t - 1, true); + show(tz, zone, t, true); + } + old = new; + } +} + #if HAVE_SNPRINTF # define my_snprintf snprintf #else -- 2.27.0
participants (2)
-
Paul Eggert
-
Tim Parenti