[PROPOSED] Fix ambiguous leapsecs by rolling up to a minute
This fixes a longstanding ambiguity in the output of localtime in oddball timezones where a positive leap second occurs when the UTC offset is not a multiple of 60 seconds. See NEWS for details. * NEWS: Describe fix. * localtime.c (timesub): Use "??:??:60" at the end of the localtime minute containing the leap second, even when the second marked "??:??:60" is not itself a leap second. This avoids ambiguity in localtime output, at the cost of having the localtime leap second occur up to a minute away from the UTC leap second when the UTC offset is not a multiple of 60 seconds. * tzfile.5: Document the fixed and unfixed behaviors. --- NEWS | 18 ++++++++++++++++++ localtime.c | 22 +++++++++++++--------- tzfile.5 | 12 ++++++++++++ 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index 1bf9cf3..6a77932 100644 --- a/NEWS +++ b/NEWS @@ -109,6 +109,24 @@ Unreleased, experimental changes transition in the table, or when handling far-future timestamps in slim TZif files lacking leap seconds. + Fix localtime misbehavior involving positive leap seconds in a + timezone with a UTC offset that is not on a minute boundary. + (No such timezone exists in tzdb, luckily.) Without the fix, + the timestamp was ambiguous during a positive leap second. + With the fix, the localtime leap second appears at the end of the + localtime minute containing the UTC second just before the UTC + leap second, which means leapseconds roll slightly in these + oddball timezones. Here is how the fix affects timestamps in a + timezone with UTC offset +01:23:45 and with a positive leap second + 78796801 (1972-06-30 23:59:60 UTC): + + time_t without the fix with the fix + 78796800 1972-07-01 01:23:45 1972-07-01 01:23:45 + 78796801 1972-07-01 01:23:45 1972-07-01 01:23:46 + ... + 78796815 1972-07-01 01:23:59 1972-07-01 01:23:60 + 78796816 1972-07-01 01:24:00 1972-07-01 01:24:00 + Fix an unlikely bug that caused 'localtime' etc. to misbehave if civil time changes a few seconds before time_t wraps around, when leap seconds are enabled. diff --git a/localtime.c b/localtime.c index 88fd35d..6a687d7 100644 --- a/localtime.c +++ b/localtime.c @@ -1681,20 +1681,23 @@ timesub(const time_t *timep, int_fast32_t offset, register time_t tdays; 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; + /* If less than SECSPERMIN, the number of seconds since the + most recent positive leap second; otherwise, do not add 1 + to localtime tm_sec because of leap seconds. */ + time_t secs_since_posleap = SECSPERMIN; + corr = 0; - hit = false; i = (sp == NULL) ? 0 : sp->leapcnt; while (--i >= 0) { lp = &sp->lsis[i]; if (*timep >= lp->ls_trans) { corr = lp->ls_corr; - hit = (*timep == lp->ls_trans - && (i == 0 ? 0 : lp[-1].ls_corr) < corr); + if ((i == 0 ? 0 : lp[-1].ls_corr) < corr) + secs_since_posleap = *timep - lp->ls_trans; break; } } @@ -1761,11 +1764,12 @@ timesub(const time_t *timep, int_fast32_t offset, tmp->tm_hour = rem / SECSPERHOUR; rem %= SECSPERHOUR; tmp->tm_min = rem / SECSPERMIN; - /* - ** A positive leap second requires a special - ** representation. This uses "... ??:59:60" et seq. - */ - tmp->tm_sec = rem % SECSPERMIN + hit; + tmp->tm_sec = rem % SECSPERMIN; + + /* Use "... ??:??:60" at the end of the localtime minute containing + the second just before the positive leap second. */ + tmp->tm_sec += secs_since_posleap <= tmp->tm_sec; + ip = mon_lengths[isleap(y)]; for (tmp->tm_mon = 0; idays >= ip[tmp->tm_mon]; ++(tmp->tm_mon)) idays -= ip[tmp->tm_mon]; diff --git a/tzfile.5 b/tzfile.5 index ec78689..4a88b75 100644 --- a/tzfile.5 +++ b/tzfile.5 @@ -279,6 +279,10 @@ the purpose of skipping over them. Readers should calculate the total lengths of the headers and data blocks and check that they all fit within the actual file size, as part of a validity check for the file. +.PP +When a positive leap second occurs, readers should append a 60th +second to the local minute containing the second just before the leap +second. .SS Common interoperability issues This section documents common problems in reading or writing TZif files. Most of these are problems in generating TZif files for use by @@ -398,6 +402,14 @@ thus swapping standard and daylight saving time. Although this workaround misidentifies which part of the year uses daylight saving time, it records UT offsets and time zone abbreviations correctly. +.IP * +Some readers generate ambiguous timestamps for positive leap seconds +that occur when the UTC offset is not a multiple of 60 seconds. +For example, in a timezone with UTC offset +01:23:45 and with +a positive leap second 78796801 (1972-06-30 23:59:60 UTC), they +map both 78796800 and 78796801 to 01:23:45 local time the next day +instead of mapping the latter to 01:23:46, and they map 78796815 to +01:23:59 instead of to 01:23:60. .PP Some interoperability problems are reader bugs that are listed here mostly as warnings to developers of readers. -- 2.31.1
On 2021-09-12 00:36, Paul Eggert via tz proposed:
+ Fix localtime misbehavior involving positive leap seconds in a + timezone with a UTC offset that is not on a minute boundary. + (No such timezone exists in tzdb, luckily.) Without the fix, + the timestamp was ambiguous during a positive leap second. + With the fix, the localtime leap second appears at the end of the + localtime minute containing the UTC second just before the UTC + leap second, which means leapseconds roll slightly in these + oddball timezones. Here is how the fix affects timestamps in a + timezone with UTC offset +01:23:45 and with a positive leap second + 78796801 (1972-06-30 23:59:60 UTC): + + time_t without the fix with the fix + 78796800 1972-07-01 01:23:45 1972-07-01 01:23:45 + 78796801 1972-07-01 01:23:45 1972-07-01 01:23:46 + ... + 78796815 1972-07-01 01:23:59 1972-07-01 01:23:60 + 78796816 1972-07-01 01:24:00 1972-07-01 01:24:00 +
I disagree with that proposal. Above all, the comment should make it clear that the proposal only affects the behavior for the "right" system time, that is, when system time is an approximation of TAI - 10 s (rather than one of UTC, as required by POSIX). Even in this infrequent case, and even though the proposal would not have an effect for any time scale currently described by tzdb (because the offsets from UTC are all integral multiples of minutes since 1972-05), the proposal does not represent any civil time scale definition that is likely to be decided in the future. In fact, such time scales will undoubtedly be specified as UTC plus or minus an offset (perhaps with GMT or similar wording instead of UTC). With the current version of localtime(), this condition is satisfied with the proviso that the offset between 23:59:60 and 01:23:45 is (deliberately) not (clearly) defined. With the proposal, however, the condition is not satisfied; for instance, in the example given in the proposed comment, the local time would differ from UTC by T01:23:46 from 1972-07-01T00:00:00Z to 1972-07-01T00:00:14Z, not by T01:23:45. I have never seen a local time scale definition saying that the offset from UTC is +01:23:45 except between a positive leap second in UTC and the next full minute in local time, where the offset is 1 s greater; and I do not expect to ever see one. The proposal would also introduce a difference between the local times determined with the "right" data, and those determined with time2posix() and the normal data. So even if the proposed change is applied, I think it is very unlikely to ever take an effect, and in that unlikely case, the effect likely will not be a desired one. Not a software change I find useful. Michael Deckers.
On 9/12/21 2:18 PM, Michael H Deckers wrote:
the comment should make it clear that the proposal only affects the behavior for the "right" system time,
Thanks, good point. Fixed in the attached patch.
On 2021-09-12 00:36, Paul Eggert via tz proposed:
+ Here is how the fix affects timestamps in a + timezone with UTC offset +01:23:45 and with a positive leap second + 78796801 (1972-06-30 23:59:60 UTC):
Oops! My text messed up. The leap second is at 78796800, not at 78796801. Sorry about the confusion; I don't know whether this affected your analysis.
+ + time_t without the fix with the fix + 78796800 1972-07-01 01:23:45 1972-07-01 01:23:45 + 78796801 1972-07-01 01:23:45 1972-07-01 01:23:46 + ... + 78796815 1972-07-01 01:23:59 1972-07-01 01:23:60 + 78796816 1972-07-01 01:24:00 1972-07-01 01:24:00 +
... such time scales will undoubtedly be specified as UTC plus or minus an offset ...
With the current version of localtime(), this condition is satisfied with the proviso that the offset between 23:59:60 and 01:23:45 is (deliberately) not (clearly) defined.
It sounds like you're saying that the situation is tricky in this area, which it certainly is (as witness my confusion!).
With the proposal, however, the condition is not satisfied; for instance, in the example given in the proposed comment, the local time would differ from UTC by T01:23:46 from 1972-07-01T00:00:00Z to 1972-07-01T00:00:14Z
I think that calculation is not quite right. With the fix, the localtime-minus-UTC value remains 5025 seconds throughout, using a calculation that takes the positive leap second into account. This can be confirmed by looking at tm_gmtoff in the affected seconds. This suggests that I documented the situation correctly, in that I said that it was a rolling leap second. It's not: it's a leap second that occurs somewhere in the middle of a localtime minute, and remaining seconds of that minute are numbered through 60 instead of through 59 so that there's room for the leap second. The attached patch attempts to fix this confusion too. It's true that it's tricky to compute the UTC offset in these oddball situations. But this is true regardless of whether the fix is present. I suppose that we could add wording like "the UTC offset is (deliberately) not (clearly) defined for the leap second and any later seconds in the same localtime minute". However, I think it's better to say that the leap second does not affect the UTC offset, as the attached proposed further patch attempts to do.
On Sun, 12 Sept 2021 at 19:43, Paul Eggert via tz <tz@iana.org> wrote:
seconds of that minute are numbered through 60 instead of through 59 so that there's room for the leap second. The attached patch attempts to fix this confusion too.
Very close, but such a minute would have been the result of adding a 61st second, not a 60th. Proposed fix attached. -- Tim Parenti
On 9/12/21 5:33 PM, Tim Parenti wrote:
Very close, but such a minute would have been the result of adding a 61st second, not a 60th.
Thanks for catching that. I was assuming origin-0 instead of origin-1. To avoid this confusion I installed the attached, which should work regardless of what origin the reader assumes.
On 2021-09-12 23:42, Paul Eggert wrote:
It sounds like you're saying that the situation is tricky in this area, which it certainly is (as witness my confusion!).
With the proposal, however, the condition is not satisfied; for instance, in the example given in the proposed comment, the local time would differ from UTC by T01:23:46 from 1972-07-01T00:00:00Z to 1972-07-01T00:00:14Z
I think that calculation is not quite right. With the fix, the localtime-minus-UTC value remains 5025 seconds throughout, using a calculation that takes the positive leap second into account. This can be confirmed by looking at tm_gmtoff in the affected seconds.
Isn't it quite simple to compute the fictitious local time LT: when UTC is 1972-06-30T23:59:59Z then LT is 1972-07-01T01:23:44 1972-06-30T23:59:60Z then LT is not well-defined 1972-07-01T00:00:00Z then LT must be 1972-07-01T01:23:45 so as to keep the offset at 01:23:45 from UTC 1972-07-01T00:00:14Z then LT must be 1972-07-01T01:23:59 1972-07-01T00:00:15Z then LT must be 1972-07-01T01:24:00 In your proposal, LT - TAI and UTC - TAI have discontinuities at different values of TAI so that LT - UTC just cannot be constant. Michael Deckers.
On Mon, 2021-09-13 at 13:38 +0000, Michael H Deckers via tz wrote:
Isn't it quite simple to compute the fictitious local time LT: when UTC is 1972-06-30T23:59:59Z then LT is 1972-07-01T01:23:44 1972-06-30T23:59:60Z then LT is not well-defined 1972-07-01T00:00:00Z then LT must be 1972-07- 01T01:23:45 so as to keep the offset at 01:23:45 from UTC 1972-07-01T00:00:14Z then LT must be 1972-07- 01T01:23:59 1972-07-01T00:00:15Z then LT must be 1972-07- 01T01:24:00
In your proposal, LT - TAI and UTC - TAI have discontinuities at different values of TAI so that LT - UTC just cannot be constant.
Michael Deckers.
Rather than have Local Time be not well-defined, wouldn't it be better to have it well-defined all the time, even though that causes the GMT offset to change for a few seconds? If the rule is that the local time minute that includes the leap second has 61 seconds, numbered 0 to 60, then we have this: when UTC is 1972-06-30T23:59:59Z then LT is 1972-07-01T01:23:44 1972-06-30T23:59:60Z then LT is 1972-07-01T01:23:45 1972-07-01T00:00:00Z then LT is 1972-07-01T01:23:46 so the GMT offset becomes 01:23:46 from UTC 1972-07-01T00:00:13Z then LT is 1972-07-01T01:23:59 1972-07-01T00:00:14Z then LT is 1972-07-01T01:23:60 1972-07-01T00:00:15Z then LT is 1972-07-01T01:24:00 and the GMT offset is back to 01:23:45 from UTC This has the benefit that LT is always well-defined and each second of UTC has a corresponding name in LT, and all LT seconds have unique names. This means that conversion between UTC and local time is always possible and is reversible. The down side is that the GMT offset is not constant, but that is only true for a few seconds near the leap second. John Sauter (John_Sauter@systemeyescomputerstore.com) -- get my PGP public key with gpg --locate-external-keys John_Sauter@systemeyescomputerstore.com
On 9/13/21 11:41 AM, John Sauter wrote:
his means that conversion between UTC and local time is always possible and is reversible. The down side is that the GMT offset is not constant, but that is only true for a few seconds near the leap second.
That's another way to interpret the situation, yes. However, an advantage of the proposed patch's approach is that the UTC offset (tm_gmtoff) remains constant, thus satisfying the civil-time rules that Michael mentioned. The only difference between the proposed patch, and what you're suggesting, is that in the proposed patch when calculating the UTC offset a minute's seconds on or after an inserted leap second are all counted relative to the next minute, not relative to the previous one.
On 9/13/21 6:38 AM, Michael H Deckers wrote:
Isn't it quite simple to compute the fictitious local time LT: when UTC is 1972-06-30T23:59:59Z then LT is 1972-07-01T01:23:44 1972-06-30T23:59:60Z then LT is not well-defined
If we took that approach, shouldn't calls to 'localtime' fail during a positive leap second? After all, localtime is not well-defined. Clearly we can't do that; too much code expects localtime to succeed unless the time_t value is extremely large or small. localtime must generate *something*, even though it's bogus according to the abovementioned approach. Unfortunately, whatever value localtime plausibly generates will lead to ambiguity in timestamps. And ambiguity is a real problem for users. If I understand things correctly, you're saying that if the UTC offset is a multiple of 60 seconds, then localtime is well-defined during a positive leap second because it ends in :60. Otherwise, localtime is not well-defined during a positive leap second. The proposed patch generalizes in a better way. It says that localtime is well-defined at all times: the minute containing the leap second has seconds numbered 0 through 60, with the leap second being one of those seconds. There is no ambiguity about which second a localtime label identifies, and no second in which localtime is undefined. And the UTC offset remains constant.
In your proposal, LT - TAI and UTC - TAI have discontinuities at different values of TAI so that LT - UTC just cannot be constant.
Such a problem doesn't occur if one uses a proper mapping from broken-down time to seconds counts. In the proposed patch, the mapping from broken-down time (date, hours, minutes, seconds) to LT (a seconds count) takes a within-minute positive leap second into account. LT - UTC is constant because LT - TAI and UTC - TAI have discontinuities at the same instant. Please see my recent email to John Sauter for another way to look at this. In thinking about this more, we have a tradeoff here. When dealing with positive leap seconds, is it more important to have unambiguous timestamps, or to have a simple way to calculate UTC offsets without using tm_gmtoff? If the former, the proposed patch is better; if the latter, then 2021a is better. For end users I think it's quite clear that unambiguous timestamps are better. If there's a real need to calculate UTC offsets without using tm_gmtoff, I think one can write a function to do that that will work even with the proposed patch present. Such a function isn't needed for current tzdb, though, since the situation in question cannot occur in the existing database. So writing such a function should be low priority.
On 2021-09-13 19:26, Paul Eggert wrote:
In thinking about this more, we have a tradeoff here. When dealing with positive leap seconds, is it more important to have unambiguous timestamps, or to have a simple way to calculate UTC offsets without using tm_gmtoff? If the former, the proposed patch is better; if the latter, then 2021a is better.
I do not see any trade-off. The charter of tzdb is to provide software that represents local times as defined by local authorities. It is not a competition for making local time scales "better" in any way. If the "right" time zone files are excepted from that charter and "right" local time scales may differ from the regular ones then this must be clearly stated. Michael Deckers.
On 9/16/21 3:47 AM, Michael H Deckers wrote:
The charter of tzdb is to provide software that represents local times as defined by local authorities.
That is indeed our goal. However, the situation in question has never occurred so there are no local authorities to consult. So we're trying to predict what future local authorities will do (a common problem in tzdb, unfortunately...). My guess is that in the unlikely event that a local authority specifies a non-minute UTC offset, if we go to that authority and say something like this: "We can implement two possibilities: "(A) Your timestamps will be unambiguous, but this will complicate some obscure calculations on old-fashioned platforms during the trailing part of a leap-second minute. "(B) We can simplify those obscure calculations, but your timestamps will be ambiguous." then the local authority will pick (A). I doubt it'll pick (B). Of course I could be wrong. And it's also possible that a local authority would pick something that's neither (A) nor (B), though it's unclear what that would be (apparent solar time, perhaps?). From what we know now, though, I think we're better off implementing (A) than implementing (B). If this unlikely event ever occurs and local authorities choose something other than (A), I hope we can adjust tzdb to what they actually do, much as we already adjust tzdb for other occasions where local authorities make unexpected changes. (If they choose apparent solar time I expect we'll be out of luck though....)
On 2021-09-16 16:02, Paul Eggert wrote:
"We can implement two possibilities:
"(A) Your timestamps will be unambiguous, but this will complicate some obscure calculations on old-fashioned platforms during the trailing part of a leap-second minute.
"(B) We can simplify those obscure calculations, but your timestamps will be ambiguous."
then the local authority will pick (A).
...even if you tame the enthusiasm of said "local authority" by admitting that she will never see her cherished variant (A) on her iphone or her Windows machines, but only on "UNIX boxes using the right timezones". Michael Deckers.
participants (4)
-
John Sauter -
Michael H Deckers -
Paul Eggert -
Tim Parenti