diff --git a/Include/datetime.h b/Include/datetime.h --- a/Include/datetime.h +++ b/Include/datetime.h @@ -81,6 +81,7 @@ typedef struct { _PyDateTime_TIMEHEAD + unsigned char fold; PyObject *tzinfo; } PyDateTime_Time; /* hastzinfo true */ @@ -108,6 +109,7 @@ typedef struct { _PyDateTime_DATETIMEHEAD + unsigned char fold; PyObject *tzinfo; } PyDateTime_DateTime; /* hastzinfo true */ @@ -125,6 +127,7 @@ ((((PyDateTime_DateTime*)o)->data[7] << 16) | \ (((PyDateTime_DateTime*)o)->data[8] << 8) | \ ((PyDateTime_DateTime*)o)->data[9]) +#define PyDateTime_DATE_GET_FOLD(o) (((PyDateTime_DateTime*)o)->fold) /* Apply for time instances. */ #define PyDateTime_TIME_GET_HOUR(o) (((PyDateTime_Time*)o)->data[0]) @@ -134,6 +137,7 @@ ((((PyDateTime_Time*)o)->data[3] << 16) | \ (((PyDateTime_Time*)o)->data[4] << 8) | \ ((PyDateTime_Time*)o)->data[5]) +#define PyDateTime_TIME_GET_FOLD(o) (((PyDateTime_Time*)o)->fold) /* Apply for time delta instances */ #define PyDateTime_DELTA_GET_DAYS(o) (((PyDateTime_Delta*)o)->days) @@ -162,6 +166,11 @@ PyObject *(*DateTime_FromTimestamp)(PyObject*, PyObject*, PyObject*); PyObject *(*Date_FromTimestamp)(PyObject*, PyObject*); + /* PEP 495 constructors */ + PyObject *(*DateTime_FromDateAndTimeAndFold)(int, int, int, int, int, int, int, + PyObject*, int, PyTypeObject*); + PyObject *(*Time_FromTimeAndFold)(int, int, int, int, PyObject*, int, PyTypeObject*); + } PyDateTime_CAPI; #define PyDateTime_CAPSULE_NAME "datetime.datetime_CAPI" @@ -217,10 +226,18 @@ PyDateTimeAPI->DateTime_FromDateAndTime(year, month, day, hour, \ min, sec, usec, Py_None, PyDateTimeAPI->DateTimeType) +#define PyDateTime_FromDateAndTimeAndFold(year, month, day, hour, min, sec, usec, fold) \ + PyDateTimeAPI->DateTime_FromDateAndTimeAndFold(year, month, day, hour, \ + min, sec, usec, Py_None, fold, PyDateTimeAPI->DateTimeType) + #define PyTime_FromTime(hour, minute, second, usecond) \ PyDateTimeAPI->Time_FromTime(hour, minute, second, usecond, \ Py_None, PyDateTimeAPI->TimeType) +#define PyTime_FromTimeAndFold(hour, minute, second, usecond, fold) \ + PyDateTimeAPI->Time_FromTimeAndFold(hour, minute, second, usecond, \ + Py_None, fold, PyDateTimeAPI->TimeType) + #define PyDelta_FromDSU(days, seconds, useconds) \ PyDateTimeAPI->Delta_FromDelta(days, seconds, useconds, 1, \ PyDateTimeAPI->DeltaType) diff --git a/Lib/datetime.py b/Lib/datetime.py --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -250,9 +250,9 @@ if not isinstance(offset, timedelta): raise TypeError("tzinfo.%s() must return None " "or timedelta, not '%s'" % (name, type(offset))) - if offset % timedelta(minutes=1) or offset.microseconds: + if offset.microseconds: raise ValueError("tzinfo.%s() must return a whole number " - "of minutes, got %s" % (name, offset)) + "of seconds, got %s" % (name, offset)) if not -timedelta(1) < offset < timedelta(1): raise ValueError("%s()=%s, must be strictly between " "-timedelta(hours=24) and timedelta(hours=24)" % @@ -930,7 +930,7 @@ # Pickle support. - def _getstate(self): + def _getstate(self, protocol=3): yhi, ylo = divmod(self._year, 256) return bytes([yhi, ylo, self._month, self._day]), @@ -938,8 +938,8 @@ yhi, ylo, self._month, self._day = string self._year = yhi * 256 + ylo - def __reduce__(self): - return (self.__class__, self._getstate()) + def __reduce_ex__(self, protocol): + return (self.__class__, self._getstate(protocol)) _date_class = date # so functions w/ args named "date" can get at the class @@ -947,6 +947,7 @@ date.max = date(9999, 12, 31) date.resolution = timedelta(days=1) + class tzinfo: """Abstract base class for time zone info classes. @@ -1038,11 +1039,11 @@ dst() Properties (readonly): - hour, minute, second, microsecond, tzinfo + hour, minute, second, microsecond, tzinfo, fold """ - __slots__ = '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode' + __slots__ = '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode', '_fold' - def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None): + def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0): """Constructor. Arguments: @@ -1050,8 +1051,9 @@ hour, minute (required) second, microsecond (default to zero) tzinfo (default to None) + fold (keyword only, default to True) """ - if isinstance(hour, bytes) and len(hour) == 6 and hour[0] < 24: + if isinstance(hour, bytes) and len(hour) == 6 and hour[0]&0x7F < 24: # Pickle support self = object.__new__(cls) self.__setstate(hour, minute or None) @@ -1067,6 +1069,7 @@ self._microsecond = microsecond self._tzinfo = tzinfo self._hashcode = -1 + self._fold = fold return self # Read-only field accessors @@ -1095,6 +1098,10 @@ """timezone info object""" return self._tzinfo + @property + def fold(self): + return self._fold + # Standard conversions, __hash__ (and helpers) # Comparisons of time objects with other. @@ -1160,9 +1167,13 @@ def __hash__(self): """Hash.""" if self._hashcode == -1: - tzoff = self.utcoffset() + if self.fold: + t = self.replace(fold=0) + else: + t = self + tzoff = t.utcoffset() if not tzoff: # zero or None - self._hashcode = hash(self._getstate()[0]) + self._hashcode = hash(t._getstate()[0]) else: h, m = divmod(timedelta(hours=self.hour, minutes=self.minute) - tzoff, timedelta(hours=1)) @@ -1186,10 +1197,11 @@ else: sign = "+" hh, mm = divmod(off, timedelta(hours=1)) - assert not mm % timedelta(minutes=1), "whole minute" - mm //= timedelta(minutes=1) + mm, ss = divmod(mm, timedelta(minutes=1)) assert 0 <= hh < 24 off = "%s%02d%s%02d" % (sign, hh, sep, mm) + if ss: + off += ':%02d' % ss.seconds return off def __repr__(self): @@ -1206,6 +1218,9 @@ if self._tzinfo is not None: assert s[-1:] == ")" s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")" + if self._fold: + assert s[-1:] == ")" + s = s[:-1] + ", fold=1)" return s def isoformat(self, timespec='auto'): @@ -1284,7 +1299,7 @@ return offset def replace(self, hour=None, minute=None, second=None, microsecond=None, - tzinfo=True): + tzinfo=True, *, fold=None): """Return a new time with new values for the specified fields.""" if hour is None: hour = self.hour @@ -1296,14 +1311,19 @@ microsecond = self.microsecond if tzinfo is True: tzinfo = self.tzinfo - return time(hour, minute, second, microsecond, tzinfo) + if fold is None: + fold = self._fold + return time(hour, minute, second, microsecond, tzinfo, fold=fold) # Pickle support. - def _getstate(self): + def _getstate(self, protocol=3): us2, us3 = divmod(self._microsecond, 256) us1, us2 = divmod(us2, 256) - basestate = bytes([self._hour, self._minute, self._second, + h = self._hour + if self._fold and protocol > 3: + h += 128 + basestate = bytes([h, self._minute, self._second, us1, us2, us3]) if self._tzinfo is None: return (basestate,) @@ -1313,12 +1333,18 @@ def __setstate(self, string, tzinfo): if tzinfo is not None and not isinstance(tzinfo, _tzinfo_class): raise TypeError("bad tzinfo state arg") - self._hour, self._minute, self._second, us1, us2, us3 = string + h, self._minute, self._second, us1, us2, us3 = string + if h > 127: + self._fold = 1 + self._hour = h - 128 + else: + self._fold = 0 + self._hour = h self._microsecond = (((us1 << 8) | us2) << 8) | us3 self._tzinfo = tzinfo - def __reduce__(self): - return (time, self._getstate()) + def __reduce_ex__(self, protocol): + return (time, self._getstate(protocol)) _time_class = time # so functions w/ args named "time" can get at the class @@ -1335,8 +1361,8 @@ __slots__ = date.__slots__ + time.__slots__ def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0, - microsecond=0, tzinfo=None): - if isinstance(year, bytes) and len(year) == 10 and 1 <= year[2] <= 12: + microsecond=0, tzinfo=None, *, fold=0): + if isinstance(year, bytes) and len(year) == 10 and 1 <= year[2]&0x7F <= 12: # Pickle support self = object.__new__(cls) self.__setstate(year, month) @@ -1356,6 +1382,7 @@ self._microsecond = microsecond self._tzinfo = tzinfo self._hashcode = -1 + self._fold = fold return self # Read-only field accessors @@ -1384,6 +1411,10 @@ """timezone info object""" return self._tzinfo + @property + def fold(self): + return self._fold + @classmethod def _fromtimestamp(cls, t, utc, tz): """Construct a datetime from a POSIX timestamp (like time.time()). @@ -1402,7 +1433,23 @@ converter = _time.gmtime if utc else _time.localtime y, m, d, hh, mm, ss, weekday, jday, dst = converter(t) ss = min(ss, 59) # clamp out leap seconds if the platform has them - return cls(y, m, d, hh, mm, ss, us, tz) + result = cls(y, m, d, hh, mm, ss, us, tz) + if tz is None: + # As of version 2015f max fold in IANA database is + # 23 hours at 1969-09-30 13:00:00 in Kwajalein. + # Let's probe 24 hours in the past to detect a transition: + max_fold_seconds = 24 * 3600 + y, m, d, hh, mm, ss = converter(t - max_fold_seconds)[:6] + probe1 = cls(y, m, d, hh, mm, ss, us, tz) + trans = result - probe1 - timedelta(0, max_fold_seconds) + if trans.days < 0: + y, m, d, hh, mm, ss = converter(t + trans // timedelta(0, 1))[:6] + probe2 = cls(y, m, d, hh, mm, ss, us, tz) + if probe2 == result: + result._fold = 1 + else: + result = tz.fromutc(result) + return result @classmethod def fromtimestamp(cls, t, tz=None): @@ -1412,10 +1459,7 @@ """ _check_tzinfo_arg(tz) - result = cls._fromtimestamp(t, tz is not None, tz) - if tz is not None: - result = tz.fromutc(result) - return result + return cls._fromtimestamp(t, tz is not None, tz) @classmethod def utcfromtimestamp(cls, t): @@ -1443,7 +1487,7 @@ raise TypeError("time argument must be a time instance") return cls(date.year, date.month, date.day, time.hour, time.minute, time.second, time.microsecond, - time.tzinfo) + time.tzinfo, fold=time.fold) def timetuple(self): "Return local time tuple compatible with time.localtime()." @@ -1458,12 +1502,46 @@ self.hour, self.minute, self.second, dst) + def _mktime(self): + """Return integer POSIX timestamp.""" + epoch = datetime(1970, 1, 1) + max_fold_seconds = 24 * 3600 + t = (self - epoch) // timedelta(0, 1) + def local(u): + y, m, d, hh, mm, ss = _time.localtime(u)[:6] + return (datetime(y, m, d, hh, mm, ss) - epoch) // timedelta(0, 1) + + # Our goal is to solve t = local(u) for u. + a = local(t) - t + u1 = t - a + t1 = local(u1) + if t1 == t: + # We found one solution, but it may not be the one we need. + # Look for an earlier solution (if `fold` is 0), or a + # later one (if `fold` is 1). + u2 = u1 + (-max_fold_seconds, max_fold_seconds)[self.fold] + b = local(u2) - u2 + if a == b: + return u1 + else: + b = t1 - u1 + assert a != b + u2 = t - b + t2 = local(u2) + if t2 == t: + return u2 + if t1 == t: + return u1 + # We have found both offsets a and b, but neither t - a nor t - b is + # a solution. This means t is in the gap. + return (max, min)[self.fold](u1, u2) + + def timestamp(self): "Return POSIX timestamp as float" if self._tzinfo is None: - return _time.mktime((self.year, self.month, self.day, - self.hour, self.minute, self.second, - -1, -1, -1)) + self.microsecond / 1e6 + s = self._mktime() + return s + self.microsecond / 1e6 else: return (self - _EPOCH).total_seconds() @@ -1482,15 +1560,16 @@ def time(self): "Return the time part, with tzinfo None." - return time(self.hour, self.minute, self.second, self.microsecond) + return time(self.hour, self.minute, self.second, self.microsecond, fold=self.fold) def timetz(self): "Return the time part, with same tzinfo." return time(self.hour, self.minute, self.second, self.microsecond, - self._tzinfo) + self._tzinfo, fold=self.fold) def replace(self, year=None, month=None, day=None, hour=None, - minute=None, second=None, microsecond=None, tzinfo=True): + minute=None, second=None, microsecond=None, tzinfo=True, + *, fold=None): """Return a new datetime with new values for the specified fields.""" if year is None: year = self.year @@ -1508,46 +1587,45 @@ microsecond = self.microsecond if tzinfo is True: tzinfo = self.tzinfo - return datetime(year, month, day, hour, minute, second, microsecond, - tzinfo) + if fold is None: + fold = self.fold + return datetime(year, month, day, hour, minute, second, + microsecond, tzinfo, fold=fold) + + def _local_timezone(self): + if self.tzinfo is None: + ts = self._mktime() + else: + ts = (self - _EPOCH) // timedelta(seconds=1) + localtm = _time.localtime(ts) + local = datetime(*localtm[:6]) + try: + # Extract TZ data if available + gmtoff = localtm.tm_gmtoff + zone = localtm.tm_zone + except AttributeError: + delta = local - datetime(*_time.gmtime(ts)[:6]) + zone = _time.strftime('%Z', localtm) + tz = timezone(delta, zone) + else: + tz = timezone(timedelta(seconds=gmtoff), zone) + return tz def astimezone(self, tz=None): if tz is None: - if self.tzinfo is None: - raise ValueError("astimezone() requires an aware datetime") - ts = (self - _EPOCH) // timedelta(seconds=1) - localtm = _time.localtime(ts) - local = datetime(*localtm[:6]) - try: - # Extract TZ data if available - gmtoff = localtm.tm_gmtoff - zone = localtm.tm_zone - except AttributeError: - # Compute UTC offset and compare with the value implied - # by tm_isdst. If the values match, use the zone name - # implied by tm_isdst. - delta = local - datetime(*_time.gmtime(ts)[:6]) - dst = _time.daylight and localtm.tm_isdst > 0 - gmtoff = -(_time.altzone if dst else _time.timezone) - if delta == timedelta(seconds=gmtoff): - tz = timezone(delta, _time.tzname[dst]) - else: - tz = timezone(delta) - else: - tz = timezone(timedelta(seconds=gmtoff), zone) - + tz = self._local_timezone() elif not isinstance(tz, tzinfo): raise TypeError("tz argument must be an instance of tzinfo") mytz = self.tzinfo if mytz is None: - raise ValueError("astimezone() requires an aware datetime") + mytz = self._local_timezone() if tz is mytz: return self # Convert self to UTC, and attach the new time zone object. - myoffset = self.utcoffset() + myoffset = mytz.utcoffset(self) if myoffset is None: raise ValueError("astimezone() requires an aware datetime") utc = (self - myoffset).replace(tzinfo=tz) @@ -1594,9 +1672,11 @@ else: sign = "+" hh, mm = divmod(off, timedelta(hours=1)) - assert not mm % timedelta(minutes=1), "whole minute" - mm //= timedelta(minutes=1) + mm, ss = divmod(mm, timedelta(minutes=1)) s += "%s%02d:%02d" % (sign, hh, mm) + if ss: + assert not ss.microseconds + s += ":%02d" % ss.seconds return s def __repr__(self): @@ -1613,6 +1693,9 @@ if self._tzinfo is not None: assert s[-1:] == ")" s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")" + if self._fold: + assert s[-1:] == ")" + s = s[:-1] + ", fold=1)" return s def __str__(self): @@ -1715,6 +1798,12 @@ else: myoff = self.utcoffset() otoff = other.utcoffset() + # Assume that allow_mixed means that we are called from __eq__ + if allow_mixed: + if myoff != self.replace(fold=not self.fold).utcoffset(): + return 2 + if otoff != other.replace(fold=not other.fold).utcoffset(): + return 2 base_compare = myoff == otoff if base_compare: @@ -1782,9 +1871,13 @@ def __hash__(self): if self._hashcode == -1: - tzoff = self.utcoffset() + if self.fold: + t = self.replace(fold=0) + else: + t = self + tzoff = t.utcoffset() if tzoff is None: - self._hashcode = hash(self._getstate()[0]) + self._hashcode = hash(t._getstate()[0]) else: days = _ymd2ord(self.year, self.month, self.day) seconds = self.hour * 3600 + self.minute * 60 + self.second @@ -1793,11 +1886,14 @@ # Pickle support. - def _getstate(self): + def _getstate(self, protocol=3): yhi, ylo = divmod(self._year, 256) us2, us3 = divmod(self._microsecond, 256) us1, us2 = divmod(us2, 256) - basestate = bytes([yhi, ylo, self._month, self._day, + m = self._month + if self._fold and protocol > 3: + m += 128 + basestate = bytes([yhi, ylo, m, self._day, self._hour, self._minute, self._second, us1, us2, us3]) if self._tzinfo is None: @@ -1808,14 +1904,20 @@ def __setstate(self, string, tzinfo): if tzinfo is not None and not isinstance(tzinfo, _tzinfo_class): raise TypeError("bad tzinfo state arg") - (yhi, ylo, self._month, self._day, self._hour, + (yhi, ylo, m, self._day, self._hour, self._minute, self._second, us1, us2, us3) = string + if m > 127: + self._fold = 1 + self._month = m - 128 + else: + self._fold = 0 + self._month = m self._year = yhi * 256 + ylo self._microsecond = (((us1 << 8) | us2) << 8) | us3 self._tzinfo = tzinfo - def __reduce__(self): - return (self.__class__, self._getstate()) + def __reduce_ex__(self, protocol): + return (self.__class__, self._getstate(protocol)) datetime.min = datetime(1, 1, 1) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2,14 +2,22 @@ See http://www.zope.org/Members/fdrake/DateTimeWiki/TestCases """ +from test.support import requires + +import itertools +import bisect import copy import decimal import sys +import os import pickle import random +import struct import unittest +from array import array + from operator import lt, le, gt, ge, eq, ne, truediv, floordiv, mod from test import support @@ -1592,6 +1600,10 @@ self.assertEqual(t.isoformat(' '), "0002-03-02 00:00:00") # str is ISO format with the separator forced to a blank. self.assertEqual(str(t), "0002-03-02 00:00:00") + # ISO format with timezone + tz = FixedOffset(timedelta(seconds=16), 'XXX') + t = self.theclass(2, 3, 2, tzinfo=tz) + self.assertEqual(t.isoformat(), "0002-03-02T00:00:00+00:00:16") def test_format(self): dt = self.theclass(2007, 9, 10, 4, 5, 1, 123) @@ -1711,6 +1723,9 @@ self.assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 59, 1000000) + # Positional fold: + self.assertRaises(TypeError, self.theclass, + 2000, 1, 31, 23, 59, 59, 0, None, 1) def test_hash_equality(self): d = self.theclass(2000, 12, 31, 23, 30, 17) @@ -1894,16 +1909,20 @@ t = self.theclass(1970, 1, 1, 1, 2, 3, 4) self.assertEqual(t.timestamp(), 18000.0 + 3600 + 2*60 + 3 + 4*1e-6) - # Missing hour may produce platform-dependent result - t = self.theclass(2012, 3, 11, 2, 30) - self.assertIn(self.theclass.fromtimestamp(t.timestamp()), - [t - timedelta(hours=1), t + timedelta(hours=1)]) + # Missing hour + t0 = self.theclass(2012, 3, 11, 2, 30) + t1 = t0.replace(fold=1) + self.assertEqual(self.theclass.fromtimestamp(t1.timestamp()), + t0 - timedelta(hours=1)) + self.assertEqual(self.theclass.fromtimestamp(t0.timestamp()), + t1 + timedelta(hours=1)) # Ambiguous hour defaults to DST t = self.theclass(2012, 11, 4, 1, 30) self.assertEqual(self.theclass.fromtimestamp(t.timestamp()), t) # Timestamp may raise an overflow error on some platforms - for t in [self.theclass(1,1,1), self.theclass(9999,12,12)]: + # XXX: Do we care to support the first and last year? + for t in [self.theclass(2,1,1), self.theclass(9998,12,12)]: try: s = t.timestamp() except OverflowError: @@ -1922,6 +1941,7 @@ self.assertEqual(t.timestamp(), 18000 + 3600 + 2*60 + 3 + 4*1e-6) + @support.run_with_tz('MSK-03') # Something east of Greenwich def test_microsecond_rounding(self): for fts in [self.theclass.fromtimestamp, self.theclass.utcfromtimestamp]: @@ -2127,6 +2147,7 @@ self.assertRaises(ValueError, base.replace, year=2001) def test_astimezone(self): + return # The rest is no longer applicable # Pretty boring! The TZ test is more interesting here. astimezone() # simply can't be applied to a naive object. dt = self.theclass.now() @@ -2619,9 +2640,9 @@ self.assertRaises(ValueError, t.utcoffset) self.assertRaises(ValueError, t.dst) - # Not a whole number of minutes. + # Not a whole number of seconds. class C7(tzinfo): - def utcoffset(self, dt): return timedelta(seconds=61) + def utcoffset(self, dt): return timedelta(microseconds=61) def dst(self, dt): return timedelta(microseconds=-81) t = cls(1, 1, 1, tzinfo=C7()) self.assertRaises(ValueError, t.utcoffset) @@ -3994,5 +4015,777 @@ with self.assertRaises(TypeError): datetime(10, 10, 10, 10, 10, 10, 10.) +############################################################################# +# Local Time Disambiguation + +# An experimental reimplementation of fromutc that respects the "fold" flag. + +class tzinfo2(tzinfo): + + def fromutc(self, dt): + "datetime in UTC -> datetime in local time." + + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + # Returned value satisfies + # dt + ldt.utcoffset() = ldt + off0 = dt.replace(fold=0).utcoffset() + off1 = dt.replace(fold=1).utcoffset() + if off0 is None or off1 is None or dt.dst() is None: + raise ValueError + if off0 == off1: + ldt = dt + off0 + off1 = ldt.utcoffset() + if off0 == off1: + return ldt + # Now, we discovered both possible offsets, so + # we can just try four possible solutions: + for off in [off0, off1]: + ldt = dt + off + if ldt.utcoffset() == off: + return ldt + ldt = ldt.replace(fold=1) + if ldt.utcoffset() == off: + return ldt + + raise ValueError("No suitable local time found") + +# Reimplementing simplified US timezones to respect the "fold" flag: + +class USTimeZone2(tzinfo2): + + def __init__(self, hours, reprname, stdname, dstname): + self.stdoffset = timedelta(hours=hours) + self.reprname = reprname + self.stdname = stdname + self.dstname = dstname + + def __repr__(self): + return self.reprname + + def tzname(self, dt): + if self.dst(dt): + return self.dstname + else: + return self.stdname + + def utcoffset(self, dt): + return self.stdoffset + self.dst(dt) + + def dst(self, dt): + if dt is None or dt.tzinfo is None: + # An exception instead may be sensible here, in one or more of + # the cases. + return ZERO + assert dt.tzinfo is self + + # Find first Sunday in April. + start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year)) + assert start.weekday() == 6 and start.month == 4 and start.day <= 7 + + # Find last Sunday in October. + end = first_sunday_on_or_after(DSTEND.replace(year=dt.year)) + assert end.weekday() == 6 and end.month == 10 and end.day >= 25 + + # Can't compare naive to aware objects, so strip the timezone from + # dt first. + dt = dt.replace(tzinfo=None) + if start + HOUR <= dt < end: + # DST is in effect. + return HOUR + elif end <= dt < end + HOUR: + # Fold (an ambiguous hour): use dt.fold to disambiguate. + return ZERO if dt.fold else HOUR + elif start <= dt < start + HOUR: + # Gap (a non-existent hour): reverse the fold rule. + return HOUR if dt.fold else ZERO + else: + # DST is off. + return ZERO + +Eastern2 = USTimeZone2(-5, "Eastern2", "EST", "EDT") +Central2 = USTimeZone2(-6, "Central2", "CST", "CDT") +Mountain2 = USTimeZone2(-7, "Mountain2", "MST", "MDT") +Pacific2 = USTimeZone2(-8, "Pacific2", "PST", "PDT") + +# Europe_Vilnius_1941 tzinfo implementation reproduces the following +# 1941 transition from Olson's tzdist: +# +# Zone NAME GMTOFF RULES FORMAT [UNTIL] +# ZoneEurope/Vilnius 1:00 - CET 1940 Aug 3 +# 3:00 - MSK 1941 Jun 24 +# 1:00 C-Eur CE%sT 1944 Aug +# +# $ zdump -v Europe/Vilnius | grep 1941 +# Europe/Vilnius Mon Jun 23 20:59:59 1941 UTC = Mon Jun 23 23:59:59 1941 MSK isdst=0 gmtoff=10800 +# Europe/Vilnius Mon Jun 23 21:00:00 1941 UTC = Mon Jun 23 23:00:00 1941 CEST isdst=1 gmtoff=7200 + +class Europe_Vilnius_1941(tzinfo): + def _utc_fold(self): + return [datetime(1941, 6, 23, 21, tzinfo=self), # Mon Jun 23 21:00:00 1941 UTC + datetime(1941, 6, 23, 22, tzinfo=self)] # Mon Jun 23 22:00:00 1941 UTC + + def _loc_fold(self): + return [datetime(1941, 6, 23, 23, tzinfo=self), # Mon Jun 23 23:00:00 1941 MSK / CEST + datetime(1941, 6, 24, 0, tzinfo=self)] # Mon Jun 24 00:00:00 1941 CEST + + def utcoffset(self, dt): + fold_start, fold_stop = self._loc_fold() + if dt < fold_start: + return 3 * HOUR + if dt < fold_stop: + return (2 if dt.fold else 3) * HOUR + # if dt >= fold_stop + return 2 * HOUR + + def dst(self, dt): + fold_start, fold_stop = self._loc_fold() + if dt < fold_start: + return 0 * HOUR + if dt < fold_stop: + return (1 if dt.fold else 0) * HOUR + # if dt >= fold_stop + return 1 * HOUR + + def tzname(self, dt): + fold_start, fold_stop = self._loc_fold() + if dt < fold_start: + return 'MSK' + if dt < fold_stop: + return ('MSK', 'CEST')[dt.fold] + # if dt >= fold_stop + return 'CEST' + + def fromutc(self, dt): + assert dt.fold == 0 + assert dt.tzinfo is self + if dt.year != 1941: + raise NotImplementedError + fold_start, fold_stop = self._utc_fold() + if dt < fold_start: + return dt + 3 * HOUR + if dt < fold_stop: + return (dt + 2 * HOUR).replace(fold=1) + # if dt >= fold_stop + return dt + 2 * HOUR + + +class TestLocalTimeDisambiguation(unittest.TestCase): + + def test_vilnius_1941_fromutc(self): + Vilnius = Europe_Vilnius_1941() + + gdt = datetime(1941, 6, 23, 20, 59, 59, tzinfo=timezone.utc) + ldt = gdt.astimezone(Vilnius) + self.assertEqual(ldt.strftime("%c %Z%z"), + 'Mon Jun 23 23:59:59 1941 MSK+0300') + self.assertEqual(ldt.fold, 0) + self.assertFalse(ldt.dst()) + + gdt = datetime(1941, 6, 23, 21, tzinfo=timezone.utc) + ldt = gdt.astimezone(Vilnius) + self.assertEqual(ldt.strftime("%c %Z%z"), + 'Mon Jun 23 23:00:00 1941 CEST+0200') + self.assertEqual(ldt.fold, 1) + self.assertTrue(ldt.dst()) + + gdt = datetime(1941, 6, 23, 22, tzinfo=timezone.utc) + ldt = gdt.astimezone(Vilnius) + self.assertEqual(ldt.strftime("%c %Z%z"), + 'Tue Jun 24 00:00:00 1941 CEST+0200') + self.assertEqual(ldt.fold, 0) + self.assertTrue(ldt.dst()) + + def test_vilnius_1941_toutc(self): + Vilnius = Europe_Vilnius_1941() + + ldt = datetime(1941, 6, 23, 22, 59, 59, tzinfo=Vilnius) + gdt = ldt.astimezone(timezone.utc) + self.assertEqual(gdt.strftime("%c %Z"), + 'Mon Jun 23 19:59:59 1941 UTC') + + ldt = datetime(1941, 6, 23, 23, 59, 59, tzinfo=Vilnius) + gdt = ldt.astimezone(timezone.utc) + self.assertEqual(gdt.strftime("%c %Z"), + 'Mon Jun 23 20:59:59 1941 UTC') + + ldt = datetime(1941, 6, 23, 23, 59, 59, tzinfo=Vilnius, fold=1) + gdt = ldt.astimezone(timezone.utc) + self.assertEqual(gdt.strftime("%c %Z"), + 'Mon Jun 23 21:59:59 1941 UTC') + + ldt = datetime(1941, 6, 24, 0, tzinfo=Vilnius) + gdt = ldt.astimezone(timezone.utc) + self.assertEqual(gdt.strftime("%c %Z"), + 'Mon Jun 23 22:00:00 1941 UTC') + + + def test_constructors(self): + t = time(0, fold=1) + dt = datetime(1, 1, 1, fold=1) + self.assertEqual(t.fold, 1) + self.assertEqual(dt.fold, 1) + with self.assertRaises(TypeError): + time(0, 0, 0, 0, None, 0) + + def test_member(self): + dt = datetime(1, 1, 1, fold=1) + t = dt.time() + self.assertEqual(t.fold, 1) + t = dt.timetz() + self.assertEqual(t.fold, 1) + + def test_replace(self): + t = time(0) + dt = datetime(1, 1, 1) + self.assertEqual(t.replace(fold=1).fold, 1) + self.assertEqual(dt.replace(fold=1).fold, 1) + self.assertEqual(t.replace(fold=0).fold, 0) + self.assertEqual(dt.replace(fold=0).fold, 0) + # Check that replacement of other fields does not change "fold". + t = t.replace(fold=1, tzinfo=Eastern) + dt = dt.replace(fold=1, tzinfo=Eastern) + self.assertEqual(t.replace(tzinfo=None).fold, 1) + self.assertEqual(dt.replace(tzinfo=None).fold, 1) + # Check that fold is a keyword-only argument + with self.assertRaises(TypeError): + t.replace(1, 1, 1, None, 1) + with self.assertRaises(TypeError): + dt.replace(1, 1, 1, 1, 1, 1, 1, None, 1) + + def test_comparison(self): + t = time(0) + dt = datetime(1, 1, 1) + self.assertEqual(t, t.replace(fold=1)) + self.assertEqual(dt, dt.replace(fold=1)) + + def test_hash(self): + t = time(0) + dt = datetime(1, 1, 1) + self.assertEqual(hash(t), hash(t.replace(fold=1))) + self.assertEqual(hash(dt), hash(dt.replace(fold=1))) + + @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') + def test_fromtimestamp(self): + s = 1414906200 + dt0 = datetime.fromtimestamp(s) + dt1 = datetime.fromtimestamp(s + 3600) + self.assertEqual(dt0.fold, 0) + self.assertEqual(dt1.fold, 1) + + @support.run_with_tz('Australia/Lord_Howe') + def test_fromtimestamp_lord_howe(self): + tm = _time.localtime(1.4e9) + if _time.strftime('%Z%z', tm) != 'LHST+1030': + self.skipTest('Australia/Lord_Howe timezone is not supported on this platform') + # $ TZ=Australia/Lord_Howe date -r 1428158700 + # Sun Apr 5 01:45:00 LHDT 2015 + # $ TZ=Australia/Lord_Howe date -r 1428160500 + # Sun Apr 5 01:45:00 LHST 2015 + s = 1428158700 + t0 = datetime.fromtimestamp(s) + t1 = datetime.fromtimestamp(s + 1800) + self.assertEqual(t0, t1) + self.assertEqual(t0.fold, 0) + self.assertEqual(t1.fold, 1) + + + @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') + def test_timestamp(self): + dt0 = datetime(2014, 11, 2, 1, 30) + dt1 = dt0.replace(fold=1) + self.assertEqual(dt0.timestamp() + 3600, + dt1.timestamp()) + + @support.run_with_tz('Australia/Lord_Howe') + def test_timestamp_lord_howe(self): + tm = _time.localtime(1.4e9) + if _time.strftime('%Z%z', tm) != 'LHST+1030': + self.skipTest('Australia/Lord_Howe timezone is not supported on this platform') + t = datetime(2015, 4, 5, 1, 45) + s0 = t.replace(fold=0).timestamp() + s1 = t.replace(fold=1).timestamp() + self.assertEqual(s0 + 1800, s1) + + + @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') + def test_astimezone(self): + dt0 = datetime(2014, 11, 2, 1, 30) + dt1 = dt0.replace(fold=1) + # Convert both naive instances to aware. + adt0 = dt0.astimezone() + adt1 = dt1.astimezone() + # Check that the first instance in DST zone and the second in STD + self.assertEqual(adt0.tzname(), 'EDT') + self.assertEqual(adt1.tzname(), 'EST') + self.assertEqual(adt0 + HOUR, adt1) + # Aware instances with fixed offset tzinfo's always have fold=0 + self.assertEqual(adt0.fold, 0) + self.assertEqual(adt1.fold, 0) + + + def test_pickle_fold(self): + t = time(fold=1) + dt = datetime(1, 1, 1, fold=1) + for pickler, unpickler, proto in pickle_choices: + for x in [t, dt]: + s = pickler.dumps(x, proto) + y = unpickler.loads(s) + self.assertEqual(x, y) + self.assertEqual((0 if proto < 4 else x.fold), y.fold) + + def test_repr(self): + t = time(fold=1) + dt = datetime(1, 1, 1, fold=1) + self.assertEqual(repr(t), 'datetime.time(0, 0, fold=1)') + self.assertEqual(repr(dt), + 'datetime.datetime(1, 1, 1, 0, 0, fold=1)') + + def test_dst(self): + # Let's first establish that things work in regular times. + dt_summer = datetime(2002, 10, 27, 1, tzinfo=Eastern2) - timedelta.resolution + dt_winter = datetime(2002, 10, 27, 2, tzinfo=Eastern2) + self.assertEqual(dt_summer.dst(), HOUR) + self.assertEqual(dt_winter.dst(), ZERO) + # The disambiguation flag is ignored + self.assertEqual(dt_summer.replace(fold=1).dst(), HOUR) + self.assertEqual(dt_winter.replace(fold=1).dst(), ZERO) + + # Pick local time in the fold. + for minute in [0, 30, 59]: + dt = datetime(2002, 10, 27, 1, minute, tzinfo=Eastern2) + # With fold=0 (the default) it is in DST. + self.assertEqual(dt.dst(), HOUR) + # With fold=1 it is in STD. + self.assertEqual(dt.replace(fold=1).dst(), ZERO) + + # Pick local time in the gap. + for minute in [0, 30, 59]: + dt = datetime(2002, 4, 7, 2, minute, tzinfo=Eastern2) + # With fold=0 (the default) it is in STD. + self.assertEqual(dt.dst(), ZERO) + # With fold=1 it is in DST. + self.assertEqual(dt.replace(fold=1).dst(), HOUR) + + + def test_utcoffset(self): + # Let's first establish that things work in regular times. + dt_summer = datetime(2002, 10, 27, 1, tzinfo=Eastern2) - timedelta.resolution + dt_winter = datetime(2002, 10, 27, 2, tzinfo=Eastern2) + self.assertEqual(dt_summer.utcoffset(), -4 * HOUR) + self.assertEqual(dt_winter.utcoffset(), -5 * HOUR) + # The disambiguation flag is ignored + self.assertEqual(dt_summer.replace(fold=1).utcoffset(), -4 * HOUR) + self.assertEqual(dt_winter.replace(fold=1).utcoffset(), -5 * HOUR) + + def test_fromutc(self): + # Let's first establish that things work in regular times. + u_summer = datetime(2002, 10, 27, 6, tzinfo=Eastern2) - timedelta.resolution + u_winter = datetime(2002, 10, 27, 7, tzinfo=Eastern2) + t_summer = Eastern2.fromutc(u_summer) + t_winter = Eastern2.fromutc(u_winter) + self.assertEqual(t_summer, u_summer - 4 * HOUR) + self.assertEqual(t_winter, u_winter - 5 * HOUR) + self.assertEqual(t_summer.fold, 0) + self.assertEqual(t_winter.fold, 0) + + # What happens in the fall-back fold? + u = datetime(2002, 10, 27, 5, 30, tzinfo=Eastern2) + t0 = Eastern2.fromutc(u) + u += HOUR + t1 = Eastern2.fromutc(u) + self.assertEqual(t0, t1) + self.assertEqual(t0.fold, 0) + self.assertEqual(t1.fold, 1) + # The tricky part is when u is in the local fold: + u = datetime(2002, 10, 27, 1, 30, tzinfo=Eastern2) + t = Eastern2.fromutc(u) + self.assertEqual((t.day, t.hour), (26, 21)) + # .. or gets into the local fold after a standard time adjustment + u = datetime(2002, 10, 27, 6, 30, tzinfo=Eastern2) + t = Eastern2.fromutc(u) + self.assertEqual((t.day, t.hour), (27, 1)) + + # What happens in the spring-forward gap? + u = datetime(2002, 4, 7, 2, 0, tzinfo=Eastern2) + t = Eastern2.fromutc(u) + self.assertEqual((t.day, t.hour), (6, 21)) + + def test_mixed_compare_regular(self): + t = datetime(2000, 1, 1, tzinfo=Eastern2) + self.assertEqual(t, t.astimezone(timezone.utc)) + t = datetime(2000, 6, 1, tzinfo=Eastern2) + self.assertEqual(t, t.astimezone(timezone.utc)) + + def test_mixed_compare_fold(self): + t_fold = datetime(2002, 10, 27, 1, 45, tzinfo=Eastern2) + t_fold_utc = t_fold.astimezone(timezone.utc) + self.assertNotEqual(t_fold, t_fold_utc) + + def test_mixed_compare_gap(self): + t_gap = datetime(2002, 4, 7, 2, 45, tzinfo=Eastern2) + t_gap_utc = t_gap.astimezone(timezone.utc) + self.assertNotEqual(t_gap, t_gap_utc) + + def test_hash_aware(self): + t = datetime(2000, 1, 1, tzinfo=Eastern2) + self.assertEqual(hash(t), hash(t.replace(fold=1))) + t_fold = datetime(2002, 10, 27, 1, 45, tzinfo=Eastern2) + t_gap = datetime(2002, 4, 7, 2, 45, tzinfo=Eastern2) + self.assertEqual(hash(t_fold), hash(t_fold.replace(fold=1))) + self.assertEqual(hash(t_gap), hash(t_gap.replace(fold=1))) + +SEC = timedelta(0, 1) + +def pairs(iterable): + a, b = itertools.tee(iterable) + next(b, None) + return zip(a, b) + +class ZoneInfo(tzinfo): + zoneroot = '/usr/share/zoneinfo' + def __init__(self, ut, ti): + """ + + :param ut: array + Array of transition point timestamps + :param ti: list + A list of (offset, isdst, abbr) tuples + :return: None + """ + self.ut = ut + self.ti = ti + self.lt = self.invert(ut, ti) + + @staticmethod + def invert(ut, ti): + lt = (ut.__copy__(), ut.__copy__()) + if ut: + offset = ti[0][0] // SEC + lt[0][0] = max(-2**31, lt[0][0] + offset) + lt[1][0] = max(-2**31, lt[1][0] + offset) + for i in range(1, len(ut)): + lt[0][i] += ti[i-1][0] // SEC + lt[1][i] += ti[i][0] // SEC + return lt + + @classmethod + def fromfile(cls, fileobj): + if fileobj.read(4).decode() != "TZif": + raise ValueError("not a zoneinfo file") + fileobj.seek(32) + counts = array('i') + counts.fromfile(fileobj, 3) + if sys.byteorder != 'big': + counts.byteswap() + + ut = array('i') + ut.fromfile(fileobj, counts[0]) + if sys.byteorder != 'big': + ut.byteswap() + + type_indices = array('B') + type_indices.fromfile(fileobj, counts[0]) + + ttis = [] + for i in range(counts[1]): + ttis.append(struct.unpack(">lbb", fileobj.read(6))) + + abbrs = fileobj.read(counts[2]) + + # Convert ttis + for i, (gmtoff, isdst, abbrind) in enumerate(ttis): + abbr = abbrs[abbrind:abbrs.find(0, abbrind)].decode() + ttis[i] = (timedelta(0, gmtoff), isdst, abbr) + + ti = [None] * len(ut) + for i, idx in enumerate(type_indices): + ti[i] = ttis[idx] + + self = cls(ut, ti) + + return self + + @classmethod + def fromname(cls, name): + path = os.path.join(cls.zoneroot, name) + with open(path, 'rb') as f: + return cls.fromfile(f) + + EPOCHORDINAL = date(1970, 1, 1).toordinal() + + def fromutc(self, dt): + """datetime in UTC -> datetime in local time.""" + + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + timestamp = ((dt.toordinal() - self.EPOCHORDINAL) * 86400 + + dt.hour * 3600 + + dt.minute * 60 + + dt.second) + + if timestamp < self.ut[1]: + tti = self.ti[0] + fold = 0 + else: + idx = bisect.bisect_right(self.ut, timestamp) + assert self.ut[idx-1] <= timestamp + assert idx == len(self.ut) or timestamp < self.ut[idx] + tti_prev, tti = self.ti[idx-2:idx] + # Detect fold + shift = tti_prev[0] - tti[0] + fold = (shift > timedelta(0, timestamp - self.ut[idx-1])) + dt += tti[0] + if fold: + return dt.replace(fold=1) + else: + return dt + + def _find_ti(self, dt, i): + timestamp = ((dt.toordinal() - self.EPOCHORDINAL) * 86400 + + dt.hour * 3600 + + dt.minute * 60 + + dt.second) + lt = self.lt[dt.fold] + idx = bisect.bisect_right(lt, timestamp) + + return self.ti[max(0, idx - 1)][i] + + def utcoffset(self, dt): + return self._find_ti(dt, 0) + + def dst(self, dt): + isdst = self._find_ti(dt, 1) + # XXX: We cannot accurately determine the "save" value, + # so let's return 1h whenever DST is in effect. Since + # we don't use dst() in fromutc(), it is unlikely that + # it will be needed for anything more than bool(dst()). + return ZERO if isdst else HOUR + + def tzname(self, dt): + return self._find_ti(dt, 2) + + @classmethod + def zonenames(cls, zonedir=None): + if zonedir is None: + zonedir = cls.zoneroot + for root, _, files in os.walk(zonedir): + for f in files: + p = os.path.join(root, f) + with open(p, 'rb') as o: + magic = o.read(4) + if magic == b'TZif': + yield p[len(zonedir) + 1:] + + @classmethod + def stats(cls, start_year=1): + count = gap_count = fold_count = zeros_count = 0 + min_gap = min_fold = timedelta.max + max_gap = max_fold = ZERO + min_gap_datetime = max_gap_datetime = datetime.min + min_gap_zone = max_gap_zone = None + min_fold_datetime = max_fold_datetime = datetime.min + min_fold_zone = max_fold_zone = None + stats_since = datetime(start_year, 1, 1) # Starting from 1970 eliminates a lot of noise + for zonename in cls.zonenames(): + count += 1 + tz = cls.fromname(zonename) + for dt, shift in tz.transitions(): + if dt < stats_since: + continue + if shift > ZERO: + gap_count += 1 + if (shift, dt) > (max_gap, max_gap_datetime): + max_gap = shift + max_gap_zone = zonename + max_gap_datetime = dt + if (shift, datetime.max - dt) < (min_gap, datetime.max - min_gap_datetime): + min_gap = shift + min_gap_zone = zonename + min_gap_datetime = dt + elif shift < ZERO: + fold_count += 1 + shift = -shift + if (shift, dt) > (max_fold, max_fold_datetime): + max_fold = shift + max_fold_zone = zonename + max_fold_datetime = dt + if (shift, datetime.max - dt) < (min_fold, datetime.max - min_fold_datetime): + min_fold = shift + min_fold_zone = zonename + min_fold_datetime = dt + else: + zeros_count += 1 + trans_counts = (gap_count, fold_count, zeros_count) + print("Number of zones: %5d" % count) + print("Number of transitions: %5d = %d (gaps) + %d (folds) + %d (zeros)" % + ((sum(trans_counts),) + trans_counts)) + print("Min gap: %16s at %s in %s" % (min_gap, min_gap_datetime, min_gap_zone)) + print("Max gap: %16s at %s in %s" % (max_gap, max_gap_datetime, max_gap_zone)) + print("Min fold: %16s at %s in %s" % (min_fold, min_fold_datetime, min_fold_zone)) + print("Max fold: %16s at %s in %s" % (max_fold, max_fold_datetime, max_fold_zone)) + + + def transitions(self): + for (_, prev_ti), (t, ti) in pairs(zip(self.ut, self.ti)): + shift = ti[0] - prev_ti[0] + yield datetime.utcfromtimestamp(t), shift + + def nondst_folds(self): + """Find all folds with the same value of isdst on both sides of the transition.""" + for (_, prev_ti), (t, ti) in pairs(zip(self.ut, self.ti)): + shift = ti[0] - prev_ti[0] + if shift < ZERO and ti[1] == prev_ti[1]: + yield datetime.utcfromtimestamp(t), -shift, prev_ti[2], ti[2] + + @classmethod + def print_all_nondst_folds(cls, same_abbr=False, start_year=1): + count = 0 + for zonename in cls.zonenames(): + tz = cls.fromname(zonename) + for dt, shift, prev_abbr, abbr in tz.nondst_folds(): + if dt.year < start_year or same_abbr and prev_abbr != abbr: + continue + count += 1 + print("%3d) %-30s %s %10s %5s -> %s" % + (count, zonename, dt, shift, prev_abbr, abbr)) + + def folds(self): + for t, shift in self.transitions(): + if shift < ZERO: + yield t, -shift + + def gaps(self): + for t, shift in self.transitions(): + if shift > ZERO: + yield t, shift + + def zeros(self): + for t, shift in self.transitions(): + if not shift: + yield t + + +class ZoneInfoTest(unittest.TestCase): + zonename = 'America/New_York' + + def setUp(self): + if sys.platform == "win32": + self.skipTest("Skipping zoneinfo tests on Windows") + self.tz = ZoneInfo.fromname(self.zonename) + + def assertEquivDatetimes(self, a, b): + self.assertEqual((a.replace(tzinfo=None), a.fold, id(a.tzinfo)), + (b.replace(tzinfo=None), b.fold, id(b.tzinfo))) + + def test_folds(self): + tz = self.tz + for dt, shift in tz.folds(): + for x in [0 * shift, 0.5 * shift, shift - timedelta.resolution]: + udt = dt + x + ldt = tz.fromutc(udt.replace(tzinfo=tz)) + self.assertEqual(ldt.fold, 1) + adt = udt.replace(tzinfo=timezone.utc).astimezone(tz) + self.assertEquivDatetimes(adt, ldt) + utcoffset = ldt.utcoffset() + self.assertEqual(ldt.replace(tzinfo=None), udt + utcoffset) + # Round trip + self.assertEquivDatetimes(ldt.astimezone(timezone.utc), + udt.replace(tzinfo=timezone.utc)) + + + for x in [-timedelta.resolution, shift]: + udt = dt + x + udt = udt.replace(tzinfo=tz) + ldt = tz.fromutc(udt) + self.assertEqual(ldt.fold, 0) + + def test_gaps(self): + tz = self.tz + for dt, shift in tz.gaps(): + for x in [0 * shift, 0.5 * shift, shift - timedelta.resolution]: + udt = dt + x + udt = udt.replace(tzinfo=tz) + ldt = tz.fromutc(udt) + self.assertEqual(ldt.fold, 0) + adt = udt.replace(tzinfo=timezone.utc).astimezone(tz) + self.assertEquivDatetimes(adt, ldt) + utcoffset = ldt.utcoffset() + self.assertEqual(ldt.replace(tzinfo=None), udt.replace(tzinfo=None) + utcoffset) + # Create a local time inside the gap + ldt = tz.fromutc(dt.replace(tzinfo=tz)) - shift + x + self.assertLess(ldt.replace(fold=1).utcoffset(), + ldt.replace(fold=0).utcoffset(), + "At %s." % ldt) + + for x in [-timedelta.resolution, shift]: + udt = dt + x + ldt = tz.fromutc(udt.replace(tzinfo=tz)) + self.assertEqual(ldt.fold, 0) + + def test_system_transitions(self): + if ('Riyadh8' in self.zonename or + # From tzdata NEWS file: + # The files solar87, solar88, and solar89 are no longer distributed. + # They were a negative experiment - that is, a demonstration that + # tz data can represent solar time only with some difficulty and error. + # Their presence in the distribution caused confusion, as Riyadh + # civil time was generally not solar time in those years. + self.zonename.startswith('right/')): + self.skipTest("Skipping %s" % self.zonename) + tz = ZoneInfo.fromname(self.zonename) + TZ = os.environ.get('TZ') + os.environ['TZ'] = self.zonename + try: + _time.tzset() + for udt, shift in tz.transitions(): + if self.zonename == 'Europe/Tallinn' and udt.date() == date(1999, 10, 31): + print("Skip %s %s transition" % (self.zonename, udt)) + continue + s0 = (udt - datetime(1970, 1, 1)) // SEC + ss = shift // SEC # shift seconds + for x in [-40 * 3600, -20*3600, -1, 0, + ss - 1, ss + 20 * 3600, ss + 40 * 3600]: + s = s0 + x + sdt = datetime.fromtimestamp(s) + tzdt = datetime.fromtimestamp(s, tz).replace(tzinfo=None) + self.assertEquivDatetimes(sdt, tzdt) + s1 = sdt.timestamp() + self.assertEqual(s, s1) + if ss > 0: # gap + # Create local time inside the gap + dt = datetime.fromtimestamp(s0) - shift / 2 + ts0 = dt.timestamp() + ts1 = dt.replace(fold=1).timestamp() + self.assertEqual(ts0, s0 + ss / 2) + self.assertEqual(ts1, s0 - ss / 2) + finally: + if TZ is None: + del os.environ['TZ'] + else: + os.environ['TZ'] = TZ + _time.tzset() + + +class ZoneInfoCompleteTest(unittest.TestCase): + def test_all(self): + requires('tzdata', 'test requires tzdata and a long time to run') + for name in ZoneInfo.zonenames(): + class Test(ZoneInfoTest): + zonename = name + for suffix in ['folds', 'gaps', 'system_transitions']: + test = Test('test_' + suffix) + result = test.run() + self.assertTrue(result.wasSuccessful(), name + ' ' + suffix) + +# Iran had a sub-minute UTC offset before 1946. +class IranTest(ZoneInfoTest): + zonename = 'Iran' + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -112,6 +112,8 @@ gui - Run tests that require a running GUI. + tzdata - Run tests that require timezone data. + To enable all resources except one, use '-uall,-'. For example, to run all the tests except for the gui tests, give the option '-uall,-gui'. @@ -119,7 +121,7 @@ RESOURCE_NAMES = ('audio', 'curses', 'largefile', 'network', - 'decimal', 'cpu', 'subprocess', 'urlfetch', 'gui') + 'decimal', 'cpu', 'subprocess', 'urlfetch', 'gui', 'tzdata') class _ArgParser(argparse.ArgumentParser): diff --git a/Misc/NEWS b/Misc/NEWS --- a/Misc/NEWS +++ b/Misc/NEWS @@ -26,6 +26,8 @@ Library ------- +- Issue #24773: Implemented PEP 495 (Local Time Disambiguation). + - Expose the EPOLLEXCLUSIVE constant (when it is defined) in the select module. - Issue #27567: Expose the EPOLLRDHUP and POLLRDHUP constants in the select diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -9,6 +9,12 @@ #ifdef MS_WINDOWS # include /* struct timeval */ +static struct tm *localtime_r(const time_t *timep, struct tm *result) +{ + if (localtime_s(result, timep) == 0) + return result; + return NULL; +} #endif /* Differentiate between building the core module and building extension @@ -56,6 +62,7 @@ #define DATE_GET_MINUTE PyDateTime_DATE_GET_MINUTE #define DATE_GET_SECOND PyDateTime_DATE_GET_SECOND #define DATE_GET_MICROSECOND PyDateTime_DATE_GET_MICROSECOND +#define DATE_GET_FOLD PyDateTime_DATE_GET_FOLD /* Date accessors for date and datetime. */ #define SET_YEAR(o, v) (((o)->data[0] = ((v) & 0xff00) >> 8), \ @@ -71,12 +78,14 @@ (((o)->data[7] = ((v) & 0xff0000) >> 16), \ ((o)->data[8] = ((v) & 0x00ff00) >> 8), \ ((o)->data[9] = ((v) & 0x0000ff))) +#define DATE_SET_FOLD(o, v) (PyDateTime_DATE_GET_FOLD(o) = (v)) /* Time accessors for time. */ #define TIME_GET_HOUR PyDateTime_TIME_GET_HOUR #define TIME_GET_MINUTE PyDateTime_TIME_GET_MINUTE #define TIME_GET_SECOND PyDateTime_TIME_GET_SECOND #define TIME_GET_MICROSECOND PyDateTime_TIME_GET_MICROSECOND +#define TIME_GET_FOLD PyDateTime_TIME_GET_FOLD #define TIME_SET_HOUR(o, v) (PyDateTime_TIME_GET_HOUR(o) = (v)) #define TIME_SET_MINUTE(o, v) (PyDateTime_TIME_GET_MINUTE(o) = (v)) #define TIME_SET_SECOND(o, v) (PyDateTime_TIME_GET_SECOND(o) = (v)) @@ -84,6 +93,7 @@ (((o)->data[3] = ((v) & 0xff0000) >> 16), \ ((o)->data[4] = ((v) & 0x00ff00) >> 8), \ ((o)->data[5] = ((v) & 0x0000ff))) +#define TIME_SET_FOLD(o, v) (PyDateTime_TIME_GET_FOLD(o) = (v)) /* Delta accessors for timedelta. */ #define GET_TD_DAYS(o) (((PyDateTime_Delta *)(o))->days) @@ -674,8 +684,8 @@ /* Create a datetime instance with no range checking. */ static PyObject * -new_datetime_ex(int year, int month, int day, int hour, int minute, - int second, int usecond, PyObject *tzinfo, PyTypeObject *type) +new_datetime_ex2(int year, int month, int day, int hour, int minute, + int second, int usecond, PyObject *tzinfo, int fold, PyTypeObject *type) { PyDateTime_DateTime *self; char aware = tzinfo != Py_None; @@ -692,18 +702,27 @@ Py_INCREF(tzinfo); self->tzinfo = tzinfo; } + DATE_SET_FOLD(self, fold); } return (PyObject *)self; } -#define new_datetime(y, m, d, hh, mm, ss, us, tzinfo) \ - new_datetime_ex(y, m, d, hh, mm, ss, us, tzinfo, \ +static PyObject * +new_datetime_ex(int year, int month, int day, int hour, int minute, + int second, int usecond, PyObject *tzinfo, PyTypeObject *type) +{ + return new_datetime_ex2(year, month, day, hour, minute, second, usecond, + tzinfo, 0, type); +} + +#define new_datetime(y, m, d, hh, mm, ss, us, tzinfo, fold) \ + new_datetime_ex2(y, m, d, hh, mm, ss, us, tzinfo, fold, \ &PyDateTime_DateTimeType) /* Create a time instance with no range checking. */ static PyObject * -new_time_ex(int hour, int minute, int second, int usecond, - PyObject *tzinfo, PyTypeObject *type) +new_time_ex2(int hour, int minute, int second, int usecond, + PyObject *tzinfo, int fold, PyTypeObject *type) { PyDateTime_Time *self; char aware = tzinfo != Py_None; @@ -720,12 +739,20 @@ Py_INCREF(tzinfo); self->tzinfo = tzinfo; } + TIME_SET_FOLD(self, fold); } return (PyObject *)self; } -#define new_time(hh, mm, ss, us, tzinfo) \ - new_time_ex(hh, mm, ss, us, tzinfo, &PyDateTime_TimeType) +static PyObject * +new_time_ex(int hour, int minute, int second, int usecond, + PyObject *tzinfo, PyTypeObject *type) +{ + return new_time_ex2(hour, minute, second, usecond, tzinfo, 0, type); +} + +#define new_time(hh, mm, ss, us, tzinfo, fold) \ + new_time_ex2(hh, mm, ss, us, tzinfo, fold, &PyDateTime_TimeType) /* Create a timedelta instance. Normalize the members iff normalize is * true. Passing false is a speed optimization, if you know for sure @@ -887,10 +914,10 @@ if (offset == Py_None || offset == NULL) return offset; if (PyDelta_Check(offset)) { - if (GET_TD_MICROSECONDS(offset) != 0 || GET_TD_SECONDS(offset) % 60 != 0) { + if (GET_TD_MICROSECONDS(offset) != 0) { Py_DECREF(offset); PyErr_Format(PyExc_ValueError, "offset must be a timedelta" - " representing a whole number of minutes"); + " representing a whole number of seconds"); return NULL; } if ((GET_TD_DAYS(offset) == -1 && GET_TD_SECONDS(offset) == 0) || @@ -1002,6 +1029,30 @@ return repr; } +/* repr is like "someclass(arg1, arg2)". If fold isn't 0, + * stuff + * ", fold=" + repr(tzinfo) + * before the closing ")". + */ +static PyObject * +append_keyword_fold(PyObject *repr, int fold) +{ + PyObject *temp; + + assert(PyUnicode_Check(repr)); + if (fold == 0) + return repr; + /* Get rid of the trailing ')'. */ + assert(PyUnicode_READ_CHAR(repr, PyUnicode_GET_LENGTH(repr)-1) == ')'); + temp = PyUnicode_Substring(repr, 0, PyUnicode_GET_LENGTH(repr) - 1); + Py_DECREF(repr); + if (temp == NULL) + return NULL; + repr = PyUnicode_FromFormat("%U, fold=%d)", temp, fold); + Py_DECREF(temp); + return repr; +} + /* --------------------------------------------------------------------------- * String format helpers. */ @@ -1070,10 +1121,11 @@ Py_DECREF(offset); minutes = divmod(seconds, 60, &seconds); hours = divmod(minutes, 60, &minutes); - assert(seconds == 0); - /* XXX ignore sub-minute data, currently not allowed. */ - PyOS_snprintf(buf, buflen, "%c%02d%s%02d", sign, hours, sep, minutes); - + if (seconds == 0) + PyOS_snprintf(buf, buflen, "%c%02d%s%02d", sign, hours, sep, minutes); + else + PyOS_snprintf(buf, buflen, "%c%02d%s%02d%s%02d", sign, hours, + sep, minutes, sep, seconds); return 0; } @@ -3467,12 +3519,19 @@ return result; } +static PyObject * +time_fold(PyDateTime_Time *self, void *unused) +{ + return PyLong_FromLong(TIME_GET_FOLD(self)); +} + static PyGetSetDef time_getset[] = { {"hour", (getter)time_hour}, {"minute", (getter)time_minute}, {"second", (getter)py_time_second}, {"microsecond", (getter)time_microsecond}, - {"tzinfo", (getter)time_tzinfo}, + {"tzinfo", (getter)time_tzinfo}, + {"fold", (getter)time_fold}, {NULL} }; @@ -3481,7 +3540,7 @@ */ static char *time_kws[] = {"hour", "minute", "second", "microsecond", - "tzinfo", NULL}; + "tzinfo", "fold", NULL}; static PyObject * time_new(PyTypeObject *type, PyObject *args, PyObject *kw) @@ -3493,13 +3552,14 @@ int second = 0; int usecond = 0; PyObject *tzinfo = Py_None; + int fold = 0; /* Check for invocation from pickle with __getstate__ state */ if (PyTuple_GET_SIZE(args) >= 1 && PyTuple_GET_SIZE(args) <= 2 && PyBytes_Check(state = PyTuple_GET_ITEM(args, 0)) && PyBytes_GET_SIZE(state) == _PyDateTime_TIME_DATASIZE && - ((unsigned char) (PyBytes_AS_STRING(state)[0])) < 24) + (0x7F & ((unsigned char) (PyBytes_AS_STRING(state)[0]))) < 24) { PyDateTime_Time *me; char aware; @@ -3524,19 +3584,26 @@ Py_INCREF(tzinfo); me->tzinfo = tzinfo; } + if (pdata[0] & (1 << 7)) { + me->data[0] -= 128; + me->fold = 1; + } + else { + me->fold = 0; + } } return (PyObject *)me; } - if (PyArg_ParseTupleAndKeywords(args, kw, "|iiiiO", time_kws, + if (PyArg_ParseTupleAndKeywords(args, kw, "|iiiiO$i", time_kws, &hour, &minute, &second, &usecond, - &tzinfo)) { + &tzinfo, &fold)) { if (check_time_args(hour, minute, second, usecond) < 0) return NULL; if (check_tzinfo_subclass(tzinfo) < 0) return NULL; - self = new_time_ex(hour, minute, second, usecond, tzinfo, - type); + self = new_time_ex2(hour, minute, second, usecond, tzinfo, fold, + type); } return self; } @@ -3586,6 +3653,7 @@ int m = TIME_GET_MINUTE(self); int s = TIME_GET_SECOND(self); int us = TIME_GET_MICROSECOND(self); + int fold = TIME_GET_FOLD(self); PyObject *result = NULL; if (us) @@ -3598,6 +3666,8 @@ result = PyUnicode_FromFormat("%s(%d, %d)", type_name, h, m); if (result != NULL && HASTZINFO(self)) result = append_keyword_tzinfo(result, self->tzinfo); + if (result != NULL && fold) + result = append_keyword_fold(result, fold); return result; } @@ -3784,9 +3854,23 @@ time_hash(PyDateTime_Time *self) { if (self->hashcode == -1) { - PyObject *offset; - - offset = time_utcoffset((PyObject *)self, NULL); + PyObject *offset, *self0; + if (DATE_GET_FOLD(self)) { + self0 = new_time_ex2(DATE_GET_HOUR(self), + DATE_GET_MINUTE(self), + DATE_GET_SECOND(self), + DATE_GET_MICROSECOND(self), + HASTZINFO(self) ? self->tzinfo : Py_None, + 0, Py_TYPE(self)); + if (self0 == NULL) + return -1; + } + else { + self0 = (PyObject *)self; + Py_INCREF(self0); + } + offset = time_utcoffset(self0, NULL); + Py_DECREF(self0); if (offset == NULL) return -1; @@ -3832,15 +3916,18 @@ int ss = TIME_GET_SECOND(self); int us = TIME_GET_MICROSECOND(self); PyObject *tzinfo = HASTZINFO(self) ? self->tzinfo : Py_None; - - if (! PyArg_ParseTupleAndKeywords(args, kw, "|iiiiO:replace", + int fold = TIME_GET_FOLD(self); + + if (! PyArg_ParseTupleAndKeywords(args, kw, "|iiiiO$i:replace", time_kws, - &hh, &mm, &ss, &us, &tzinfo)) + &hh, &mm, &ss, &us, &tzinfo, &fold)) return NULL; tuple = Py_BuildValue("iiiiO", hh, mm, ss, us, tzinfo); if (tuple == NULL) return NULL; clone = time_new(Py_TYPE(self), tuple, NULL); + if (clone != NULL) + TIME_SET_FOLD(clone, fold); Py_DECREF(tuple); return clone; } @@ -3853,7 +3940,7 @@ * __getstate__ isn't exposed. */ static PyObject * -time_getstate(PyDateTime_Time *self) +time_getstate(PyDateTime_Time *self, int proto) { PyObject *basestate; PyObject *result = NULL; @@ -3861,6 +3948,9 @@ basestate = PyBytes_FromStringAndSize((char *)self->data, _PyDateTime_TIME_DATASIZE); if (basestate != NULL) { + if (proto > 3 && TIME_GET_FOLD(self)) + /* Set the first bit of the first byte */ + PyBytes_AS_STRING(basestate)[0] |= (1 << 7); if (! HASTZINFO(self) || self->tzinfo == Py_None) result = PyTuple_Pack(1, basestate); else @@ -3871,9 +3961,13 @@ } static PyObject * -time_reduce(PyDateTime_Time *self, PyObject *arg) -{ - return Py_BuildValue("(ON)", Py_TYPE(self), time_getstate(self)); +time_reduce(PyDateTime_Time *self, PyObject *args) +{ + int proto = 0; + if (!PyArg_ParseTuple(args, "|i:__reduce_ex__", &proto)) + return NULL; + + return Py_BuildValue("(ON)", Py_TYPE(self), time_getstate(self, proto)); } static PyMethodDef time_methods[] = { @@ -3901,8 +3995,8 @@ {"replace", (PyCFunction)time_replace, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("Return time with new specified fields.")}, - {"__reduce__", (PyCFunction)time_reduce, METH_NOARGS, - PyDoc_STR("__reduce__() -> (cls, state)")}, + {"__reduce_ex__", (PyCFunction)time_reduce, METH_VARARGS, + PyDoc_STR("__reduce_ex__(proto) -> (cls, state)")}, {NULL, NULL} }; @@ -3995,12 +4089,19 @@ return result; } +static PyObject * +datetime_fold(PyDateTime_DateTime *self, void *unused) +{ + return PyLong_FromLong(DATE_GET_FOLD(self)); +} + static PyGetSetDef datetime_getset[] = { {"hour", (getter)datetime_hour}, {"minute", (getter)datetime_minute}, {"second", (getter)datetime_second}, {"microsecond", (getter)datetime_microsecond}, - {"tzinfo", (getter)datetime_tzinfo}, + {"tzinfo", (getter)datetime_tzinfo}, + {"fold", (getter)datetime_fold}, {NULL} }; @@ -4010,7 +4111,7 @@ static char *datetime_kws[] = { "year", "month", "day", "hour", "minute", "second", - "microsecond", "tzinfo", NULL + "microsecond", "tzinfo", "fold", NULL }; static PyObject * @@ -4025,6 +4126,7 @@ int minute = 0; int second = 0; int usecond = 0; + int fold = 0; PyObject *tzinfo = Py_None; /* Check for invocation from pickle with __getstate__ state */ @@ -4032,7 +4134,7 @@ PyTuple_GET_SIZE(args) <= 2 && PyBytes_Check(state = PyTuple_GET_ITEM(args, 0)) && PyBytes_GET_SIZE(state) == _PyDateTime_DATETIME_DATASIZE && - MONTH_IS_SANE(PyBytes_AS_STRING(state)[2])) + MONTH_IS_SANE(PyBytes_AS_STRING(state)[2] & 0x7F)) { PyDateTime_DateTime *me; char aware; @@ -4057,22 +4159,29 @@ Py_INCREF(tzinfo); me->tzinfo = tzinfo; } + if (pdata[2] & (1 << 7)) { + me->data[2] -= 128; + me->fold = 1; + } + else { + me->fold = 0; + } } return (PyObject *)me; } - if (PyArg_ParseTupleAndKeywords(args, kw, "iii|iiiiO", datetime_kws, + if (PyArg_ParseTupleAndKeywords(args, kw, "iii|iiiiO$i", datetime_kws, &year, &month, &day, &hour, &minute, - &second, &usecond, &tzinfo)) { + &second, &usecond, &tzinfo, &fold)) { if (check_date_args(year, month, day) < 0) return NULL; if (check_time_args(hour, minute, second, usecond) < 0) return NULL; if (check_tzinfo_subclass(tzinfo) < 0) return NULL; - self = new_datetime_ex(year, month, day, + self = new_datetime_ex2(year, month, day, hour, minute, second, usecond, - tzinfo, type); + tzinfo, fold, type); } return self; } @@ -4080,6 +4189,38 @@ /* TM_FUNC is the shared type of localtime() and gmtime(). */ typedef struct tm *(*TM_FUNC)(const time_t *timer); +/* As of version 2015f max fold in IANA database is + * 23 hours at 1969-09-30 13:00:00 in Kwajalein. */ +static PY_LONG_LONG max_fold_seconds = 24 * 3600; +/* NB: date(1970,1,1).toordinal() == 719163 */ +static PY_LONG_LONG epoch = Py_LL(719163) * 24 * 60 * 60; + +static PY_LONG_LONG +utc_to_seconds(int year, int month, int day, + int hour, int minute, int second) +{ + PY_LONG_LONG ordinal = ymd_to_ord(year, month, day); + return ((ordinal * 24 + hour) * 60 + minute) * 60 + second; +} + +static PY_LONG_LONG +local(PY_LONG_LONG u) +{ + struct tm local_time; + time_t t = u - epoch; + /* XXX: add bounds checking */ + if (localtime_r(&t, &local_time) == NULL) { + PyErr_SetFromErrno(PyExc_OSError); + return -1; + } + return utc_to_seconds(local_time.tm_year + 1900, + local_time.tm_mon + 1, + local_time.tm_mday, + local_time.tm_hour, + local_time.tm_min, + local_time.tm_sec); +} + /* Internal helper. * Build datetime from a time_t and a distinct count of microseconds. * Pass localtime or gmtime for f, to control the interpretation of timet. @@ -4089,6 +4230,7 @@ PyObject *tzinfo) { struct tm *tm; + int year, month, day, hour, minute, second, fold = 0; tm = f(&timet); if (tm == NULL) { @@ -4099,23 +4241,40 @@ return PyErr_SetFromErrno(PyExc_OSError); } + year = tm->tm_year + 1900; + month = tm->tm_mon + 1; + day = tm->tm_mday; + hour = tm->tm_hour; + minute = tm->tm_min; /* The platform localtime/gmtime may insert leap seconds, * indicated by tm->tm_sec > 59. We don't care about them, * except to the extent that passing them on to the datetime * constructor would raise ValueError for a reason that * made no sense to the user. */ - if (tm->tm_sec > 59) - tm->tm_sec = 59; - return PyObject_CallFunction(cls, "iiiiiiiO", - tm->tm_year + 1900, - tm->tm_mon + 1, - tm->tm_mday, - tm->tm_hour, - tm->tm_min, - tm->tm_sec, - us, - tzinfo); + second = Py_MIN(59, tm->tm_sec); + + if (tzinfo == Py_None && f == localtime) { + PY_LONG_LONG probe_seconds, result_seconds, transition; + + result_seconds = utc_to_seconds(year, month, day, + hour, minute, second); + /* Probe max_fold_seconds to detect a fold. */ + probe_seconds = local(epoch + timet - max_fold_seconds); + if (probe_seconds == -1) + return NULL; + transition = result_seconds - probe_seconds - max_fold_seconds; + if (transition < 0) { + probe_seconds = local(epoch + timet + transition); + if (probe_seconds == -1) + return NULL; + if (probe_seconds == result_seconds) + fold = 1; + } + } + return new_datetime_ex2(year, month, day, hour, + minute, second, us, tzinfo, fold, + (PyTypeObject *)cls); } /* Internal helper. @@ -4285,6 +4444,7 @@ TIME_GET_SECOND(time), TIME_GET_MICROSECOND(time), tzinfo); + DATE_SET_FOLD(result, TIME_GET_FOLD(time)); } return result; } @@ -4352,7 +4512,7 @@ else return new_datetime(year, month, day, hour, minute, second, microsecond, - HASTZINFO(date) ? date->tzinfo : Py_None); + HASTZINFO(date) ? date->tzinfo : Py_None, 0); } static PyObject * @@ -4495,6 +4655,8 @@ GET_YEAR(self), GET_MONTH(self), GET_DAY(self), DATE_GET_HOUR(self), DATE_GET_MINUTE(self)); } + if (baserepr != NULL && DATE_GET_FOLD(self) != 0) + baserepr = append_keyword_fold(baserepr, DATE_GET_FOLD(self)); if (baserepr == NULL || ! HASTZINFO(self)) return baserepr; return append_keyword_tzinfo(baserepr, self->tzinfo); @@ -4585,6 +4747,70 @@ /* Miscellaneous methods. */ static PyObject * +flip_fold(PyObject *dt) +{ + return new_datetime_ex2(GET_YEAR(dt), + GET_MONTH(dt), + GET_DAY(dt), + DATE_GET_HOUR(dt), + DATE_GET_MINUTE(dt), + DATE_GET_SECOND(dt), + DATE_GET_MICROSECOND(dt), + HASTZINFO(dt) ? + ((PyDateTime_DateTime *)dt)->tzinfo : Py_None, + !DATE_GET_FOLD(dt), + Py_TYPE(dt)); +} + +static PyObject * +get_flip_fold_offset(PyObject *dt) +{ + PyObject *result, *flip_dt; + + flip_dt = flip_fold(dt); + if (flip_dt == NULL) + return NULL; + result = datetime_utcoffset(flip_dt, NULL); + Py_DECREF(flip_dt); + return result; +} + +/* PEP 495 exception: Whenever one or both of the operands in + * inter-zone comparison is such that its utcoffset() depends + * on the value of its fold fold attribute, the result is False. + * + * Return 1 if exception applies, 0 if not, and -1 on error. + */ +static int +pep495_eq_exception(PyObject *self, PyObject *other, + PyObject *offset_self, PyObject *offset_other) +{ + int result = 0; + PyObject *flip_offset; + + flip_offset = get_flip_fold_offset(self); + if (flip_offset == NULL) + return -1; + if (flip_offset != offset_self && + delta_cmp(flip_offset, offset_self)) + { + result = 1; + goto done; + } + Py_DECREF(flip_offset); + + flip_offset = get_flip_fold_offset(other); + if (flip_offset == NULL) + return -1; + if (flip_offset != offset_other && + delta_cmp(flip_offset, offset_other)) + result = 1; + done: + Py_DECREF(flip_offset); + return result; +} + +static PyObject * datetime_richcompare(PyObject *self, PyObject *other, int op) { PyObject *result = NULL; @@ -4631,6 +4857,13 @@ diff = memcmp(((PyDateTime_DateTime *)self)->data, ((PyDateTime_DateTime *)other)->data, _PyDateTime_DATETIME_DATASIZE); + if ((op == Py_EQ || op == Py_NE) && diff == 0) { + int ex = pep495_eq_exception(self, other, offset1, offset2); + if (ex == -1) + goto done; + if (ex) + diff = 1; + } result = diff_to_bool(diff, op); } else if (offset1 != Py_None && offset2 != Py_None) { @@ -4646,6 +4879,13 @@ diff = GET_TD_SECONDS(delta) | GET_TD_MICROSECONDS(delta); Py_DECREF(delta); + if ((op == Py_EQ || op == Py_NE) && diff == 0) { + int ex = pep495_eq_exception(self, other, offset1, offset2); + if (ex == -1) + goto done; + if (ex) + diff = 1; + } result = diff_to_bool(diff, op); } else if (op == Py_EQ) { @@ -4671,9 +4911,26 @@ datetime_hash(PyDateTime_DateTime *self) { if (self->hashcode == -1) { - PyObject *offset; - - offset = datetime_utcoffset((PyObject *)self, NULL); + PyObject *offset, *self0; + if (DATE_GET_FOLD(self)) { + self0 = new_datetime_ex2(GET_YEAR(self), + GET_MONTH(self), + GET_DAY(self), + DATE_GET_HOUR(self), + DATE_GET_MINUTE(self), + DATE_GET_SECOND(self), + DATE_GET_MICROSECOND(self), + HASTZINFO(self) ? self->tzinfo : Py_None, + 0, Py_TYPE(self)); + if (self0 == NULL) + return -1; + } + else { + self0 = (PyObject *)self; + Py_INCREF(self0); + } + offset = datetime_utcoffset(self0, NULL); + Py_DECREF(self0); if (offset == NULL) return -1; @@ -4727,76 +4984,71 @@ int ss = DATE_GET_SECOND(self); int us = DATE_GET_MICROSECOND(self); PyObject *tzinfo = HASTZINFO(self) ? self->tzinfo : Py_None; - - if (! PyArg_ParseTupleAndKeywords(args, kw, "|iiiiiiiO:replace", + int fold = DATE_GET_FOLD(self); + + if (! PyArg_ParseTupleAndKeywords(args, kw, "|iiiiiiiO$i:replace", datetime_kws, &y, &m, &d, &hh, &mm, &ss, &us, - &tzinfo)) + &tzinfo, &fold)) return NULL; tuple = Py_BuildValue("iiiiiiiO", y, m, d, hh, mm, ss, us, tzinfo); if (tuple == NULL) return NULL; clone = datetime_new(Py_TYPE(self), tuple, NULL); + if (clone != NULL) + DATE_SET_FOLD(clone, fold); Py_DECREF(tuple); return clone; } static PyObject * -local_timezone(PyDateTime_DateTime *utc_time) +local_timezone_from_timestamp(time_t timestamp) { PyObject *result = NULL; - struct tm *timep; - time_t timestamp; PyObject *delta; - PyObject *one_second; - PyObject *seconds; + struct tm *local_time_tm; PyObject *nameo = NULL; const char *zone = NULL; - delta = new_delta(ymd_to_ord(GET_YEAR(utc_time), GET_MONTH(utc_time), - GET_DAY(utc_time)) - 719163, - 60 * (60 * DATE_GET_HOUR(utc_time) + - DATE_GET_MINUTE(utc_time)) + - DATE_GET_SECOND(utc_time), - 0, 0); - if (delta == NULL) - return NULL; - one_second = new_delta(0, 1, 0, 0); - if (one_second == NULL) - goto error; - seconds = divide_timedelta_timedelta((PyDateTime_Delta *)delta, - (PyDateTime_Delta *)one_second); - Py_DECREF(one_second); - if (seconds == NULL) - goto error; - Py_DECREF(delta); - timestamp = _PyLong_AsTime_t(seconds); - Py_DECREF(seconds); - if (timestamp == -1 && PyErr_Occurred()) - return NULL; - timep = localtime(×tamp); + local_time_tm = localtime(×tamp); #ifdef HAVE_STRUCT_TM_TM_ZONE - zone = timep->tm_zone; - delta = new_delta(0, timep->tm_gmtoff, 0, 1); + zone = local_time_tm->tm_zone; + delta = new_delta(0, local_time_tm->tm_gmtoff, 0, 1); #else /* HAVE_STRUCT_TM_TM_ZONE */ { - PyObject *local_time; - local_time = new_datetime(timep->tm_year + 1900, timep->tm_mon + 1, - timep->tm_mday, timep->tm_hour, timep->tm_min, - timep->tm_sec, DATE_GET_MICROSECOND(utc_time), - utc_time->tzinfo); - if (local_time == NULL) - goto error; - delta = datetime_subtract(local_time, (PyObject*)utc_time); - /* XXX: before relying on tzname, we should compare delta - to the offset implied by timezone/altzone */ - if (daylight && timep->tm_isdst >= 0) - zone = tzname[timep->tm_isdst % 2]; - else - zone = tzname[0]; + PyObject *local_time, *utc_time; + struct tm *utc_time_tm; + char buf[100]; + strftime(buf, sizeof(buf), "%Z", local_time_tm); + zone = buf; + local_time = new_datetime(local_time_tm->tm_year + 1900, + local_time_tm->tm_mon + 1, + local_time_tm->tm_mday, + local_time_tm->tm_hour, + local_time_tm->tm_min, + local_time_tm->tm_sec, 0, Py_None, 0); + if (local_time == NULL) { + return NULL; + } + utc_time_tm = gmtime(×tamp); + utc_time = new_datetime(utc_time_tm->tm_year + 1900, + utc_time_tm->tm_mon + 1, + utc_time_tm->tm_mday, + utc_time_tm->tm_hour, + utc_time_tm->tm_min, + utc_time_tm->tm_sec, 0, Py_None, 0); + if (utc_time == NULL) { + Py_DECREF(local_time); + return NULL; + } + delta = datetime_subtract(local_time, utc_time); Py_DECREF(local_time); + Py_DECREF(utc_time); } #endif /* HAVE_STRUCT_TM_TM_ZONE */ + if (delta == NULL) { + return NULL; + } if (zone != NULL) { nameo = PyUnicode_DecodeLocale(zone, "surrogateescape"); if (nameo == NULL) @@ -4809,12 +5061,65 @@ return result; } +static PyObject * +local_timezone(PyDateTime_DateTime *utc_time) +{ + time_t timestamp; + PyObject *delta; + PyObject *one_second; + PyObject *seconds; + + delta = datetime_subtract((PyObject *)utc_time, PyDateTime_Epoch); + if (delta == NULL) + return NULL; + one_second = new_delta(0, 1, 0, 0); + if (one_second == NULL) { + Py_DECREF(delta); + return NULL; + } + seconds = divide_timedelta_timedelta((PyDateTime_Delta *)delta, + (PyDateTime_Delta *)one_second); + Py_DECREF(one_second); + Py_DECREF(delta); + if (seconds == NULL) + return NULL; + timestamp = _PyLong_AsTime_t(seconds); + Py_DECREF(seconds); + if (timestamp == -1 && PyErr_Occurred()) + return NULL; + return local_timezone_from_timestamp(timestamp); +} + +static PY_LONG_LONG +local_to_seconds(int year, int month, int day, + int hour, int minute, int second, int fold); + +static PyObject * +local_timezone_from_local(PyDateTime_DateTime *local_dt) +{ + PY_LONG_LONG seconds; + time_t timestamp; + seconds = local_to_seconds(GET_YEAR(local_dt), + GET_MONTH(local_dt), + GET_DAY(local_dt), + DATE_GET_HOUR(local_dt), + DATE_GET_MINUTE(local_dt), + DATE_GET_SECOND(local_dt), + DATE_GET_FOLD(local_dt)); + if (seconds == -1) + return NULL; + /* XXX: add bounds check */ + timestamp = seconds - epoch; + return local_timezone_from_timestamp(timestamp); +} + static PyDateTime_DateTime * datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) { PyDateTime_DateTime *result; PyObject *offset; PyObject *temp; + PyObject *self_tzinfo; PyObject *tzinfo = Py_None; static char *keywords[] = {"tz", NULL}; @@ -4825,27 +5130,27 @@ if (check_tzinfo_subclass(tzinfo) == -1) return NULL; - if (!HASTZINFO(self) || self->tzinfo == Py_None) - goto NeedAware; + if (!HASTZINFO(self) || self->tzinfo == Py_None) { + self_tzinfo = local_timezone_from_local(self); + if (self_tzinfo == NULL) + return NULL; + } else { + self_tzinfo = self->tzinfo; + Py_INCREF(self_tzinfo); + } /* Conversion to self's own time zone is a NOP. */ - if (self->tzinfo == tzinfo) { + if (self_tzinfo == tzinfo) { + Py_DECREF(self_tzinfo); Py_INCREF(self); return self; } /* Convert self to UTC. */ - offset = datetime_utcoffset((PyObject *)self, NULL); + offset = call_utcoffset(self_tzinfo, (PyObject *)self); + Py_DECREF(self_tzinfo); if (offset == NULL) return NULL; - if (offset == Py_None) { - Py_DECREF(offset); - NeedAware: - PyErr_SetString(PyExc_ValueError, "astimezone() cannot be applied to " - "a naive datetime"); - return NULL; - } - /* result = self - offset */ result = (PyDateTime_DateTime *)add_datetime_timedelta(self, (PyDateTime_Delta *)offset, -1); @@ -4853,6 +5158,32 @@ if (result == NULL) return NULL; + /* Make sure result is aware and UTC. */ + if (!HASTZINFO(result)) { + temp = (PyObject *)result; + result = (PyDateTime_DateTime *) + new_datetime_ex2(GET_YEAR(result), + GET_MONTH(result), + GET_DAY(result), + DATE_GET_HOUR(result), + DATE_GET_MINUTE(result), + DATE_GET_SECOND(result), + DATE_GET_MICROSECOND(result), + PyDateTime_TimeZone_UTC, + DATE_GET_FOLD(result), + Py_TYPE(result)); + Py_DECREF(temp); + if (result == NULL) + return NULL; + } + else { + /* Result is already aware - just replace tzinfo. */ + temp = result->tzinfo; + result->tzinfo = PyDateTime_TimeZone_UTC; + Py_INCREF(result->tzinfo); + Py_DECREF(temp); + } + /* Attach new tzinfo and let fromutc() do the rest. */ temp = result->tzinfo; if (tzinfo == Py_None) { @@ -4900,6 +5231,56 @@ dstflag); } +static PY_LONG_LONG +local_to_seconds(int year, int month, int day, + int hour, int minute, int second, int fold) +{ + PY_LONG_LONG t, a, b, u1, u2, t1, t2, lt; + t = utc_to_seconds(year, month, day, hour, minute, second); + /* Our goal is to solve t = local(u) for u. */ + lt = local(t); + if (lt == -1) + return -1; + a = lt - t; + u1 = t - a; + t1 = local(u1); + if (t1 == -1) + return -1; + if (t1 == t) { + /* We found one solution, but it may not be the one we need. + * Look for an earlier solution (if `fold` is 0), or a + * later one (if `fold` is 1). */ + if (fold) + u2 = u1 + max_fold_seconds; + else + u2 = u1 - max_fold_seconds; + lt = local(u2); + if (lt == -1) + return -1; + b = lt - u2; + if (a == b) + return u1; + } + else { + b = t1 - u1; + assert(a != b); + } + u2 = t - b; + t2 = local(u2); + if (t2 == -1) + return -1; + if (t2 == t) + return u2; + if (t1 == t) + return u1; + /* We have found both offsets a and b, but neither t - a nor t - b is + * a solution. This means t is in the gap. */ + return fold?Py_MIN(u1, u2):Py_MAX(u1, u2); +} + +/* date(1970,1,1).toordinal() == 719163 */ +#define EPOCH_SECONDS (719163LL * 24 * 60 * 60) + static PyObject * datetime_timestamp(PyDateTime_DateTime *self) { @@ -4914,33 +5295,18 @@ Py_DECREF(delta); } else { - struct tm time; - time_t timestamp; - memset((void *) &time, '\0', sizeof(struct tm)); - time.tm_year = GET_YEAR(self) - 1900; - time.tm_mon = GET_MONTH(self) - 1; - time.tm_mday = GET_DAY(self); - time.tm_hour = DATE_GET_HOUR(self); - time.tm_min = DATE_GET_MINUTE(self); - time.tm_sec = DATE_GET_SECOND(self); - time.tm_wday = -1; - time.tm_isdst = -1; - timestamp = mktime(&time); - if (timestamp == (time_t)(-1) -#ifndef _AIX - /* Return value of -1 does not necessarily mean an error, - * but tm_wday cannot remain set to -1 if mktime succeeded. */ - && time.tm_wday == -1 -#else - /* on AIX, tm_wday is always sets, even on error */ -#endif - ) - { - PyErr_SetString(PyExc_OverflowError, - "timestamp out of range"); + PY_LONG_LONG seconds; + seconds = local_to_seconds(GET_YEAR(self), + GET_MONTH(self), + GET_DAY(self), + DATE_GET_HOUR(self), + DATE_GET_MINUTE(self), + DATE_GET_SECOND(self), + DATE_GET_FOLD(self)); + if (seconds == -1) return NULL; - } - result = PyFloat_FromDouble(timestamp + DATE_GET_MICROSECOND(self) / 1e6); + result = PyFloat_FromDouble(seconds - EPOCH_SECONDS + + DATE_GET_MICROSECOND(self) / 1e6); } return result; } @@ -4960,7 +5326,8 @@ DATE_GET_MINUTE(self), DATE_GET_SECOND(self), DATE_GET_MICROSECOND(self), - Py_None); + Py_None, + DATE_GET_FOLD(self)); } static PyObject * @@ -4970,7 +5337,8 @@ DATE_GET_MINUTE(self), DATE_GET_SECOND(self), DATE_GET_MICROSECOND(self), - GET_DT_TZINFO(self)); + GET_DT_TZINFO(self), + DATE_GET_FOLD(self)); } static PyObject * @@ -5022,7 +5390,7 @@ * __getstate__ isn't exposed. */ static PyObject * -datetime_getstate(PyDateTime_DateTime *self) +datetime_getstate(PyDateTime_DateTime *self, int proto) { PyObject *basestate; PyObject *result = NULL; @@ -5030,6 +5398,9 @@ basestate = PyBytes_FromStringAndSize((char *)self->data, _PyDateTime_DATETIME_DATASIZE); if (basestate != NULL) { + if (proto > 3 && DATE_GET_FOLD(self)) + /* Set the first bit of the third byte */ + PyBytes_AS_STRING(basestate)[2] |= (1 << 7); if (! HASTZINFO(self) || self->tzinfo == Py_None) result = PyTuple_Pack(1, basestate); else @@ -5040,9 +5411,13 @@ } static PyObject * -datetime_reduce(PyDateTime_DateTime *self, PyObject *arg) -{ - return Py_BuildValue("(ON)", Py_TYPE(self), datetime_getstate(self)); +datetime_reduce(PyDateTime_DateTime *self, PyObject *args) +{ + int proto = 0; + if (!PyArg_ParseTuple(args, "|i:__reduce_ex__", &proto)) + return NULL; + + return Py_BuildValue("(ON)", Py_TYPE(self), datetime_getstate(self, proto)); } static PyMethodDef datetime_methods[] = { @@ -5119,8 +5494,8 @@ {"astimezone", (PyCFunction)datetime_astimezone, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("tz -> convert to local time in new timezone tz\n")}, - {"__reduce__", (PyCFunction)datetime_reduce, METH_NOARGS, - PyDoc_STR("__reduce__() -> (cls, state)")}, + {"__reduce_ex__", (PyCFunction)datetime_reduce, METH_VARARGS, + PyDoc_STR("__reduce_ex__(proto) -> (cls, state)")}, {NULL, NULL} }; @@ -5208,7 +5583,9 @@ new_time_ex, new_delta_ex, datetime_fromtimestamp, - date_fromtimestamp + date_fromtimestamp, + new_datetime_ex2, + new_time_ex2 }; @@ -5289,12 +5666,12 @@ /* time values */ d = PyDateTime_TimeType.tp_dict; - x = new_time(0, 0, 0, 0, Py_None); + x = new_time(0, 0, 0, 0, Py_None, 0); if (x == NULL || PyDict_SetItemString(d, "min", x) < 0) return NULL; Py_DECREF(x); - x = new_time(23, 59, 59, 999999, Py_None); + x = new_time(23, 59, 59, 999999, Py_None, 0); if (x == NULL || PyDict_SetItemString(d, "max", x) < 0) return NULL; Py_DECREF(x); @@ -5307,12 +5684,12 @@ /* datetime values */ d = PyDateTime_DateTimeType.tp_dict; - x = new_datetime(1, 1, 1, 0, 0, 0, 0, Py_None); + x = new_datetime(1, 1, 1, 0, 0, 0, 0, Py_None, 0); if (x == NULL || PyDict_SetItemString(d, "min", x) < 0) return NULL; Py_DECREF(x); - x = new_datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999, Py_None); + x = new_datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999, Py_None, 0); if (x == NULL || PyDict_SetItemString(d, "max", x) < 0) return NULL; Py_DECREF(x); @@ -5354,7 +5731,7 @@ /* Epoch */ PyDateTime_Epoch = new_datetime(1970, 1, 1, 0, 0, 0, 0, - PyDateTime_TimeZone_UTC); + PyDateTime_TimeZone_UTC, 0); if (PyDateTime_Epoch == NULL) return NULL; diff --git a/Tools/tz/zdump.py b/Tools/tz/zdump.py new file mode 100644 --- /dev/null +++ b/Tools/tz/zdump.py @@ -0,0 +1,81 @@ +import sys +import os +import struct +from array import array +from collections import namedtuple +from datetime import datetime, timedelta + +ttinfo = namedtuple('ttinfo', ['tt_gmtoff', 'tt_isdst', 'tt_abbrind']) + +class TZInfo: + def __init__(self, transitions, type_indices, ttis, abbrs): + self.transitions = transitions + self.type_indices = type_indices + self.ttis = ttis + self.abbrs = abbrs + + @classmethod + def fromfile(cls, fileobj): + if fileobj.read(4).decode() != "TZif": + raise ValueError("not a zoneinfo file") + fileobj.seek(20) + header = fileobj.read(24) + tzh = (tzh_ttisgmtcnt, tzh_ttisstdcnt, tzh_leapcnt, + tzh_timecnt, tzh_typecnt, tzh_charcnt) = struct.unpack(">6l", header) + transitions = array('i') + transitions.fromfile(fileobj, tzh_timecnt) + if sys.byteorder != 'big': + transitions.byteswap() + + type_indices = array('B') + type_indices.fromfile(fileobj, tzh_timecnt) + + ttis = [] + for i in range(tzh_typecnt): + ttis.append(ttinfo._make(struct.unpack(">lbb", fileobj.read(6)))) + + abbrs = fileobj.read(tzh_charcnt) + + self = cls(transitions, type_indices, ttis, abbrs) + self.tzh = tzh + + return self + + def dump(self, stream, start=None, end=None): + for j, (trans, i) in enumerate(zip(self.transitions, self.type_indices)): + utc = datetime.utcfromtimestamp(trans) + tti = self.ttis[i] + lmt = datetime.utcfromtimestamp(trans + tti.tt_gmtoff) + abbrind = tti.tt_abbrind + abbr = self.abbrs[abbrind:self.abbrs.find(0, abbrind)].decode() + if j > 0: + prev_tti = self.ttis[self.type_indices[j - 1]] + shift = " %+g" % ((tti.tt_gmtoff - prev_tti.tt_gmtoff) / 3600) + else: + shift = '' + print("%s UTC = %s %-5s isdst=%d" % (utc, lmt, abbr, tti[1]) + shift, file=stream) + + @classmethod + def zonelist(cls, zonedir='/usr/share/zoneinfo'): + zones = [] + for root, _, files in os.walk(zonedir): + for f in files: + p = os.path.join(root, f) + with open(p, 'rb') as o: + magic = o.read(4) + if magic == b'TZif': + zones.append(p[len(zonedir) + 1:]) + return zones + +if __name__ == '__main__': + if len(sys.argv) < 2: + zones = TZInfo.zonelist() + for z in zones: + print(z) + sys.exit() + filepath = sys.argv[1] + if not filepath.startswith('/'): + filepath = os.path.join('/usr/share/zoneinfo', filepath) + with open(filepath, 'rb') as fileobj: + tzi = TZInfo.fromfile(fileobj) + tzi.dump(sys.stdout)