diff --git a/python/datetime.py b/python/datetime.py new file mode 100644 index 00000000..73465a60 --- /dev/null +++ b/python/datetime.py @@ -0,0 +1,233 @@ +import math + + +def float_as_integer_ratio(x): + if x == 0: + return (0, 1) + + sign = 1 if x > 0 else -1 + x = abs(x) + + p0, q0 = 0, 1 + p1, q1 = 1, 0 + while True: + n = int(x) + p2 = n * p1 + p0 + q2 = n * q1 + q0 + if x == n: + break + x = 1 / (x - n) + p0, q0 = p1, q1 + p1, q1 = p2, q2 + gcd_value = math.gcd(p2, q2) + return (sign * p2 // gcd_value, q2 // gcd_value) + + +def _divide_and_round(a, b): + """divide a by b and round result to the nearest integer + + When the ratio is exactly half-way between two integers, + the even integer is returned. + """ + # Based on the reference implementation for divmod_near + # in Objects/longobject.c. + q, r = divmod(a, b) + # round up if either r / b > 0.5, or r / b == 0.5 and q is odd. + # The expression r / b > 0.5 is equivalent to 2 * r > b if b is + # positive, 2 * r < b if b negative. + r *= 2 + greater_than_half = r > b if b > 0 else r < b + if greater_than_half or r == b and q % 2 == 1: + q += 1 + + return q + + +class timedelta: + def __init__(self, days=0, seconds=0, minutes=0, hours=0, weeks=0): + d = 0 + s = 0 + + days += weeks * 7 + seconds += minutes * 60 + hours * 3600 + # 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 * float(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 + if round(secondsfrac) == 1.0: + s += 1 + assert isinstance(seconds, int) + days, seconds = divmod(seconds, 24 * 3600) + + d += days + s += int(seconds) # can't overflow + + while s > 0 and d < 0: + s -= 86400 + d += 1 + + while s < 0 and d > 0: + s += 86400 + d -= 1 + + assert isinstance(s, int) + assert abs(s) <= 2 * 24 * 3600 + + # seconds isn't referenced again before redefinition + + if abs(d) > 999999999: + raise OverflowError # "timedelta # of days is too large: {d}" + self.days = d + self.second = s + + def __repr__(self) -> str: + args = [] + if self.days: + args.append(f'days={self.days:1d}') + if self.second: + args.append(f'seconds={self.second:1d}') + if not args: + args.append('0') + args = ", ".join(map(str, args)) + return f'datetime.timedelta({args})' + + def __cmp__(self, other) -> int: + assert isinstance(other, timedelta) + return self._cmp(self._getstate(), other._getstate()) + + def __eq__(self, other: 'timedelta') -> bool: + if isinstance(other, timedelta): + return self.__cmp__(other) == 0 + raise NotImplemented + + def __lt__(self, other: 'timedelta') -> bool: + if isinstance(other, timedelta): + return self.__cmp__(other) < 0 + raise NotImplemented + + def __le__(self, other: 'timedelta') -> bool: + if isinstance(other, timedelta): + return self.__cmp__(other) <= 0 + raise NotImplemented + + def __gt__(self, other: 'timedelta') -> bool: + if isinstance(other, timedelta): + return self.__cmp__(other) > 0 + raise NotImplemented + + def __ge__(self, other: 'timedelta') -> bool: + if isinstance(other, timedelta): + return self.__cmp__(other) >= 0 + raise NotImplemented + + def __radd__(self, other: 'timedelta') -> 'timedelta': + if isinstance(other, timedelta): + return timedelta(days=self.days + other.days, seconds=self.second + other.second) + raise NotImplemented + + def __add__(self, other: 'timedelta') -> 'timedelta': + if isinstance(other, timedelta): + return timedelta(days=self.days + other.days, seconds=self.second + other.second) + raise NotImplemented + + def __rsub__(self, other: 'timedelta'): + if isinstance(other, timedelta): + return -self + other + raise NotImplemented + + def __sub__(self, other: 'timedelta') -> 'timedelta': + if isinstance(other, timedelta): + return timedelta(days=self.days - other.days, seconds=self.second - other.second) + raise NotImplemented + + def __mul__(self, other: 'timedelta') -> 'timedelta': + if isinstance(other, int): + # for CPython compatibility, we cannot use + # our __class__ here, but need a real timedelta + return timedelta(self.days * other, self.second * other) + raise NotImplemented + + def __neg__(self) -> 'timedelta': + return timedelta(days=-self.days, seconds=-self.second) + + def __pos__(self): + return self + + def __abs__(self) -> 'timedelta': + if self.days < 0: + return -self + else: + return self + + def _to_seconds(self) -> int: + return self.days * (24 * 3600) + self.second + + def __floordiv__(self, other: 'timedelta') -> 'timedelta': + if not (isinstance(other, int) or isinstance(other, timedelta)): + raise NotImplemented + usec = self._to_seconds() + if isinstance(other, timedelta): + return usec // other._to_seconds() + if isinstance(other, int): + return timedelta(0, usec // other) + + def __truediv__(self, other: 'timedelta') -> 'timedelta': + if not (isinstance(other, int) or isinstance(other, float) or isinstance(other, timedelta)): + raise NotImplemented + usec = self._to_seconds() + if isinstance(other, timedelta): + return usec / other._to_seconds() + if isinstance(other, int): + return timedelta(0, _divide_and_round(usec, other)) + if isinstance(other, float): + a, b = float_as_integer_ratio(other) + return timedelta(0, _divide_and_round(b * usec, a)) + + def _getstate(self): + return (self.days, self.second) + + def _cmp(self, a, b) -> int: + if a[0] > b[0]: + return 1 + if a[0] < b[0]: + return -1 + if a[1] > b[1]: + return 1 + if a[1] < b[1]: + return -1 + return 0 + + def __hash__(self): + return hash(self._getstate()) + + +timedelta.min = timedelta(-999999999) +timedelta.max = timedelta(days=999999999, seconds=59) +timedelta.resolution = timedelta(seconds=1) diff --git a/tests/70_datetime.py b/tests/70_datetime.py new file mode 100644 index 00000000..7ab7c5c5 --- /dev/null +++ b/tests/70_datetime.py @@ -0,0 +1,88 @@ +from datetime import timedelta + +assert repr(timedelta(days=50, seconds=27)) == 'datetime.timedelta(days=50, seconds=27)' +assert repr(timedelta(days=1.0 / (60 * 60 * 24), seconds=0)) == 'datetime.timedelta(seconds=1)' +assert repr(timedelta(days=0, seconds=1.0)) == 'datetime.timedelta(seconds=1)' +assert repr(timedelta(days=1.0 / (60 * 60 * 24), seconds=0)) == 'datetime.timedelta(seconds=1)' +assert repr(timedelta(42)) == 'datetime.timedelta(days=42)' + + +def eq(a, b): + assert a == b + + +# test_constructor: +# Check keyword args to constructor +eq(timedelta(), timedelta(days=0, seconds=0, minutes=0, hours=0, weeks=0)) +eq(timedelta(1), timedelta(days=1)) +eq(timedelta(0, 1), timedelta(seconds=1)) + +# Check float args to constructor +eq(timedelta(days=1.0), timedelta(seconds=60 * 60 * 24)) +eq(timedelta(days=1.0 / 24), timedelta(hours=1)) +eq(timedelta(hours=1.0 / 60), timedelta(minutes=1)) +eq(timedelta(minutes=1.0 / 60), timedelta(seconds=1)) + +# test_hash_equality: +t1 = timedelta(days=100, seconds=-8640000) +t2 = timedelta() +eq(hash(t1), hash(t2)) + +a = timedelta(days=7) # One week +b = timedelta(0, 60) # One minute +eq(a + b, timedelta(7, 60)) +eq(a - b, timedelta(6, 24 * 3600 - 60)) +eq(b.__rsub__(a), timedelta(6, 24 * 3600 - 60)) +eq(-a, timedelta(-7)) +eq(a, timedelta(7)) +eq(timedelta(-1, 24 * 3600 - 60), -b) + +eq(timedelta(6, 24 * 3600), a) +# TODO __rmul__ +# eq(a * 10, timedelta(70)) +# eq(a * 10, 10 * a) +# eq(a * 10, 10 * a) +# eq(b * 10, timedelta(0, 600)) +# eq(10 * b, timedelta(0, 600)) +eq(b * 10, timedelta(0, 600)) +eq(a * -1, -a) +eq(b * -2, -b - b) +eq(b * (60 * 24), (b * 60) * 24) +eq(a // 7, timedelta(1)) +eq(b // 10, timedelta(0, 6)) +eq(a // 10, timedelta(0, 7 * 24 * 360)) +eq(a / 0.5, timedelta(14)) +eq(b / 0.5, timedelta(0, 120)) +eq(a / 7, timedelta(1)) +eq(b / 10, timedelta(0, 6)) +eq(a / 10, timedelta(0, 7 * 24 * 360)) + +# test_compare: +t1 = timedelta(2, 4) +t2 = timedelta(2, 4) +eq(t1, t2) + +assert t1 <= t2 +assert t1 >= t2 +assert not t1 < t2 +assert not t1 > t2 + +# test_repr: +eq(repr(timedelta(1)), "datetime.timedelta(days=1)") +eq(repr(timedelta(10, 2)), "datetime.timedelta(days=10, seconds=2)") +eq(repr(timedelta(seconds=60)), "datetime.timedelta(seconds=60)") +eq(repr(timedelta()), "datetime.timedelta(0)") + +# test_resolution_info: +assert isinstance(timedelta.min, timedelta) +assert isinstance(timedelta.max, timedelta) +assert isinstance(timedelta.resolution, timedelta) +assert timedelta.max > timedelta.min +eq(timedelta.min, timedelta(-999999999)) +eq(timedelta.max, timedelta(999999999, 59)) +eq(timedelta.resolution, timedelta(0, 1)) + +# test_bool: +assert timedelta(1) +assert timedelta(0, 1) +assert timedelta(0) \ No newline at end of file