summaryrefslogtreecommitdiffstats
path: root/Lib/datetime.py
diff options
context:
space:
mode:
authorAlexander Belopolsky <alexander.belopolsky@gmail.com>2016-07-22 22:47:04 (GMT)
committerAlexander Belopolsky <alexander.belopolsky@gmail.com>2016-07-22 22:47:04 (GMT)
commit5d0c59838223ce46a6e2b90a7d3ed48ee1ac481e (patch)
tree896ad1e002ff1392427e25bb0b95b8ec08fb399a /Lib/datetime.py
parent638e6220557db50b01653b410eb12f11b9b8ab1c (diff)
downloadcpython-5d0c59838223ce46a6e2b90a7d3ed48ee1ac481e.zip
cpython-5d0c59838223ce46a6e2b90a7d3ed48ee1ac481e.tar.gz
cpython-5d0c59838223ce46a6e2b90a7d3ed48ee1ac481e.tar.bz2
Closes issue #24773: Implement PEP 495 (Local Time Disambiguation).
Diffstat (limited to 'Lib/datetime.py')
-rw-r--r--Lib/datetime.py242
1 files changed, 172 insertions, 70 deletions
diff --git a/Lib/datetime.py b/Lib/datetime.py
index b1321a3..19d2f67 100644
--- a/Lib/datetime.py
+++ b/Lib/datetime.py
@@ -250,9 +250,9 @@ def _check_utc_offset(name, offset):
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 @@ class date:
# 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 @@ class date:
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.min = date(1, 1, 1)
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 @@ class time:
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 @@ class time:
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 @@ class time:
self._microsecond = microsecond
self._tzinfo = tzinfo
self._hashcode = -1
+ self._fold = fold
return self
# Read-only field accessors
@@ -1095,6 +1098,10 @@ class time:
"""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 @@ class time:
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 @@ class time:
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 @@ class time:
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 @@ class time:
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 @@ class time:
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 @@ class time:
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 @@ class datetime(date):
__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 @@ class datetime(date):
self._microsecond = microsecond
self._tzinfo = tzinfo
self._hashcode = -1
+ self._fold = fold
return self
# Read-only field accessors
@@ -1384,6 +1411,10 @@ class datetime(date):
"""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 @@ class datetime(date):
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 @@ class datetime(date):
"""
_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 @@ class datetime(date):
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 @@ class datetime(date):
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 @@ class datetime(date):
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 @@ class datetime(date):
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 @@ class datetime(date):
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 @@ class datetime(date):
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 @@ class datetime(date):
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 @@ class datetime(date):
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 @@ class datetime(date):
# 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 @@ class datetime(date):
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)