diff --git a/Lib/datetime.py b/Lib/datetime.py --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1017,10 +1017,10 @@ dst() Properties (readonly): - hour, minute, second, microsecond, tzinfo + hour, minute, second, microsecond, tzinfo, 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: @@ -1028,6 +1028,7 @@ hour, minute (required) second, microsecond (default to zero) tzinfo (default to None) + first (keyword only, default to True) """ self = object.__new__(cls) if isinstance(hour, bytes) and len(hour) == 6: @@ -1041,6 +1042,7 @@ self._second = second self._microsecond = microsecond self._tzinfo = tzinfo + self._first = first return self # Read-only field accessors @@ -1069,6 +1071,10 @@ """timezone info object""" return self._tzinfo + @property + def first(self): + return self._first + # Standard conversions, __hash__ (and helpers) # Comparisons of time objects with other. @@ -1141,7 +1147,7 @@ """Hash.""" tzoff = self.utcoffset() if not tzoff: # zero or None - return hash(self._getstate()[0]) + return hash(self.replace(first=True)._getstate()[0]) h, m = divmod(timedelta(hours=self.hour, minutes=self.minute) - tzoff, timedelta(hours=1)) assert not m % timedelta(minutes=1), "whole minute" @@ -1176,11 +1182,14 @@ s = ", %d" % self._second else: s = "" - s= "%s(%d, %d%s)" % ('datetime.' + self.__class__.__name__, + s = "%s(%d, %d%s)" % ('datetime.' + self.__class__.__name__, self._hour, self._minute, s) 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): @@ -1254,7 +1263,7 @@ 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 @@ -1266,9 +1275,11 @@ microsecond = self.microsecond if tzinfo is True: tzinfo = self.tzinfo + if first is None: + first = self._first _check_time_fields(hour, minute, second, microsecond) _check_tzinfo_arg(tzinfo) - return time(hour, minute, second, microsecond, tzinfo) + return time(hour, minute, second, microsecond, tzinfo, first=first) def __bool__(self): if self.second or self.microsecond: @@ -1281,7 +1292,10 @@ 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,) @@ -1291,8 +1305,14 @@ def __setstate(self, string, tzinfo): if len(string) != 6 or string[0] >= 24: raise TypeError("an integer is required") - (self._hour, self._minute, self._second, + (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 if tzinfo is None or isinstance(tzinfo, _tzinfo_class): self._tzinfo = tzinfo @@ -1317,9 +1337,9 @@ __slots__ = date.__slots__ + ( '_hour', '_minute', '_second', - '_microsecond', '_tzinfo') + '_microsecond', '_tzinfo', '_first') 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: # Pickle support self = date.__new__(cls, year[:4]) @@ -1333,6 +1353,7 @@ self._second = second self._microsecond = microsecond self._tzinfo = tzinfo + self._first = first return self # Read-only field accessors @@ -1361,6 +1382,10 @@ """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()). @@ -1385,7 +1410,10 @@ 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 @@ -1432,7 +1460,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, first=time.first) def timetuple(self): "Return local time tuple compatible with time.localtime()." @@ -1450,9 +1478,18 @@ 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() @@ -1471,15 +1508,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, 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 @@ -1497,11 +1535,13 @@ microsecond = self.microsecond if tzinfo is True: tzinfo = self.tzinfo + if first is None: + first = self.first _check_date_fields(year, month, day) _check_time_fields(hour, minute, second, microsecond) _check_tzinfo_arg(tzinfo) return datetime(year, month, day, hour, minute, second, - microsecond, tzinfo) + microsecond, tzinfo, first=first) def astimezone(self, tz=None): if tz is None: @@ -1601,6 +1641,9 @@ 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): @@ -1777,7 +1820,7 @@ def __hash__(self): tzoff = self.utcoffset() if tzoff is None: - return hash(self._getstate()[0]) + return hash(self.replace(first=True)._getstate()[0]) days = _ymd2ord(self.year, self.month, self.day) seconds = self.hour * 3600 + self.minute * 60 + self.second return hash(timedelta(days, seconds, self.microsecond) - tzoff) @@ -1788,8 +1831,11 @@ 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,) @@ -1798,7 +1844,13 @@ def __setstate(self, string, tzinfo): (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 if tzinfo is None or isinstance(tzinfo, _tzinfo_class): @@ -2137,7 +2189,10 @@ # 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 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3814,6 +3814,76 @@ self.assertEqual(as_datetime, datetime_sc) self.assertEqual(datetime_sc, as_datetime) +############################################################################# +# 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)') + def test_main(): support.run_unittest(__name__)