Add timedelta and test

This commit is contained in:
FinBird 2023-06-02 00:56:36 +08:00 committed by GitHub
parent acc8ab802f
commit c5592d3742
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 321 additions and 0 deletions

233
python/datetime.py Normal file
View File

@ -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 <wink>.
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)

88
tests/70_datetime.py Normal file
View File

@ -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)