diff --git a/Lib/datetime.py b/Lib/datetime.py index db13b12..602358a 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1022,11 +1022,11 @@ class time: dst() Properties (readonly): - hour, minute, second, microsecond, tzinfo + hour, minute, second, microsecond, tzinfo, first """ - __slots__ = '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode' + __slots__ = '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode', '_first' - 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, *, first=True): """Constructor. Arguments: @@ -1034,6 +1034,7 @@ class time: hour, minute (required) second, microsecond (default to zero) tzinfo (default to None) + first (keyword only, default to True) """ if isinstance(hour, bytes) and len(hour) == 6 and hour[0] < 24: # Pickle support @@ -1051,6 +1052,7 @@ class time: self._microsecond = microsecond self._tzinfo = tzinfo self._hashcode = -1 + self._first = first return self # Read-only field accessors @@ -1079,6 +1081,10 @@ class time: """timezone info object""" return self._tzinfo + @property + def first(self): + return self._first + # Standard conversions, __hash__ (and helpers) # Comparisons of time objects with other. @@ -1146,7 +1152,7 @@ class time: if self._hashcode == -1: tzoff = self.utcoffset() if not tzoff: # zero or None - self._hashcode = hash(self._getstate()[0]) + self._hashcode = hash(self.replace(first=True)._getstate()[0]) else: h, m = divmod(timedelta(hours=self.hour, minutes=self.minute) - tzoff, timedelta(hours=1)) @@ -1190,6 +1196,9 @@ class time: if self._tzinfo is not None: assert s[-1:] == ")" s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")" + if not self._first: + assert s[-1:] == ")" + s = s[:-1] + ", first=False)" return s def isoformat(self): @@ -1265,7 +1274,7 @@ class time: return offset def replace(self, hour=None, minute=None, second=None, microsecond=None, - tzinfo=True): + tzinfo=True, *, first=None): """Return a new time with new values for the specified fields.""" if hour is None: hour = self.hour @@ -1277,14 +1286,19 @@ class time: microsecond = self.microsecond if tzinfo is True: tzinfo = self.tzinfo - return time(hour, minute, second, microsecond, tzinfo) + if first is None: + first = self._first + return time(hour, minute, second, microsecond, tzinfo, first=first) # Pickle support. def _getstate(self): us2, us3 = divmod(self._microsecond, 256) us1, us2 = divmod(us2, 256) - basestate = bytes([self._hour, self._minute, self._second, + m = self._minute + if not self._first: + m += 128 + basestate = bytes([self._hour, m, self._second, us1, us2, us3]) if self._tzinfo is None: return (basestate,) @@ -1294,7 +1308,13 @@ 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 + self._hour, m, self._second, us1, us2, us3 = string + if m > 127: + self._first = False + self._minute = m - 128 + else: + self._first = True + self._minute = m self._microsecond = (((us1 << 8) | us2) << 8) | us3 self._tzinfo = tzinfo @@ -1316,7 +1336,7 @@ 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): + microsecond=0, tzinfo=None, *, first=True): if isinstance(year, bytes) and len(year) == 10 and 1 <= year[2] <= 12: # Pickle support self = object.__new__(cls) @@ -1337,6 +1357,7 @@ class datetime(date): self._microsecond = microsecond self._tzinfo = tzinfo self._hashcode = -1 + self._first = first return self # Read-only field accessors @@ -1365,6 +1386,10 @@ class datetime(date): """timezone info object""" return self._tzinfo + @property + def first(self): + return self._first + @classmethod def fromtimestamp(cls, t, tz=None): """Construct a datetime from a POSIX timestamp (like time.time()). @@ -1388,7 +1413,10 @@ class datetime(date): y, m, d, hh, mm, ss, weekday, jday, dst = converter(t) ss = min(ss, 59) # clamp out leap seconds if the platform has them result = cls(y, m, d, hh, mm, ss, us, tz) - if tz is not None: + if tz is None: + # Detect a repeated hour + result._first = (hh != converter(t - 3600)[3]) + else: result = tz.fromutc(result) return result @@ -1430,7 +1458,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, first=time.first) def timetuple(self): "Return local time tuple compatible with time.localtime()." @@ -1448,9 +1476,18 @@ class datetime(date): def timestamp(self): "Return POSIX timestamp as float" if self._tzinfo is None: - return _time.mktime((self.year, self.month, self.day, + s = _time.mktime((self.year, self.month, self.day, self.hour, self.minute, self.second, - -1, -1, -1)) + self.microsecond / 1e6 + -1, -1, -1)) + # Detect ambiguous hour. Since we don't know what mktime + # returned, we test both sides: + if _time.localtime(s - 3600)[3] == self.hour: + if self.first: + s -= 3600 + elif _time.localtime(s + 3600)[3] == self.hour: + if not self.first: + s += 3600 + return s + self.microsecond / 1e6 else: return (self - _EPOCH).total_seconds() @@ -1469,15 +1506,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, first=self.first) def timetz(self): "Return the time part, with same tzinfo." return time(self.hour, self.minute, self.second, self.microsecond, - self._tzinfo) + self._tzinfo, first=self.first) 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, + *, first=True): """Return a new datetime with new values for the specified fields.""" if year is None: year = self.year @@ -1495,8 +1533,10 @@ class datetime(date): microsecond = self.microsecond if tzinfo is True: tzinfo = self.tzinfo - return datetime(year, month, day, hour, minute, second, microsecond, - tzinfo) + if first is None: + first = self.first + return datetime(year, month, day, hour, minute, second, + microsecond, tzinfo, first=first) def astimezone(self, tz=None): if tz is None: @@ -1596,6 +1636,9 @@ class datetime(date): if self._tzinfo is not None: assert s[-1:] == ")" s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")" + if not self._first: + assert s[-1:] == ")" + s = s[:-1] + ", first=False)" return s def __str__(self): @@ -1767,7 +1810,7 @@ class datetime(date): if self._hashcode == -1: tzoff = self.utcoffset() if tzoff is None: - self._hashcode = hash(self._getstate()[0]) + self._hashcode = hash(self.replace(first=True)._getstate()[0]) else: days = _ymd2ord(self.year, self.month, self.day) seconds = self.hour * 3600 + self.minute * 60 + self.second @@ -1780,8 +1823,11 @@ class datetime(date): yhi, ylo = divmod(self._year, 256) us2, us3 = divmod(self._microsecond, 256) us1, us2 = divmod(us2, 256) + m = self._minute + if not self._first: + m += 128 basestate = bytes([yhi, ylo, self._month, self._day, - self._hour, self._minute, self._second, + self._hour, m, self._second, us1, us2, us3]) if self._tzinfo is None: return (basestate,) @@ -1792,7 +1838,13 @@ class datetime(date): 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, - self._minute, self._second, us1, us2, us3) = string + m, self._second, us1, us2, us3) = string + if m > 127: + self._first = False + self._minute = m - 128 + else: + self._first = True + self._minute = m self._year = yhi * 256 + ylo self._microsecond = (((us1 << 8) | us2) << 8) | us3 self._tzinfo = tzinfo @@ -2129,7 +2181,10 @@ _EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) # pretty bizarre, and a tzinfo subclass can override fromutc() if it is. try: - from _datetime import * + # XXX: Temporarily disable acceleration while working on the Python + # prototype. This is accomplished by appending '_' to the module name + # below. + from _datetime_ import * except ImportError: pass else: diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index babeb44..470bc9f 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3885,5 +3885,75 @@ class Oddballs(unittest.TestCase): with self.assertRaises(TypeError): datetime(10, 10, 10, 10, 10, 10, 10.) +############################################################################# +# Local Time Disambiguation + +class TestLocalTimeDisambiguation(unittest.TestCase): + + def test_constructors(self): + t = time(0, first=False) + dt = datetime(1, 1, 1, first=False) + self.assertFalse(t.first) + self.assertFalse(dt.first) + + def test_member(self): + dt = datetime(1, 1, 1, first=False) + t = dt.time() + self.assertFalse(t.first) + t = dt.timetz() + self.assertFalse(t.first) + + def test_replace(self): + t = time(0) + dt = datetime(1, 1, 1) + self.assertFalse(t.replace(first=False).first) + self.assertFalse(dt.replace(first=False).first) + self.assertTrue(t.replace(first=True).first) + self.assertTrue(dt.replace(first=True).first) + + def test_comparison(self): + t = time(0) + dt = datetime(1, 1, 1) + self.assertEqual(t, t.replace(first=False)) + self.assertEqual(dt, dt.replace(first=False)) + + def test_hash(self): + t = time(0) + dt = datetime(1, 1, 1) + self.assertEqual(hash(t), hash(t.replace(first=False))) + self.assertEqual(hash(dt), hash(dt.replace(first=False))) + + @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.assertTrue(dt0.first) + self.assertFalse(dt1.first) + + @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(first=False) + self.assertEqual(dt0.timestamp() + 3600, + dt1.timestamp()) + + def test_pickle(self): + t = time(first=False) + dt = datetime(1, 1, 1, first=False) + for pickler, unpickler, proto in pickle_choices: + for x in [t, dt]: + s = pickler.dumps(x) + y = unpickler.loads(s) + self.assertEqual(x, y) + self.assertEqual(x.first, y.first) + + def test_repr(self): + t = time(first=False) + dt = datetime(1, 1, 1, first=False) + self.assertEqual(repr(t), 'datetime.time(0, 0, first=False)') + self.assertEqual(repr(dt), + 'datetime.datetime(1, 1, 1, 0, 0, first=False)') + if __name__ == "__main__": unittest.main()