Search code examples
pythondjangotimezonepytz

Django timezone.make_aware raised AmbiguousTimeError for 2014-10-26 1:45:00


I found some strange things. Here some examples.

from django.utils import timezone
value = u'2014-10-26 01:45:00'
#I know that a variable has  'Europe / Moscow' timezone. Let's tell Django about it.
TZ = timezone.pytz.timezone('Europe/Moscow')
d = timezone.datetime.strptime(value,'%Y-%m-%d %H:%M:%S')
print timezone.make_aware(d,TZ)
#raised AmbiguousTimeError: 2014-10-26 01:45:00

And then the fun begins

print timezone.make_aware(d+timezone.timedelta(minutes=15),TZ)
#out: 2014-10-26 02:00:00+03:00
print timezone.make_aware(d+timezone.timedelta(minutes=14),TZ)
#raised AmbiguousTimeError
print timezone.make_aware(d-timezone.timedelta(minutes=46),TZ)
#out: 2014-10-26 00:59:00+04:00
print timezone.make_aware(d-timezone.timedelta(minutes=45),TZ)
#raised AmbiguousTimeError     

So AmbiguousTimeError raised between 2014-10-26 00:59:00 and 2014-10-26 02:00:00

WHY? And how solve it?


Solution

  • timezon.make_aware(d, TZ) is equivalent to TZ.localize(d, is_dst=None) that raises an error for ambiguous times: 2014-10-26 01:45:00 happens twice in Europe/Moscow timezone:

    # Europe/Moscow               UTC                           timestamp
    2014-10-26 00:45:00 MSK+0400; 2014-10-25 20:45:00 UTC+0000; 1414269900
    2014-10-26 01:00:00 MSK+0400; 2014-10-25 21:00:00 UTC+0000; 1414270800
    2014-10-26 01:15:00 MSK+0400; 2014-10-25 21:15:00 UTC+0000; 1414271700
    2014-10-26 01:30:00 MSK+0400; 2014-10-25 21:30:00 UTC+0000; 1414272600
    2014-10-26 01:45:00 MSK+0400; 2014-10-25 21:45:00 UTC+0000; 1414273500
    2014-10-26 01:15:00 MSK+0300; 2014-10-25 22:15:00 UTC+0000; 1414275300
    2014-10-26 01:30:00 MSK+0300; 2014-10-25 22:30:00 UTC+0000; 1414276200
    2014-10-26 01:45:00 MSK+0300; 2014-10-25 22:45:00 UTC+0000; 1414277100
    2014-10-26 02:00:00 MSK+0300; 2014-10-25 23:00:00 UTC+0000; 1414278000
    

    Notice: the utc offset is changed from +0400 to +0300 at 2am (Федеральный закон от 21 июля 2014 г. N 248-ФЗ).

    To avoid the exception, you could call TZ.localize(d) (note: no is_dst=None) that works fine for existing non-ambiguous times but may fail (return wrong answer) for non-existing or ambiguous times.

    If pytz Bug #1378150: Enhance support for end-of-DST-like ambiguous time is fixed then you could use TZ.localize(d, is_dst=True), TZ.localize(d, is_dst=False) to get time before and after the transition correspondingly.

    If the bug is not fixed you could use my answer from Parsing of Ordered Timestamps in Local Time (to UTC) While Observing Daylight Saving Time to get the time after the transition:

    # `naive` is a naive datetime object in local (Europe/Moscow) time
    if tz.localize(naive, is_dst=False) == tz.localize(naive, is_dst=True):
        # Example: 2014/10/26 in Europe/Moscow timezone
        # ambiguous time but is_dst=False/True yield the same result
        # i.e., tz.localize() can't help, find UTC time  manually
        #NOTE: assume there is no other changes to UTC offset today (local.day)
        new_offset = tz.localize(naive + timedelta(1), is_dst=None).utcoffset()
        assert tz.localize(naive).utcoffset() != new_offset
        utc = (naive - new_offset).replace(tzinfo=pytz.utc)
        local = utc.astimezone(tz)