[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