diff --git a/Lib/datetime.py b/Lib/datetime.py --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -7,6 +7,9 @@ import time as _time import math as _math +# Sentinel value to disallow None +_SENTINEL = object() + def _cmp(x, y): return 0 if x == y else 1 if x > y else -1 @@ -14,6 +17,12 @@ MAXYEAR = 9999 _MAXORDINAL = 3652059 # date.max.toordinal() +# Nine decimal digits is easy to communicate, and leaves enough room +# so that two delta days can be added w/o fear of overflowing a signed +# 32-bit int, and with plenty of room left over to absorb any possible +# carries from adding seconds. +_MAX_DELTA_DAYS = 999999999 + # Utility functions, adapted from Python's Demo/classes/Dates.py, which # also assumes the current Gregorian calendar indefinitely extended in # both directions. Difference: Dates.py calls January 1 of year 0 day @@ -316,6 +325,24 @@ return q +def _accum(tag, sofar, num, factor, leftover): + if isinstance(num, int): + prod = num * factor + result = sofar + prod + return result, leftover + if isinstance(num, float): + fracpart, intpart = _math.modf(num) + prod = int(intpart) * factor + result = sofar + prod + if fracpart == 0.0: + return result, leftover + assert isinstance(factor, int) + fracpart, intpart = _math.modf(factor * fracpart) + result += int(intpart) + return result, leftover + fracpart + raise TypeError("unsupported type for timedelta %s component: %s" % + (tag, type(num).__name__)) + class timedelta: """Represent the difference between two datetime objects. @@ -336,104 +363,52 @@ """ __slots__ = '_days', '_seconds', '_microseconds', '_hashcode' - def __new__(cls, days=0, seconds=0, microseconds=0, - milliseconds=0, minutes=0, hours=0, weeks=0): - # Doing this efficiently and accurately in C is going to be difficult - # and error-prone, due to ubiquitous overflow possibilities, and that - # C double doesn't have enough bits of precision to represent - # microseconds over 10K years faithfully. The code here tries to make - # explicit where go-fast assumptions can be relied on, in order to - # guide the C implementation; it's way more convoluted than speed- - # ignoring auto-overflow-to-long idiomatic Python could be. + def __new__(cls, days=_SENTINEL, seconds=_SENTINEL, microseconds=_SENTINEL, + milliseconds=_SENTINEL, minutes=_SENTINEL, hours=_SENTINEL, + weeks=_SENTINEL): + x = 0 + leftover = 0.0 + if microseconds is not _SENTINEL: + x, leftover = _accum("microseconds", x, microseconds, + 1, leftover) + if milliseconds is not _SENTINEL: + x, leftover = _accum("milliseconds", x, milliseconds, + 1000, leftover) + if seconds is not _SENTINEL: + x, leftover = _accum("seconds", x, seconds, + 10**6, leftover) + if minutes is not _SENTINEL: + x, leftover = _accum("minutes", x, minutes, + 60 * 10**6, leftover) + if hours is not _SENTINEL: + x, leftover = _accum("hours", x, hours, + 3600 * 10**6, leftover) + if days is not _SENTINEL: + x, leftover = _accum("days", x, days, + 24 * 3600 * 10**6, leftover) + if weeks is not _SENTINEL: + x, leftover = _accum("weeks", x, weeks, + 7 * 24 * 3600 * 10**6, leftover) + if leftover != 0.0: + whole = round(leftover) + if abs(whole - leftover) == 0.5: + x_is_odd = x & 1 + whole = 2 * round((leftover + x_is_odd) * 0.5) - x_is_odd + x += whole + return cls._from_microseconds(x) - # XXX Check that all inputs are ints or floats. + @classmethod + def _from_microseconds(cls, microseconds): + seconds, microseconds = divmod(microseconds, 10**6) + days, seconds = divmod(seconds, 24 * 3600) - # Final values, all integer. - # s and us fit in 32-bit signed ints; d isn't bounded. - d = s = us = 0 - - # Normalize everything to days, seconds, microseconds. - days += weeks*7 - seconds += minutes*60 + hours*3600 - microseconds += milliseconds*1000 - - # Get rid of all fractions, and normalize s and us. - # Take a deep breath . - if isinstance(days, float): - dayfrac, days = _math.modf(days) - daysecondsfrac, daysecondswhole = _math.modf(dayfrac * (24.*3600.)) - assert daysecondswhole == int(daysecondswhole) # can't overflow - s = int(daysecondswhole) - assert days == int(days) - d = int(days) - else: - daysecondsfrac = 0.0 - d = days - assert isinstance(daysecondsfrac, float) - assert abs(daysecondsfrac) <= 1.0 - assert isinstance(d, int) - assert abs(s) <= 24 * 3600 - # days isn't referenced again before redefinition - - if isinstance(seconds, float): - secondsfrac, seconds = _math.modf(seconds) - assert seconds == int(seconds) - seconds = int(seconds) - secondsfrac += daysecondsfrac - assert abs(secondsfrac) <= 2.0 - else: - secondsfrac = daysecondsfrac - # daysecondsfrac isn't referenced again - assert isinstance(secondsfrac, float) - assert abs(secondsfrac) <= 2.0 - - assert isinstance(seconds, int) - days, seconds = divmod(seconds, 24*3600) - d += days - s += int(seconds) # can't overflow - assert isinstance(s, int) - assert abs(s) <= 2 * 24 * 3600 - # seconds isn't referenced again before redefinition - - usdouble = secondsfrac * 1e6 - assert abs(usdouble) < 2.1e6 # exact value not critical - # secondsfrac isn't referenced again - - if isinstance(microseconds, float): - microseconds = round(microseconds + usdouble) - seconds, microseconds = divmod(microseconds, 1000000) - days, seconds = divmod(seconds, 24*3600) - d += days - s += seconds - else: - microseconds = int(microseconds) - seconds, microseconds = divmod(microseconds, 1000000) - days, seconds = divmod(seconds, 24*3600) - d += days - s += seconds - microseconds = round(microseconds + usdouble) - assert isinstance(s, int) - assert isinstance(microseconds, int) - assert abs(s) <= 3 * 24 * 3600 - assert abs(microseconds) < 3.1e6 - - # Just a little bit of carrying possible for microseconds and seconds. - seconds, us = divmod(microseconds, 1000000) - s += seconds - days, s = divmod(s, 24*3600) - d += days - - assert isinstance(d, int) - assert isinstance(s, int) and 0 <= s < 24*3600 - assert isinstance(us, int) and 0 <= us < 1000000 - - if abs(d) > 999999999: - raise OverflowError("timedelta # of days is too large: %d" % d) + if not (-_MAX_DELTA_DAYS <= days <= _MAX_DELTA_DAYS): + raise OverflowError("timedelta # of days is too large: %d" % days) self = object.__new__(cls) - self._days = d - self._seconds = s - self._microseconds = us + self._days = days + self._seconds = seconds + self._microseconds = microseconds self._hashcode = -1 return self @@ -467,8 +442,7 @@ def total_seconds(self): """Total seconds in the duration.""" - return ((self.days * 86400 + self.seconds) * 10**6 + - self.microseconds) / 10**6 + return self._to_microseconds() / 10**6 # Read-only field accessors @property @@ -634,9 +608,8 @@ def __reduce__(self): return (self.__class__, self._getstate()) -timedelta.min = timedelta(-999999999) -timedelta.max = timedelta(days=999999999, hours=23, minutes=59, seconds=59, - microseconds=999999) +timedelta.min = timedelta(-_MAX_DELTA_DAYS) +timedelta.max = timedelta(_MAX_DELTA_DAYS, 24*3600-1, 1000000-1) timedelta.resolution = timedelta(microseconds=1) class date: @@ -1815,12 +1788,10 @@ class timezone(tzinfo): __slots__ = '_offset', '_name' - # Sentinel value to disallow None - _Omitted = object() - def __new__(cls, offset, name=_Omitted): + def __new__(cls, offset, name=_SENTINEL): if not isinstance(offset, timedelta): raise TypeError("offset must be a timedelta") - if name is cls._Omitted: + if name is _SENTINEL: if not offset: return cls.utc name = None @@ -2132,12 +2103,13 @@ else: # Clean up unused names del (_DAYNAMES, _DAYS_BEFORE_MONTH, _DAYS_IN_MONTH, _DI100Y, _DI400Y, - _DI4Y, _EPOCH, _MAXORDINAL, _MONTHNAMES, _build_struct_time, + _DI4Y, _EPOCH, _MAXORDINAL, _MAX_DELTA_DAYS, _MONTHNAMES, _SENTINEL, _check_date_fields, _check_int_field, _check_time_fields, _check_tzinfo_arg, _check_tzname, _check_utc_offset, _cmp, _cmperror, _date_class, _days_before_month, _days_before_year, _days_in_month, _format_time, _is_leap, _isoweek1monday, _math, _ord2ymd, - _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord) + _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord, + _build_struct_time, _accum, _divide_and_round) # XXX Since import * above excludes names that start with _, # docstring does not get overwritten. In the future, it may be # appropriate to maintain a single module level docstring and diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -53,7 +53,7 @@ self.assertEqual(datetime.MAXYEAR, 9999) def test_name_cleanup(self): - if '_Fast' not in str(self): + if '_Pure' in type(self).__name__: return datetime = datetime_module names = set(name for name in dir(datetime) @@ -64,7 +64,7 @@ self.assertEqual(names - allowed, set([])) def test_divide_and_round(self): - if '_Fast' in str(self): + if '_Fast' in type(self).__name__: return dar = datetime_module._divide_and_round @@ -416,6 +416,11 @@ eq(td(seconds=0.001), td(milliseconds=1)) eq(td(milliseconds=0.001), td(microseconds=1)) + with self.assertRaisesRegexp(TypeError, + "^unsupported type for timedelta days " + "component: NoneType$"): + td(None) + def test_computations(self): eq = self.assertEqual td = timedelta