-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
- [0] http://www.ecma-international.org/ecma-262/7.0/index.html#sec-localtime
- [1] http://www.ecma-international.org/ecma-262/7.0/index.html#sec-utc-t
- [2] http://www.ecma-international.org/ecma-262/7.0/index.html#sec-daylight-saving-time-adjustment
As noted at the end of [1], UTC(Localtime(t)) is not always equal to t.
Indeed, Localtime(UTC(t)) is not always equal to t, either, although neither [0] nor [1] notes this.
Nor does [1] address the question of what to do when the parameter passed to UTC is either invalid (because it represents a time that local time skipped over when starting DST, during a "spring forward") or ambiguous (because it represents a time that was reused at the end of DST - first in DST then in standard time - in a "fall back").
The spec [2] of DaylightSavingTA() neglects to say that the input parameter, t, is presumed to be a UTC time; this is, however, made clear by the way it is used in the stipulated implementations of UTC() and Localtime(). Those uses make clear that, for any t, LocalTZA + DaylightSavingTA(t) should give local time's applicable offset from UTC at time t.
The spec [2] should make clear that t is presumed to be given in UTC.
LocalTZA is specified to be local time's present (at run-time, not at time t) standard offset from UTC, without any DST correction. If local time has ever had a change of standard offset, the tacit requirement that LocalTZA + DaylightSavingTA(t) was local time's applicable offset from UTC at time t then comes into conflict with the wording of the specification for DaylightSavingTA(); when t refers to a time the other side (from run-time) of a standard offset change in local time, DaylightSavingTA(t) needs to incorporate both the standard offset change and any DST that may have been in effect at time t, while the wording of the spec says it should only return the DST offset applicable at that time.
In the case of Pacific/Kiritimati looking at times before 1995, or Pacific/Apia before 2012, this implies a DaylightSavingTA() of 24 hours at times when DST was not in effect. For a zone that's now given up DST in favour of setting their standard offset to what used to be their summer offset, this shall mean reporting a negative DaylightSavingTA() before the change for non-DST times, while reporting a zero offset for when DST was in force.
The cause of this is the oversimplification of using LocalTZA as base offset and treating all deviations from it as DST. In practice, a good time offset API would have an OffsetFromUTC(t) method, taking a UTC time and returning the actual offset at that time; for legacy purposes, you can define LocalTZA as at present and DaylightSavingTA(t) as OffsetFromUTC(t) - LocalTZA. There may be some use to also providing StandardOffsetFromUTC(t) and DaylightSavingAdjustment(t) with which to decompose OffsetFromUTC(t), but it's OffsetFromUTC(t) that most code is going to actually care about.
You can then define Localtime(t) to be t + OffsetFromUTC(t). I encourage you to, at the same time, change the spec of UTC(); doing that right shall be contentious, but I do recommend looking at python's definitions [3] in this area.
Some mechanism for disambiguating fall-back hours is necessary; while client code shall often now know the needed hint, requiring sensible default behaviour (and matching present behaviour shall make sense for that), it is worth providing some mechanism for callers to
- indicate when they do know whether DST was in effect, over-riding the default; and
- suppress default behaviour in some way that'll let them address ambiguity/invalidity themselves, for example by catching an exception.
Whatever mechanism disambiguates fall-backs can also be used to decide what to do with invalid local time values in a spring forward. (My usual model for these is that someone stepped forward or backwards an hour, a day or a week from a valid time and landed in a gap. If the prior time was in DST, that step (backwards) should have landed in the hour before the spring forward; if the prior time was in standard time, the step (forward) should land in the hour after. So I select the time that actually contradicts the alleged DSTness, where handling of a fall-back seeks to match it.) Actual changes in a zone's standard offset can be handled the same way as a DST transition in the same direction.
Ideally it should be the case that UTC(t) returns a u for which u + OffsetFromUTC(u) is indeed t. Note, however, that OffsetFromUTC(u) need not be equal to OffsetFromUTC(t), so t - OffsetFromUTC(t) is not an adequately reliable answer, although it may be the best you can do, unless you arrange for your local-time values to come with an extra bit (or trit, to allow "unknown" as an option) of data, indicating their (presumed) DST-ness or some equivalent truth (such as python's "fold").
Time software would all be so much simpler if we all just stuck to UTC; I recommend doing so for all internal types and only meddling with the political mess of time zone variation at the very surface of software, where users are exposed to the data. Local time may seem like a convenience, but avoiding bugs at transitions annihilates what little convenience it actually confers.
I may be reached as edward.welbourne@qt.io (and shall now fix Qt's V4 engine to be less broken than it presently is - only as broken as the spec requires it to be).