[Python-Dev] Aware datetime from naive local time Was: Status on PEP-431 Timezones
Akira Li
4kir4.1i at gmail.com
Thu Apr 16 00:02:48 CEST 2015
Alexander Belopolsky <alexander.belopolsky at gmail.com> writes:
> ...
> For most world locations past discontinuities are fairly well documented
> for at least a century and future changes are published with at least 6
> months lead time.
It is important to note that the different versions of the tz database
may lead to different tzinfo (utc offset, tzname) even for *past* dates.
i.e., (lt, tzid, isdst) is not enough because the result for (lt,
tzid(2015b), isdst) may be different from (lt, tzid(X), isdst)
where
lt = local time e.g., naive datetime
tzid = timezone from the tz database e.g., Europe/Kiev
isdst = a boolean flag for disambiguation
X != 2015b
In other words, a fixed utc offset might not be sufficient even for past
dates.
>...
> Moreover, a program that rejects invalid times on input, but stores them
> for a long time may see its database silently corrupted after a zoneinfo
> update.
> Now it is time to make specific proposal. I would like to extend
> datetime.astimezone() method to work on naive datetime instances. Such
> instances will be assumed to be in local time and discontinuities will be
> handled as follows:
>
>
> 1. wall(t) == lt has a single solution. This is the trivial case and
> lt.astimezone(utc) and lt.astimezone(utc, which=i) for i=0,1 should return
> that solution.
>
> 2. wall(t) == lt has two solutions t1 and t2 such that t1 < t2. In this
> case lt.astimezone(utc) == lt.astimezone(utc, which=0) == t1 and
> lt.astimezone(utc, which=1) == t2.
In pytz terms: `which = not isdst` (end-of-DST-like transition: isdst
changes from True to False in the direction of utc time).
It resolves AmbiguousTimeError raised by `tz.localize(naive, is_dst=None)`.
> 3. wall(t) == lt has no solution. This happens when there is UTC time t0
> such that wall(t0) < lt and wall(t0+epsilon) > lt (a positive discontinuity
> at time t0). In this case lt.astimezone(utc) should return t0 + lt -
> wall(t0). I.e., we ignore the discontinuity and extend wall(t) linearly
> past t0. Obviously, in this case the invariant wall(lt.astimezone(utc)) ==
> lt won't hold. The "which" flag should be handled as follows:
> lt.astimezone(utc) == lt.astimezone(utc, which=0) and lt.astimezone(utc,
> which=0) == t0 + lt - wall(t0+eps).
It is inconsistent with the previous case: here `which = isdst` but
`which = not isdst` above.
`lt.astimezone(utc, which=0) == t0 + lt - wall(t0+eps)` corresponds to:
result = tz.normalize(tz.localize(lt, isdst=False))
i.e., `which = isdst` (t0 is at the start of DST and therefore isdst
changes from False to True).
It resolves NonExistentTimeError raised by `tz.localize(naive,
is_dst=None)`. start-of-DST-like transition ("Spring forward").
For example,
from datetime import datetime, timedelta
import pytz
tz = pytz.timezone('America/New_York')
# 2am -- non-existent time
print(tz.normalize(tz.localize(datetime(2015, 3, 8, 2), is_dst=False)))
# -> 2015-03-08 03:00:00-04:00 # after the jump (wall(t0+eps))
print(tz.localize(datetime(2015, 3, 8, 3), is_dst=None))
# -> 2015-03-08 03:00:00-04:00 # same time, unambiguous
# 2:01am -- non-existent time
print(tz.normalize(tz.localize(datetime(2015, 3, 8, 2, 1), is_dst=False)))
# -> 2015-03-08 03:01:00-04:00
print(tz.localize(datetime(2015, 3, 8, 3, 1), is_dst=None))
# -> 2015-03-08 03:01:00-04:00 # same time, unambiguous
# 2:59am non-existent time
dt = tz.normalize(tz.localize(datetime(2015, 3, 8, 2, 59), is_dst=True))
print(dt)
# -> 2015-03-08 01:59:00-05:00 # before the jump (wall(t0-eps))
print(tz.normalize(dt + timedelta(minutes=1)))
# -> 2015-03-08 03:00:00-04:00
> With the proposed features in place, one can use the naive code
>
> t = lt.astimezone(utc)
>
> and get predictable behavior in all cases and no crashes.
>
> A more sophisticated program can be written like this:
>
> t1 = lt.astimezone(utc, which=0)
> t2 = lt.astimezone(utc, which=1)
> if t1 == t2:
> t = t1
> elif t2 > t1:
> # ask the user to pick between t1 and t2 or raise
> AmbiguousLocalTimeError
> else:
> t = t1
> # warn the user that time was invalid and changed or raise
> InvalidLocalTimeError
More information about the Python-Dev
mailing list