diff --git a/neo4j/meta.py b/neo4j/meta.py index d9b0ae872..127caf702 100644 --- a/neo4j/meta.py +++ b/neo4j/meta.py @@ -34,6 +34,11 @@ def get_user_agent(): return template.format(*fields) +def deprecation_warn(message): + from warnings import warn + warn(message, category=DeprecationWarning, stacklevel=2) + + def deprecated(message): """ Decorator for deprecating functions and methods. @@ -46,8 +51,7 @@ def foo(x): """ def f__(f): def f_(*args, **kwargs): - from warnings import warn - warn(message, category=DeprecationWarning, stacklevel=2) + deprecation_warn(message) return f(*args, **kwargs) f_.__name__ = f.__name__ f_.__doc__ = f.__doc__ diff --git a/neo4j/time/__init__.py b/neo4j/time/__init__.py index 54460f312..11ed4586e 100644 --- a/neo4j/time/__init__.py +++ b/neo4j/time/__init__.py @@ -23,12 +23,20 @@ a number of utility functions. """ +from contextlib import contextmanager from datetime import ( timedelta, date, time, datetime, ) +from decimal import ( + Decimal, + localcontext, + ROUND_DOWN, + ROUND_HALF_EVEN, + ROUND_HALF_UP, +) from functools import total_ordering from re import compile as re_compile from time import ( @@ -36,15 +44,14 @@ mktime, struct_time, ) -from decimal import Decimal +from neo4j.meta import ( + deprecated, + deprecation_warn +) from neo4j.time.arithmetic import ( nano_add, - nano_sub, - nano_mul, nano_div, - nano_mod, - nano_divmod, symmetric_divmod, round_half_to_even, ) @@ -54,6 +61,25 @@ DateTimeType, ) + +@contextmanager +def _decimal_context(prec=9, rounding=ROUND_HALF_EVEN): + with localcontext() as ctx: + ctx.prec = prec + ctx.rounding = rounding + yield ctx + + +def _decimal_context_decorator(prec=9): + def outer(fn): + def inner(*args, **kwargs): + with _decimal_context(prec=prec): + return fn(*args, **kwargs) + + return inner + return outer + + MIN_INT64 = -(2 ** 63) MAX_INT64 = (2 ** 63) - 1 @@ -64,10 +90,14 @@ """The largest year number allowed in a :class:`neo4j.time.Date` or :class:`neo4j.time.DateTime` object to be compatible with :class:`python:datetime.date` and :class:`python:datetime.datetime`.""" DATE_ISO_PATTERN = re_compile(r"^(\d{4})-(\d{2})-(\d{2})$") -TIME_ISO_PATTERN = re_compile(r"^(\d{2})(:(\d{2})(:((\d{2})" - r"(\.\d*)?))?)?(([+-])(\d{2}):(\d{2})(:((\d{2})(\.\d*)?))?)?$") -DURATION_ISO_PATTERN = re_compile(r"^P((\d+)Y)?((\d+)M)?((\d+)D)?" - r"(T((\d+)H)?((\d+)M)?((\d+(\.\d+)?)?S)?)?$") +TIME_ISO_PATTERN = re_compile( + r"^(\d{2})(:(\d{2})(:((\d{2})" + r"(\.\d*)?))?)?(([+-])(\d{2}):(\d{2})(:((\d{2})(\.\d*)?))?)?$" +) +DURATION_ISO_PATTERN = re_compile( + r"^P((\d+)Y)?((\d+)M)?((\d+)D)?" + r"(T((\d+)H)?((\d+)M)?(((\d+)(\.\d+)?)?S)?)?$" +) NANO_SECONDS = 1000000000 @@ -173,7 +203,9 @@ class ClockTime(tuple): """ def __new__(cls, seconds=0, nanoseconds=0): - seconds, nanoseconds = nano_divmod(int(1000000000 * seconds) + int(nanoseconds), 1000000000) + seconds, nanoseconds = divmod( + int(NANO_SECONDS * seconds) + int(nanoseconds), NANO_SECONDS + ) return tuple.__new__(cls, (seconds, nanoseconds)) def __add__(self, other): @@ -185,7 +217,7 @@ def __add__(self, other): if other.months or other.days: raise ValueError("Cannot add Duration with months or days") return ClockTime(self.seconds + other.seconds, self.nanoseconds + - int(other.subseconds * 1000000000)) + int(other.nanoseconds)) return NotImplemented def __sub__(self, other): @@ -196,7 +228,7 @@ def __sub__(self, other): if isinstance(other, Duration): if other.months or other.days: raise ValueError("Cannot subtract Duration with months or days") - return ClockTime(self.seconds - other.seconds, self.nanoseconds - int(other.subseconds * 1000000000)) + return ClockTime(self.seconds - other.seconds, self.nanoseconds - int(other.nanoseconds)) return NotImplemented def __repr__(self): @@ -297,25 +329,34 @@ class Duration(tuple): min = None max = None - def __new__(cls, years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, - subseconds=0, milliseconds=0, microseconds=0, nanoseconds=0): + def __new__(cls, years=0, months=0, weeks=0, days=0, hours=0, minutes=0, + seconds=0, subseconds=0, milliseconds=0, microseconds=0, + nanoseconds=0): + + if subseconds: + deprecation_warn("`subseconds` will be removed in 5.0. " + "Use `nanoseconds` instead.") + with _decimal_context(prec=9, rounding=ROUND_HALF_EVEN): + nanoseconds = int(Decimal(subseconds) * NANO_SECONDS) + mo = int(12 * years + months) if mo < MIN_INT64 or mo > MAX_INT64: raise ValueError("Months value out of range") d = int(7 * weeks + days) + ns = (int(3600000000000 * hours) + + int(60000000000 * minutes) + + int(1000000000 * seconds) + + int(1000000 * milliseconds) + + int(1000 * microseconds) + + int(nanoseconds)) + s, ns = symmetric_divmod(ns, NANO_SECONDS) if d < MIN_INT64 or d > MAX_INT64: raise ValueError("Days value out of range") - s = (int(3600000000000 * hours) + - int(60000000000 * minutes) + - int(1000000000 * seconds) + - int(1000000000 * subseconds) + - int(1000000 * milliseconds) + - int(1000 * microseconds) + - int(nanoseconds)) - s, ss = symmetric_divmod(s, 1000000000) if s < MIN_INT64 or s > MAX_INT64: raise ValueError("Seconds value out of range") - return tuple.__new__(cls, (mo, d, s, ss / 1000000000)) + if s < MIN_INT64 or s > MAX_INT64: + raise ValueError("Seconds value out of range") + return tuple.__new__(cls, (mo, d, s, ns)) def __bool__(self): return any(map(bool, self)) @@ -324,52 +365,94 @@ def __bool__(self): def __add__(self, other): if isinstance(other, Duration): - return Duration(months=self[0] + int(other[0]), days=self[1] + int(other[1]), - seconds=self[2] + int(other[2]), subseconds=nano_add(self[3], other[3])) + return Duration( + months=self[0] + int(other.months), + days=self[1] + int(other.days), + seconds=self[2] + int(other.seconds), + nanoseconds=self[3] + int(other.nanoseconds) + ) if isinstance(other, timedelta): - return Duration(months=self[0], days=self[1] + int(other.days), - seconds=self[2] + int(other.seconds), - subseconds=nano_add(self[3], other.microseconds / 1000000)) + return Duration( + months=self[0], days=self[1] + other.days, + seconds=self[2] + other.seconds, + nanoseconds=self[3] + other.microseconds * 1000 + ) return NotImplemented def __sub__(self, other): if isinstance(other, Duration): - return Duration(months=self[0] - int(other[0]), days=self[1] - int(other[1]), - seconds=self[2] - int(other[2]), subseconds=nano_sub(self[3], other[3])) + return Duration( + months=self[0] - int(other.months), + days=self[1] - int(other.days), + seconds=self[2] - int(other.seconds), + nanoseconds=self[3] - int(other.nanoseconds) + ) if isinstance(other, timedelta): - return Duration(months=self[0], days=self[1] - int(other.days), - seconds=self[2] - int(other.seconds), - subseconds=nano_sub(self[3], other.microseconds / 1000000)) + return Duration( + months=self[0], + days=self[1] - other.days, + seconds=self[2] - other.seconds, + nanoseconds=self[3] - other.microseconds * 1000 + ) return NotImplemented def __mul__(self, other): + if isinstance(other, float): + deprecation_warn("Multiplication with float will be deprecated in " + "5.0.") if isinstance(other, (int, float)): - return Duration(months=self[0] * other, days=self[1] * other, - seconds=self[2] * other, subseconds=nano_mul(self[3], other)) + return Duration( + months=self[0] * other, days=self[1] * other, + seconds=self[2] * other, nanoseconds=self[3] * other + ) return NotImplemented + @deprecated("Will be removed in 5.0.") def __floordiv__(self, other): if isinstance(other, int): - return Duration(months=int(self[0] // other), days=int(self[1] // other), - seconds=int(nano_add(self[2], self[3]) // other), subseconds=0) + # TODO 5.0: new method (floor months, days, nanoseconds) or remove + # return Duration( + # months=self[0] // other, days=self[1] // other, + # nanoseconds=(self[2] * NANO_SECONDS + self[3]) // other + # ) + seconds = self[2] + Decimal(self[3]) / NANO_SECONDS + return Duration(months=int(self[0] // other), + days=int(self[1] // other), + seconds=int(seconds // other)) return NotImplemented + @deprecated("Will be removed in 5.0.") def __mod__(self, other): if isinstance(other, int): - seconds, subseconds = symmetric_divmod(nano_add(self[2], self[3]) % other, 1) - return Duration(months=round_half_to_even(self[0] % other), days=round_half_to_even(self[1] % other), + # TODO 5.0: new method (mod months, days, nanoseconds) or remove + # return Duration( + # months=self[0] % other, days=self[1] % other, + # nanoseconds=(self[2] * NANO_SECONDS + self[3]) % other + # ) + seconds = self[2] + Decimal(self[3]) / NANO_SECONDS + seconds, subseconds = symmetric_divmod(seconds % other, 1) + return Duration(months=round_half_to_even(self[0] % other), + days=round_half_to_even(self[1] % other), seconds=seconds, subseconds=subseconds) return NotImplemented + @deprecated("Will be removed in 5.0.") def __divmod__(self, other): if isinstance(other, int): return self.__floordiv__(other), self.__mod__(other) return NotImplemented + @deprecated("Will be removed in 5.0.") def __truediv__(self, other): if isinstance(other, (int, float)): - return Duration(months=round_half_to_even(float(self[0]) / other), days=round_half_to_even(float(self[1]) / other), - seconds=float(self[2]) / other, subseconds=nano_div(self[3], other)) + return Duration( + months=round_half_to_even(self[0] / other), + days=round_half_to_even(self[1] / other), + nanoseconds=round_half_to_even( + self[2] * NANO_SECONDS / other + + self[3] / other + ) + ) return NotImplemented __div__ = __truediv__ @@ -378,34 +461,41 @@ def __pos__(self): return self def __neg__(self): - return Duration(months=-self[0], days=-self[1], seconds=-self[2], subseconds=-self[3]) + return Duration(months=-self[0], days=-self[1], seconds=-self[2], + nanoseconds=-self[3]) def __abs__(self): - return Duration(months=abs(self[0]), days=abs(self[1]), seconds=abs(self[2]), subseconds=abs(self[3])) + return Duration(months=abs(self[0]), days=abs(self[1]), + seconds=abs(self[2]), nanoseconds=abs(self[3])) def __repr__(self): - return "Duration(months=%r, days=%r, seconds=%r, subseconds=%r)" % self + return "Duration(months=%r, days=%r, seconds=%r, nanoseconds=%r)" % self def __str__(self): return self.iso_format() def __copy__(self): - return self.__new(self.ticks, self.hour, self.minute, self.second, self.tzinfo) + return self.__new__(self.__class__, months=self[0], days=self[1], + seconds=self[2], nanoseconds=self[3]) def __deepcopy__(self, memodict={}): return self.__copy__() @classmethod def from_iso_format(cls, s): - m = DURATION_ISO_PATTERN.match(s) - if m: + match = DURATION_ISO_PATTERN.match(s) + if match: + ns = 0 + if match.group(15): + ns = int(match.group(15)[1:10].ljust(9, "0")) return cls( - years=int(m.group(2) or 0), - months=int(m.group(4) or 0), - days=int(m.group(6) or 0), - hours=int(m.group(9) or 0), - minutes=int(m.group(11) or 0), - seconds=float(m.group(13) or 0.0), + years=int(match.group(2) or 0), + months=int(match.group(4) or 0), + days=int(match.group(6) or 0), + hours=int(match.group(9) or 0), + minutes=int(match.group(11) or 0), + seconds=int(match.group(14) or 0), + nanoseconds=ns ) raise ValueError("Duration string must be in ISO format") @@ -419,16 +509,26 @@ def iso_format(self, sep="T"): :rtype: str """ parts = [] - hours, minutes, seconds = self.hours_minutes_seconds + hours, minutes, seconds, nanoseconds = \ + self.hours_minutes_seconds_nanoseconds if hours: parts.append("%dH" % hours) if minutes: parts.append("%dM" % minutes) - if seconds: - if seconds == seconds // 1: - parts.append("%dS" % seconds) + if nanoseconds: + if seconds >= 0 and nanoseconds >= 0: + parts.append("%d.%sS" % + (seconds, + str(nanoseconds).rjust(9, "0").rstrip("0"))) + elif seconds <= 0 and nanoseconds <= 0: + parts.append("-%d.%sS" % + (abs(seconds), + str(abs(nanoseconds)).rjust(9, "0").rstrip("0"))) + else: - parts.append("%rS" % seconds) + assert False and "Please report this issue" + elif seconds: + parts.append("%dS" % seconds) if parts: parts.insert(0, sep) years, months, days = self.years_months_days @@ -469,9 +569,21 @@ def seconds(self): return self[2] @property + @deprecated("Will be removed in 5.0. Use `nanoseconds` instead.") def subseconds(self): """ + :return: + """ + if self[3] < 0: + return Decimal(("-0.%09i" % -self[3])[:11]) + else: + return Decimal(("0.%09i" % self[3])[:11]) + + @property + def nanoseconds(self): + """ + :return: """ return self[3] @@ -486,16 +598,29 @@ def years_months_days(self): return years, months, self[1] @property + @deprecated("Will be removed in 5.0. " + "Use `hours_minutes_seconds_nanoseconds` instead.") def hours_minutes_seconds(self): """ A 3-tuple of (hours, minutes, seconds). """ minutes, seconds = symmetric_divmod(self[2], 60) hours, minutes = symmetric_divmod(minutes, 60) - return hours, minutes, float(seconds) + self[3] + with _decimal_context(prec=11): + return hours, minutes, seconds + self.subseconds + @property + def hours_minutes_seconds_nanoseconds(self): + """ A 4-tuple of (hours, minutes, seconds, nanoseconds). + """ + minutes, seconds = symmetric_divmod(self[2], 60) + hours, minutes = symmetric_divmod(minutes, 60) + return hours, minutes, seconds, self[3] -Duration.min = Duration(months=MIN_INT64, days=MIN_INT64, seconds=MIN_INT64, subseconds=-0.999999999) -Duration.max = Duration(months=MAX_INT64, days=MAX_INT64, seconds=MAX_INT64, subseconds=+0.999999999) + +Duration.min = Duration(months=MIN_INT64, days=MIN_INT64, seconds=MIN_INT64, + nanoseconds=-999999999) +Duration.max = Duration(months=MAX_INT64, days=MAX_INT64, seconds=MAX_INT64, + nanoseconds=999999999) class Date(metaclass=DateType): @@ -828,8 +953,8 @@ def add_days(d, days): d.__year, d.__month, d.__day = d0.__year, d0.__month, d0.__day if isinstance(other, Duration): - if other.seconds or other.subseconds: - raise ValueError("Cannot add a Duration with seconds or subseconds to a Date") + if other.seconds or other.nanoseconds: + raise ValueError("Cannot add a Duration with seconds or nanoseconds to a Date") if other.months == other.days == 0: return self new_date = self.replace() @@ -928,18 +1053,24 @@ class Time(metaclass=TimeType): # CONSTRUCTOR # - def __new__(cls, hour, minute, second, tzinfo=None): - hour, minute, second = cls.__normalize_second(hour, minute, second) - ticks = 3600 * hour + 60 * minute + second - return cls.__new(ticks, hour, minute, second, tzinfo) + def __new__(cls, hour=0, minute=0, second=0, nanosecond=0, tzinfo=None): + hour, minute, second, nanosecond = cls.__normalize_nanosecond( + hour, minute, second, nanosecond + ) + ticks = (3600000000000 * hour + + 60000000000 * minute + + 1000000000 * second + + nanosecond) + return cls.__new(ticks, hour, minute, second, nanosecond, tzinfo) @classmethod - def __new(cls, ticks, hour, minute, second, tzinfo): + def __new(cls, ticks, hour, minute, second, nanosecond, tzinfo): instance = object.__new__(cls) - instance.__ticks = float(ticks) + instance.__ticks = int(ticks) instance.__hour = int(hour) instance.__minute = int(minute) - instance.__second = float(second) + instance.__second = int(second) + instance.__nanosecond = int(nanosecond) instance.__tzinfo = tzinfo return instance @@ -983,9 +1114,14 @@ def from_iso_format(cls, s): if m: hour = int(m.group(1)) minute = int(m.group(3) or 0) - second = float(m.group(5) or 0.0) + second = int(m.group(6) or 0) + nanosecond = m.group(7) + if nanosecond: + nanosecond = int(nanosecond[1:10].ljust(9, "0")) + else: + nanosecond = 0 if m.group(8) is None: - return cls(hour, minute, second) + return cls(hour, minute, second, nanosecond) else: offset_multiplier = 1 if m.group(9) == "+" else -1 offset_hour = int(m.group(10)) @@ -994,52 +1130,88 @@ def from_iso_format(cls, s): # so we can ignore this part # offset_second = float(m.group(13) or 0.0) offset = 60 * offset_hour + offset_minute - return cls(hour, minute, second, tzinfo=FixedOffset(offset_multiplier * offset)) + return cls(hour, minute, second, nanosecond, + tzinfo=FixedOffset(offset_multiplier * offset)) raise ValueError("Time string is not in ISO format") @classmethod def from_ticks(cls, ticks, tz=None): if 0 <= ticks < 86400: - minute, second = nano_divmod(ticks, 60) - hour, minute = divmod(minute, 60) - return cls.__new(ticks, hour, minute, second, tz) + ticks = Decimal(ticks) * NANO_SECONDS + ticks = int(ticks.quantize(Decimal("1."), rounding=ROUND_HALF_EVEN)) + assert 0 <= ticks < 86400000000000 + return cls.from_ticks_ns(ticks, tz=tz) raise ValueError("Ticks out of range (0..86400)") + @classmethod + def from_ticks_ns(cls, ticks, tz=None): + # TODO 5.0: this will become from_ticks + if not isinstance(ticks, int): + raise TypeError("Ticks must be int") + if 0 <= ticks < 86400000000000: + second, nanosecond = divmod(ticks, NANO_SECONDS) + minute, second = divmod(second, 60) + hour, minute = divmod(minute, 60) + return cls.__new(ticks, hour, minute, second, nanosecond, tz) + raise ValueError("Ticks out of range (0..86400000000000)") + @classmethod def from_native(cls, t): """ Convert from a native Python `datetime.time` value. """ - second = (1000000 * t.second + t.microsecond) / 1000000 - return Time(t.hour, t.minute, second, t.tzinfo) + nanosecond = t.microsecond * 1000 + return Time(t.hour, t.minute, t.second, nanosecond, t.tzinfo) @classmethod def from_clock_time(cls, clock_time, epoch): """ Convert from a `.ClockTime` relative to a given epoch. + + This method, in contrast to most others of this package, assumes days of + exactly 24 hours. """ clock_time = ClockTime(*clock_time) ts = clock_time.seconds % 86400 - nanoseconds = int(1000000000 * ts + clock_time.nanoseconds) - return Time.from_ticks(epoch.time().ticks + nanoseconds / 1000000000) + nanoseconds = int(NANO_SECONDS * ts + clock_time.nanoseconds) + ticks = (epoch.time().ticks_ns + nanoseconds) % (86400 * NANO_SECONDS) + return Time.from_ticks_ns(ticks) @classmethod def __normalize_hour(cls, hour): + hour = int(hour) if 0 <= hour < 24: - return int(hour) + return hour raise ValueError("Hour out of range (0..23)") @classmethod def __normalize_minute(cls, hour, minute): hour = cls.__normalize_hour(hour) + minute = int(minute) if 0 <= minute < 60: - return hour, int(minute) + return hour, minute raise ValueError("Minute out of range (0..59)") @classmethod def __normalize_second(cls, hour, minute, second): hour, minute = cls.__normalize_minute(hour, minute) + second = int(second) if 0 <= second < 60: - return hour, minute, float(second) - raise ValueError("Second out of range (0..<60)") + return hour, minute, second + raise ValueError("Second out of range (0..59)") + + @classmethod + def __normalize_nanosecond(cls, hour, minute, second, nanosecond): + # TODO 5.0: remove ----------------------------------------------------- + seconds, extra_ns = divmod(second, 1) + if extra_ns: + deprecation_warn("Float support second will be removed in 5.0. " + "Use `nanosecond` instead.") + # ---------------------------------------------------------------------- + hour, minute, second = cls.__normalize_second(hour, minute, second) + nanosecond = int(nanosecond + + round_half_to_even(extra_ns * NANO_SECONDS)) + if 0 <= nanosecond < NANO_SECONDS: + return hour, minute, second, nanosecond + extra_ns + raise ValueError("Nanosecond out of range (0..%s)" % (NANO_SECONDS - 1)) # CLASS ATTRIBUTES # @@ -1059,12 +1231,22 @@ def __normalize_second(cls, hour, minute, second): __second = 0 + __nanosecond = 0 + __tzinfo = None @property def ticks(self): """ Return the total number of seconds since midnight. """ + with _decimal_context(prec=15): + return self.__ticks / NANO_SECONDS + + @property + def ticks_ns(self): + """ Return the total number of seconds since midnight. + """ + # TODO 5.0: this will replace self.ticks return self.__ticks @property @@ -1077,11 +1259,23 @@ def minute(self): @property def second(self): - return self.__second + # TODO 5.0: return plain self.__second + with _decimal_context(prec=11): + return self.__second + Decimal(("0.%09i" % self.__nanosecond)[:11]) @property + def nanosecond(self): + return self.__nanosecond + + @property + @deprecated("hour_minute_second will be removed in 5.0. " + "Use `hour_minute_second_nanoseconds` instead.") def hour_minute_second(self): - return self.__hour, self.__minute, self.__second + return self.__hour, self.__minute, self.second + + @property + def hour_minute_second_nanoseconds(self): + return self.__hour, self.__minute, self.__second, self.__nanosecond @property def tzinfo(self): @@ -1090,14 +1284,17 @@ def tzinfo(self): # OPERATIONS # def __hash__(self): - return hash(self.ticks) ^ hash(self.tzinfo) + return hash(self.__ticks) ^ hash(self.tzinfo) def __eq__(self, other): if isinstance(other, Time): - return self.ticks == other.ticks and self.tzinfo == other.tzinfo + return self.__ticks == other.__ticks and self.tzinfo == other.tzinfo if isinstance(other, time): - other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000) - return self.ticks == other_ticks and self.tzinfo == other.tzinfo + other_ticks = (3600000000000 * other.hour + + 60000000000 * other.minute + + NANO_SECONDS * other.second + + 1000 * other.microsecond) + return self.ticks_ns == other_ticks and self.tzinfo == other.tzinfo return False def __ne__(self, other): @@ -1105,48 +1302,51 @@ def __ne__(self, other): def __lt__(self, other): if isinstance(other, Time): - return self.ticks < other.ticks + return (self.tzinfo == other.tzinfo + and self.ticks_ns < other.ticks_ns) if isinstance(other, time): + if self.tzinfo != other.tzinfo: + return False other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000) - return self.ticks < other_ticks - raise TypeError("'<' not supported between instances of 'Time' and %r" % type(other).__name__) + return self.ticks_ns < other_ticks + return NotImplemented def __le__(self, other): if isinstance(other, Time): - return self.ticks <= other.ticks + return (self.tzinfo == other.tzinfo + and self.ticks_ns <= other.ticks_ns) if isinstance(other, time): + if self.tzinfo != other.tzinfo: + return False other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000) - return self.ticks <= other_ticks - raise TypeError("'<=' not supported between instances of 'Time' and %r" % type(other).__name__) + return self.ticks_ns <= other_ticks + return NotImplemented def __ge__(self, other): if isinstance(other, Time): - return self.ticks >= other.ticks + return (self.tzinfo == other.tzinfo + and self.ticks_ns >= other.ticks_ns) if isinstance(other, time): + if self.tzinfo != other.tzinfo: + return False other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000) - return self.ticks >= other_ticks - raise TypeError("'>=' not supported between instances of 'Time' and %r" % type(other).__name__) + return self.ticks_ns >= other_ticks + return NotImplemented def __gt__(self, other): if isinstance(other, Time): - return self.ticks >= other.ticks + return (self.tzinfo == other.tzinfo + and self.ticks_ns >= other.ticks_ns) if isinstance(other, time): + if self.tzinfo != other.tzinfo: + return False other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000) - return self.ticks >= other_ticks - raise TypeError("'>' not supported between instances of 'Time' and %r" % type(other).__name__) - - def __add__(self, other): - if isinstance(other, Duration): - return NotImplemented - if isinstance(other, timedelta): - return NotImplemented - return NotImplemented - - def __sub__(self, other): + return self.ticks_ns >= other_ticks return NotImplemented def __copy__(self): - return self.__new(self.__ticks, self.__hour, self.__minute, self.__second, self.__tzinfo) + return self.__new(self.__ticks, self.__hour, self.__minute, + self.__second, self.__nanosecond, self.__tzinfo) def __deepcopy__(self, *args, **kwargs): return self.__copy__() @@ -1157,10 +1357,11 @@ def replace(self, **kwargs): """ Return a :class:`.Time` with one or more components replaced with new values. """ - return Time(kwargs.get("hour", self.__hour), - kwargs.get("minute", self.__minute), - kwargs.get("second", self.__second), - kwargs.get("tzinfo", self.__tzinfo)) + return Time(hour=kwargs.get("hour", self.__hour), + minute=kwargs.get("minute", self.__minute), + second=kwargs.get("second", self.__second), + nanosecond=kwargs.get("nanosecond", self.__nanosecond), + tzinfo=kwargs.get("tzinfo", self.__tzinfo)) def utc_offset(self): if self.tzinfo is None: @@ -1197,21 +1398,19 @@ def tzname(self): return self.tzinfo.tzname(self) def to_clock_time(self): - seconds, nanoseconds = nano_divmod(self.ticks, 1) # int, float - nanoseconds_int = int(Decimal(str(nanoseconds)) * NANO_SECONDS) # Convert fractions to an integer without losing precision - return ClockTime(seconds, nanoseconds_int) + seconds, nanoseconds = divmod(self.ticks_ns, NANO_SECONDS) + return ClockTime(seconds, nanoseconds) def to_native(self): """ Convert to a native Python `datetime.time` value. """ - h, m, s = self.hour_minute_second - s, ns = nano_divmod(s, 1) - ms = int(nano_mul(ns, 1000000)) + h, m, s, ns = self.hour_minute_second_nanoseconds + µs = round_half_to_even(ns / 1000) tz = self.tzinfo - return time(h, m, s, ms, tz) + return time(h, m, s, µs, tz) def iso_format(self): - s = "%02d:%02d:%012.9f" % self.hour_minute_second + s = "%02d:%02d:%02d.%09d" % self.hour_minute_second_nanoseconds if self.tzinfo is not None: offset = self.tzinfo.utcoffset(self) s += "%+03d:%02d" % divmod(offset.total_seconds() // 60, 60) @@ -1219,9 +1418,11 @@ def iso_format(self): def __repr__(self): if self.tzinfo is None: - return "neo4j.time.Time(%r, %r, %r)" % self.hour_minute_second + return "neo4j.time.Time(%r, %r, %r, %r)" % \ + self.hour_minute_second_nanoseconds else: - return "neo4j.time.Time(%r, %r, %r, tzinfo=%r)" % (self.hour_minute_second + (self.tzinfo,)) + return "neo4j.time.Time(%r, %r, %r, %r, tzinfo=%r)" % \ + (self.hour_minute_second_nanoseconds + (self.tzinfo,)) def __str__(self): return self.iso_format() @@ -1230,11 +1431,11 @@ def __format__(self, format_spec): raise NotImplementedError() -Time.min = Time(0, 0, 0) -Time.max = Time(23, 59, 59.999999999) +Time.min = Time(hour=0, minute=0, second=0, nanosecond=0) +Time.max = Time(hour=23, minute=59, second=59, nanosecond=999999999) Midnight = Time.min -Midday = Time(12, 0, 0) +Midday = Time(hour=12) @total_ordering @@ -1249,8 +1450,8 @@ class DateTime(metaclass=DateTimeType): sub-second values to be passed, with up to nine decimal places of precision held by the object within the `second` attribute. - >>> dt = DateTime(2018, 4, 30, 12, 34, 56.789123456); dt - neo4j.time.DateTime(2018, 4, 30, 12, 34, 56.789123456) + >>> dt = DateTime(2018, 4, 30, 12, 34, 56, 789123456); dt + neo4j.time.DateTime(2018, 4, 30, 12, 34, 56, 789123456) >>> dt.second 56.789123456 @@ -1258,8 +1459,10 @@ class DateTime(metaclass=DateTimeType): # CONSTRUCTOR # - def __new__(cls, year, month, day, hour=0, minute=0, second=0.0, tzinfo=None): - return cls.combine(Date(year, month, day), Time(hour, minute, second, tzinfo)) + def __new__(cls, year, month, day, hour=0, minute=0, second=0, nanosecond=0, + tzinfo=None): + return cls.combine(Date(year, month, day), + Time(hour, minute, second, nanosecond, tzinfo)) def __getattr__(self, name): """ Map standard library attribute names to local attribute names, @@ -1360,10 +1563,12 @@ def from_clock_time(cls, clock_time, epoch): except (TypeError, ValueError): raise ValueError("Clock time must be a 2-tuple of (s, ns)") else: - ordinal, ticks = divmod(seconds, 86400) + ordinal, seconds = divmod(seconds, 86400) + ticks = epoch.time().ticks_ns + seconds * NANO_SECONDS + nanoseconds + days, ticks = divmod(ticks, 86400 * NANO_SECONDS) + ordinal += days date_ = Date.from_ordinal(ordinal + epoch.date().to_ordinal()) - nanoseconds = int(1000000000 * ticks + nanoseconds) - time_ = Time.from_ticks(epoch.time().ticks + (nanoseconds / 1000000000)) + time_ = Time.from_ticks_ns(ticks) return cls.combine(date_, time_) # CLASS ATTRIBUTES # @@ -1412,6 +1617,10 @@ def minute(self): def second(self): return self.__time.second + @property + def nanosecond(self): + return self.__time.nanosecond + @property def tzinfo(self): return self.__time.tzinfo @@ -1420,6 +1629,10 @@ def tzinfo(self): def hour_minute_second(self): return self.__time.hour_minute_second + @property + def hour_minute_second_nanoseconds(self): + return self.__time.hour_minute_second_nanoseconds + # OPERATIONS # def __hash__(self): @@ -1470,7 +1683,9 @@ def __add__(self, other): t = self.to_clock_time() + ClockTime(86400 * other.days + other.seconds, other.microseconds * 1000) days, seconds = symmetric_divmod(t.seconds, 86400) date_ = Date.from_ordinal(days + 1) - time_ = Time.from_ticks(seconds + (t.nanoseconds / 1000000000)) + time_ = Time.from_ticks_ns(round_half_to_even( + seconds * NANO_SECONDS + t.nanoseconds + )) return self.combine(date_, time_) return NotImplemented @@ -1560,17 +1775,15 @@ def to_clock_time(self): for month in range(1, self.month): total_seconds += 86400 * Date.days_in_month(self.year, month) total_seconds += 86400 * (self.day - 1) - seconds, nanoseconds = nano_divmod(self.__time.ticks, 1) # int, float - nanoseconds_int = int(Decimal(str(nanoseconds)) * NANO_SECONDS) # Convert fractions to an integer without losing precision - return ClockTime(total_seconds + seconds, nanoseconds_int) + seconds, nanoseconds = divmod(self.__time.ticks_ns, NANO_SECONDS) + return ClockTime(total_seconds + seconds, nanoseconds) def to_native(self): """ Convert to a native Python `datetime.datetime` value. """ y, mo, d = self.year_month_day - h, m, s = self.hour_minute_second - s, ns = nano_divmod(s, 1) - ms = int(nano_mul(ns, 1000000)) + h, m, s, ns = self.hour_minute_second_nanoseconds + ms = int(ns / 1000) tz = self.tzinfo return datetime(y, mo, d, h, m, s, ms, tz) @@ -1588,11 +1801,14 @@ def iso_format(self, sep="T"): def __repr__(self): if self.tzinfo is None: - fields = self.year_month_day + self.hour_minute_second - return "neo4j.time.DateTime(%r, %r, %r, %r, %r, %r)" % fields + fields = (*self.year_month_day, + *self.hour_minute_second_nanoseconds) + return "neo4j.time.DateTime(%r, %r, %r, %r, %r, %r, %r)" % fields else: - fields = self.year_month_day + self.hour_minute_second + (self.tzinfo,) - return "neo4j.time.DateTime(%r, %r, %r, %r, %r, %r, tzinfo=%r)" % fields + fields = (*self.year_month_day, + *self.hour_minute_second_nanoseconds, self.tzinfo) + return ("neo4j.time.DateTime(%r, %r, %r, %r, %r, %r, %r, tzinfo=%r)" + % fields) def __str__(self): return self.iso_format() diff --git a/neo4j/time/arithmetic.py b/neo4j/time/arithmetic.py index d9b374199..c39ea2c2a 100644 --- a/neo4j/time/arithmetic.py +++ b/neo4j/time/arithmetic.py @@ -41,44 +41,6 @@ def nano_add(x, y): return (int(1000000000 * x) + int(1000000000 * y)) / 1000000000 -def nano_sub(x, y): - """ - - >>> 0.7 - 0.2 - 0.49999999999999994 - >>> -0.7 - 0.2 - -0.8999999999999999 - >>> nano_sub(0.7, 0.2) - 0.5 - >>> nano_sub(-0.7, 0.2) - -0.9 - - :param x: - :param y: - :return: - """ - return (int(1000000000 * x) - int(1000000000 * y)) / 1000000000 - - -def nano_mul(x, y): - """ - - >>> 0.7 * 0.2 - 0.13999999999999999 - >>> -0.7 * 0.2 - -0.13999999999999999 - >>> nano_mul(0.7, 0.2) - 0.14 - >>> nano_mul(-0.7, 0.2) - -0.14 - - :param x: - :param y: - :return: - """ - return int(1000000000 * x) * int(1000000000 * y) / 1000000000000000000 - - def nano_div(x, y): """ @@ -98,29 +60,6 @@ def nano_div(x, y): return float(1000000000 * x) / int(1000000000 * y) -def nano_mod(x, y): - """ - - >>> 0.7 % 0.2 - 0.09999999999999992 - >>> -0.7 % 0.2 - 0.10000000000000009 - >>> nano_mod(0.7, 0.2) - 0.1 - >>> nano_mod(-0.7, 0.2) - 0.1 - - :param x: - :param y: - :return: - """ - number = type(x) - nx = int(1000000000 * x) - ny = int(1000000000 * y) - q, r = divmod(nx, ny) - return number(r / 1000000000) - - def nano_divmod(x, y): """ @@ -140,19 +79,6 @@ def nano_divmod(x, y): return int(q), number(r / 1000000000) -def signum(n): - try: - if isnan(n): - return float("nan") - if n > 0 or n == float("inf"): - return 1 - if n < 0 or n == float("-inf"): - return -1 - return 0 - except TypeError: - raise TypeError(n) - - def symmetric_divmod(dividend, divisor): number = type(dividend) if dividend >= 0: diff --git a/neo4j/time/hydration.py b/neo4j/time/hydration.py index 313ed1619..c31e7a531 100644 --- a/neo4j/time/hydration.py +++ b/neo4j/time/hydration.py @@ -77,8 +77,7 @@ def hydrate_time(nanoseconds, tz=None): seconds, nanoseconds = map(int, divmod(nanoseconds, 1000000000)) minutes, seconds = map(int, divmod(seconds, 60)) hours, minutes = map(int, divmod(minutes, 60)) - seconds = (1000000000 * seconds + nanoseconds) / 1000000000 - t = Time(hours, minutes, seconds) + t = Time(hours, minutes, seconds, nanoseconds) if tz is None: return t tz_offset_minutes, tz_offset_seconds = divmod(tz, 60) @@ -94,7 +93,7 @@ def dehydrate_time(value): :return: """ if isinstance(value, Time): - nanoseconds = int(value.ticks * 1000000000) + nanoseconds = value.ticks_ns elif isinstance(value, time): nanoseconds = (3600000000000 * value.hour + 60000000000 * value.minute + 1000000000 * value.second + 1000 * value.microsecond) @@ -118,8 +117,10 @@ def hydrate_datetime(seconds, nanoseconds, tz=None): minutes, seconds = map(int, divmod(seconds, 60)) hours, minutes = map(int, divmod(minutes, 60)) days, hours = map(int, divmod(hours, 24)) - seconds = (1000000000 * seconds + nanoseconds) / 1000000000 - t = DateTime.combine(Date.from_ordinal(get_date_unix_epoch_ordinal() + days), Time(hours, minutes, seconds)) + t = DateTime.combine( + Date.from_ordinal(get_date_unix_epoch_ordinal() + days), + Time(hours, minutes, seconds, nanoseconds) + ) if tz is None: return t if isinstance(tz, int): @@ -183,7 +184,7 @@ def dehydrate_duration(value): :type value: Duration :return: """ - return Structure(b"E", value.months, value.days, value.seconds, int(1000000000 * value.subseconds)) + return Structure(b"E", value.months, value.days, value.seconds, value.nanoseconds) def dehydrate_timedelta(value): diff --git a/tests/unit/time/test_datetime.py b/tests/unit/time/test_datetime.py index 42c9d676e..64d14199a 100644 --- a/tests/unit/time/test_datetime.py +++ b/tests/unit/time/test_datetime.py @@ -19,11 +19,13 @@ # limitations under the License. import copy -from unittest import TestCase +from decimal import Decimal from datetime import ( datetime, timedelta, ) + +import pytest from pytz import ( timezone, FixedOffset, @@ -59,6 +61,11 @@ timezone_utc = timezone("UTC") +def seconds_options(seconds, nanoseconds): + yield seconds, nanoseconds + yield seconds + nanoseconds / 1000000000, + + class FixedClock(Clock): @classmethod @@ -77,277 +84,304 @@ def utc_time(self): return ClockTime(45296, 789000000) -class DateTimeTestCase(TestCase): +class TestDateTime: def test_zero(self): t = DateTime(0, 0, 0, 0, 0, 0) - self.assertEqual(t.year, 0) - self.assertEqual(t.month, 0) - self.assertEqual(t.day, 0) - self.assertEqual(t.hour, 0) - self.assertEqual(t.minute, 0) - self.assertEqual(t.second, 0) - - def test_non_zero_naive(self): - t = DateTime(2018, 4, 26, 23, 0, 17.914390409) - self.assertEqual(t.year, 2018) - self.assertEqual(t.month, 4) - self.assertEqual(t.day, 26) - self.assertEqual(t.hour, 23) - self.assertEqual(t.minute, 0) - self.assertEqual(t.second, 17.914390409) + assert t.year == 0 + assert t.month == 0 + assert t.day == 0 + assert t.hour == 0 + assert t.minute == 0 + assert t.second == 0 + + @pytest.mark.parametrize("seconds_args", [*seconds_options(17, 914390409)]) + def test_non_zero_naive(self, seconds_args): + t = DateTime(2018, 4, 26, 23, 0, *seconds_args) + assert t.year == 2018 + assert t.month == 4 + assert t.day == 26 + assert t.hour == 23 + assert t.minute == 0 + assert t.second == Decimal("17.914390409") + assert t.nanosecond == 914390409 def test_year_lower_bound(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = DateTime(MIN_YEAR - 1, 1, 1, 0, 0, 0) def test_year_upper_bound(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = DateTime(MAX_YEAR + 1, 1, 1, 0, 0, 0) def test_month_lower_bound(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = DateTime(2000, 0, 1, 0, 0, 0) def test_month_upper_bound(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = DateTime(2000, 13, 1, 0, 0, 0) def test_day_zero(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = DateTime(2000, 1, 0, 0, 0, 0) def test_day_30_of_29_day_month(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = DateTime(2000, 2, 30, 0, 0, 0) def test_day_32_of_31_day_month(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = DateTime(2000, 3, 32, 0, 0, 0) def test_day_31_of_30_day_month(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = DateTime(2000, 4, 31, 0, 0, 0) def test_day_29_of_28_day_month(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = DateTime(1999, 2, 29, 0, 0, 0) def test_last_day_of_month(self): t = DateTime(2000, 1, -1, 0, 0, 0) - self.assertEqual(t.year, 2000) - self.assertEqual(t.month, 1) - self.assertEqual(t.day, 31) + assert t.year == 2000 + assert t.month == 1 + assert t.day == 31 def test_today(self): t = DateTime.today() - self.assertEqual(t.year, 1970) - self.assertEqual(t.month, 1) - self.assertEqual(t.day, 1) - self.assertEqual(t.hour, 12) - self.assertEqual(t.minute, 34) - self.assertEqual(t.second, 56.789) + assert t.year == 1970 + assert t.month == 1 + assert t.day == 1 + assert t.hour == 12 + assert t.minute == 34 + assert t.second == Decimal("56.789000000") + assert t.nanosecond == 789000000 def test_now_without_tz(self): t = DateTime.now() - self.assertEqual(t.year, 1970) - self.assertEqual(t.month, 1) - self.assertEqual(t.day, 1) - self.assertEqual(t.hour, 12) - self.assertEqual(t.minute, 34) - self.assertEqual(t.second, 56.789) - self.assertIsNone(t.tzinfo) + assert t.year == 1970 + assert t.month == 1 + assert t.day == 1 + assert t.hour == 12 + assert t.minute == 34 + assert t.second == Decimal("56.789000000") + assert t.nanosecond == 789000000 + assert t.tzinfo is None def test_now_with_tz(self): t = DateTime.now(timezone_us_eastern) - self.assertEqual(t.year, 1970) - self.assertEqual(t.month, 1) - self.assertEqual(t.day, 1) - self.assertEqual(t.hour, 7) - self.assertEqual(t.minute, 34) - self.assertEqual(t.second, 56.789) - self.assertEqual(t.utcoffset(), timedelta(seconds=-18000)) - self.assertEqual(t.dst(), timedelta()) - self.assertEqual(t.tzname(), "EST") + assert t.year == 1970 + assert t.month == 1 + assert t.day == 1 + assert t.hour == 7 + assert t.minute == 34 + assert t.second == Decimal("56.789000000") + assert t.nanosecond == 789000000 + assert t.utcoffset() == timedelta(seconds=-18000) + assert t.dst() == timedelta() + assert t.tzname() == "EST" def test_utc_now(self): t = DateTime.utc_now() - self.assertEqual(t.year, 1970) - self.assertEqual(t.month, 1) - self.assertEqual(t.day, 1) - self.assertEqual(t.hour, 12) - self.assertEqual(t.minute, 34) - self.assertEqual(t.second, 56.789) - self.assertIsNone(t.tzinfo) + assert t.year == 1970 + assert t.month == 1 + assert t.day == 1 + assert t.hour == 12 + assert t.minute == 34 + assert t.second == Decimal("56.789000000") + assert t.nanosecond == 789000000 + assert t.tzinfo is None def test_from_timestamp(self): t = DateTime.from_timestamp(0) - self.assertEqual(t.year, 1970) - self.assertEqual(t.month, 1) - self.assertEqual(t.day, 1) - self.assertEqual(t.hour, 0) - self.assertEqual(t.minute, 0) - self.assertEqual(t.second, 0.0) - self.assertIsNone(t.tzinfo) + assert t.year == 1970 + assert t.month == 1 + assert t.day == 1 + assert t.hour == 0 + assert t.minute == 0 + assert t.second == Decimal("0.0") + assert t.nanosecond == 0 + assert t.tzinfo is None def test_from_overflowing_timestamp(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = DateTime.from_timestamp(999999999999999999) def test_from_timestamp_with_tz(self): t = DateTime.from_timestamp(0, timezone_us_eastern) - self.assertEqual(t.year, 1969) - self.assertEqual(t.month, 12) - self.assertEqual(t.day, 31) - self.assertEqual(t.hour, 19) - self.assertEqual(t.minute, 0) - self.assertEqual(t.second, 0.0) - self.assertEqual(t.utcoffset(), timedelta(seconds=-18000)) - self.assertEqual(t.dst(), timedelta()) - self.assertEqual(t.tzname(), "EST") - - def test_conversion_to_t(self): - dt = DateTime(2018, 4, 26, 23, 0, 17.914390409) + assert t.year == 1969 + assert t.month == 12 + assert t.day == 31 + assert t.hour == 19 + assert t.minute == 0 + assert t.second == Decimal("0.0") + assert t.nanosecond == 0 + assert t.utcoffset() == timedelta(seconds=-18000) + assert t.dst() == timedelta() + assert t.tzname() == "EST" + + @pytest.mark.parametrize("seconds_args", seconds_options(17, 914390409)) + def test_conversion_to_t(self, seconds_args): + dt = DateTime(2018, 4, 26, 23, 0, *seconds_args) t = dt.to_clock_time() - self.assertEqual(t, ClockTime(63660380417, 914390409)) + assert t, ClockTime(63660380417 == 914390409) - def test_add_timedelta(self): - dt1 = DateTime(2018, 4, 26, 23, 0, 17.914390409) + @pytest.mark.parametrize("seconds_args1", seconds_options(17, 914390409)) + @pytest.mark.parametrize("seconds_args2", seconds_options(17, 914390409)) + def test_add_timedelta(self, seconds_args1, seconds_args2): + dt1 = DateTime(2018, 4, 26, 23, 0, *seconds_args1) delta = timedelta(days=1) dt2 = dt1 + delta - self.assertEqual(dt2, DateTime(2018, 4, 27, 23, 0, 17.914390409)) + assert dt2, DateTime(2018, 4, 27, 23, 0 == seconds_args2) - def test_subtract_datetime_1(self): - dt1 = DateTime(2018, 4, 26, 23, 0, 17.914390409) - dt2 = DateTime(2018, 1, 1, 0, 0, 0.0) + @pytest.mark.parametrize("seconds_args", seconds_options(17, 914390409)) + def test_subtract_datetime_1(self, seconds_args): + dt1 = DateTime(2018, 4, 26, 23, 0, *seconds_args) + dt2 = DateTime(2018, 1, 1, 0, 0, 0) t = dt1 - dt2 - self.assertEqual(t, Duration(months=3, days=25, hours=23, seconds=17.914390409)) - def test_subtract_datetime_2(self): - dt1 = DateTime(2018, 4, 1, 23, 0, 17.914390409) + assert t == Duration(months=3, days=25, hours=23, seconds=17.914390409) + assert t == Duration(months=3, days=25, hours=23, seconds=17, + nanoseconds=914390409) + + @pytest.mark.parametrize("seconds_args", seconds_options(17, 914390409)) + def test_subtract_datetime_2(self, seconds_args): + dt1 = DateTime(2018, 4, 1, 23, 0, *seconds_args) dt2 = DateTime(2018, 1, 26, 0, 0, 0.0) t = dt1 - dt2 - self.assertEqual(t, Duration(months=3, days=-25, hours=23, seconds=17.914390409)) + assert t == Duration(months=3, days=-25, hours=23, seconds=17.914390409) + assert t == Duration(months=3, days=-25, hours=23, seconds=17, + nanoseconds=914390409) - def test_subtract_native_datetime_1(self): - dt1 = DateTime(2018, 4, 26, 23, 0, 17.914390409) + @pytest.mark.parametrize("seconds_args", seconds_options(17, 914390409)) + def test_subtract_native_datetime_1(self, seconds_args): + dt1 = DateTime(2018, 4, 26, 23, 0, *seconds_args) dt2 = datetime(2018, 1, 1, 0, 0, 0) t = dt1 - dt2 - self.assertEqual(t, timedelta(days=115, hours=23, seconds=17.914390409)) + assert t == timedelta(days=115, hours=23, seconds=17.914390409) - def test_subtract_native_datetime_2(self): - dt1 = DateTime(2018, 4, 1, 23, 0, 17.914390409) + @pytest.mark.parametrize("seconds_args", seconds_options(17, 914390409)) + def test_subtract_native_datetime_2(self, seconds_args): + dt1 = DateTime(2018, 4, 1, 23, 0, *seconds_args) dt2 = datetime(2018, 1, 26, 0, 0, 0) t = dt1 - dt2 - self.assertEqual(t, timedelta(days=65, hours=23, seconds=17.914390409)) + assert t == timedelta(days=65, hours=23, seconds=17.914390409) def test_normalization(self): ndt1 = timezone_us_eastern.normalize(DateTime(2018, 4, 27, 23, 0, 17, tzinfo=timezone_us_eastern)) ndt2 = timezone_us_eastern.normalize(datetime(2018, 4, 27, 23, 0, 17, tzinfo=timezone_us_eastern)) - self.assertEqual(ndt1, ndt2) + assert ndt1 == ndt2 def test_localization(self): ldt1 = timezone_us_eastern.localize(datetime(2018, 4, 27, 23, 0, 17)) ldt2 = timezone_us_eastern.localize(DateTime(2018, 4, 27, 23, 0, 17)) - self.assertEqual(ldt1, ldt2) + assert ldt1 == ldt2 def test_from_native(self): native = datetime(2018, 10, 1, 12, 34, 56, 789123) dt = DateTime.from_native(native) - self.assertEqual(dt.year, native.year) - self.assertEqual(dt.month, native.month) - self.assertEqual(dt.day, native.day) - self.assertEqual(dt.hour, native.hour) - self.assertEqual(dt.minute, native.minute) - self.assertEqual(dt.second, nano_add(native.second, nano_div(native.microsecond, 1000000))) + assert dt.year == native.year + assert dt.month == native.month + assert dt.day == native.day + assert dt.hour == native.hour + assert dt.minute == native.minute + assert dt.second == (native.second + + Decimal(native.microsecond) / 1000000) + assert int(dt.second) == native.second + assert dt.nanosecond == native.microsecond * 1000 def test_to_native(self): dt = DateTime(2018, 10, 1, 12, 34, 56.789123456) native = dt.to_native() - self.assertEqual(dt.year, native.year) - self.assertEqual(dt.month, native.month) - self.assertEqual(dt.day, native.day) - self.assertEqual(dt.hour, native.hour) - self.assertEqual(dt.minute, native.minute) - self.assertEqual(56.789123, nano_add(native.second, nano_div(native.microsecond, 1000000))) + assert dt.year == native.year + assert dt.month == native.month + assert dt.day == native.day + assert dt.hour == native.hour + assert dt.minute == native.minute + assert 56.789123, nano_add(native.second, nano_div(native.microsecond == 1000000)) def test_iso_format(self): dt = DateTime(2018, 10, 1, 12, 34, 56.789123456) - self.assertEqual("2018-10-01T12:34:56.789123456", dt.iso_format()) + assert "2018-10-01T12:34:56.789123456" == dt.iso_format() def test_iso_format_with_trailing_zeroes(self): dt = DateTime(2018, 10, 1, 12, 34, 56.789) - self.assertEqual("2018-10-01T12:34:56.789000000", dt.iso_format()) + assert "2018-10-01T12:34:56.789000000" == dt.iso_format() def test_iso_format_with_tz(self): dt = timezone_us_eastern.localize(DateTime(2018, 10, 1, 12, 34, 56.789123456)) - self.assertEqual("2018-10-01T12:34:56.789123456-04:00", dt.iso_format()) + assert "2018-10-01T12:34:56.789123456-04:00" == dt.iso_format() def test_iso_format_with_tz_and_trailing_zeroes(self): dt = timezone_us_eastern.localize(DateTime(2018, 10, 1, 12, 34, 56.789)) - self.assertEqual("2018-10-01T12:34:56.789000000-04:00", dt.iso_format()) + assert "2018-10-01T12:34:56.789000000-04:00" == dt.iso_format() def test_from_iso_format_hour_only(self): expected = DateTime(2018, 10, 1, 12, 0, 0) actual = DateTime.from_iso_format("2018-10-01T12") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_hour_and_minute(self): expected = DateTime(2018, 10, 1, 12, 34, 0) actual = DateTime.from_iso_format("2018-10-01T12:34") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_hour_minute_second(self): expected = DateTime(2018, 10, 1, 12, 34, 56) actual = DateTime.from_iso_format("2018-10-01T12:34:56") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_hour_minute_second_milliseconds(self): - expected = DateTime(2018, 10, 1, 12, 34, 56.123) + expected = DateTime(2018, 10, 1, 12, 34, 56, 123000000) actual = DateTime.from_iso_format("2018-10-01T12:34:56.123") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_hour_minute_second_microseconds(self): - expected = DateTime(2018, 10, 1, 12, 34, 56.123456) + expected = DateTime(2018, 10, 1, 12, 34, 56, 123456000) actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_hour_minute_second_nanoseconds(self): - expected = DateTime(2018, 10, 1, 12, 34, 56.123456789) + expected = DateTime(2018, 10, 1, 12, 34, 56, 123456789) actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456789") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_with_positive_tz(self): - expected = DateTime(2018, 10, 1, 12, 34, 56.123456789, tzinfo=FixedOffset(754)) + expected = DateTime(2018, 10, 1, 12, 34, 56, 123456789, + tzinfo=FixedOffset(754)) actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456789+12:34") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_with_negative_tz(self): - expected = DateTime(2018, 10, 1, 12, 34, 56.123456789, tzinfo=FixedOffset(-754)) + expected = DateTime(2018, 10, 1, 12, 34, 56, 123456789, + tzinfo=FixedOffset(-754)) actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456789-12:34") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_with_positive_long_tz(self): - expected = DateTime(2018, 10, 1, 12, 34, 56.123456789, tzinfo=FixedOffset(754)) + expected = DateTime(2018, 10, 1, 12, 34, 56, 123456789, + tzinfo=FixedOffset(754)) actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456789+12:34:56.123456") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_with_negative_long_tz(self): - expected = DateTime(2018, 10, 1, 12, 34, 56.123456789, tzinfo=FixedOffset(-754)) + expected = DateTime(2018, 10, 1, 12, 34, 56, 123456789, + tzinfo=FixedOffset(-754)) actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456789-12:34:56.123456") - self.assertEqual(expected, actual) + assert expected == actual def test_datetime_copy(self): d = DateTime(2010, 10, 1, 10, 0, 10) d2 = copy.copy(d) - self.assertIsNot(d, d2) - self.assertEqual(d, d2) + assert d is not d2 + assert d == d2 def test_datetime_deep_copy(self): d = DateTime(2010, 10, 1, 10, 0, 12) d2 = copy.deepcopy(d) - self.assertIsNot(d, d2) - self.assertEqual(d, d2) + assert d is not d2 + assert d == d2 def test_iso_format_with_time_zone_case_1(): @@ -388,6 +422,18 @@ def test_to_native_case_2(): assert native.isoformat() == "2019-10-30T12:34:56.789123+00:00" +def test_to_native_case_3(): + # python -m pytest tests/unit/time/test_datetime.py -s -v -k test_to_native_case_3 + timestamp = "2021-04-06T00:00:00.500006+00:00" + neo4j_datetime = DateTime.from_iso_format(timestamp) + native_from_neo4j = neo4j_datetime.to_native() + native_from_datetime = datetime(2021, 4, 6, 0, 0, 0, 500006, + tzinfo=timezone_utc) + + assert neo4j_datetime == native_from_datetime + assert native_from_neo4j == native_from_datetime + + def test_from_native_case_1(): # python -m pytest tests/unit/time/test_datetime.py -s -v -k test_from_native_case_1 native = datetime(2018, 10, 1, 12, 34, 56, 789123) @@ -397,7 +443,10 @@ def test_from_native_case_1(): assert dt.day == native.day assert dt.hour == native.hour assert dt.minute == native.minute - assert dt.second == nano_add(native.second, nano_div(native.microsecond, 1000000)) + assert dt.second == (native.second + + Decimal(native.microsecond) / 1000000) + assert int(dt.second) == native.second + assert dt.nanosecond == native.microsecond * 1000 assert dt.tzinfo is None @@ -410,5 +459,8 @@ def test_from_native_case_2(): assert dt.day == native.day assert dt.hour == native.hour assert dt.minute == native.minute - assert dt.second == nano_add(native.second, nano_div(native.microsecond, 1000000)) + assert dt.second == (native.second + + Decimal(native.microsecond) / 1000000) + assert int(dt.second) == native.second + assert dt.nanosecond == native.microsecond * 1000 assert dt.tzinfo == FixedOffset(0) diff --git a/tests/unit/time/test_duration.py b/tests/unit/time/test_duration.py index 4195a9428..2a80d0acc 100644 --- a/tests/unit/time/test_duration.py +++ b/tests/unit/time/test_duration.py @@ -20,316 +20,398 @@ from datetime import timedelta -from unittest import TestCase +from decimal import Decimal +import copy + +import pytest from neo4j.time import Duration -class DurationTestCase(TestCase): +def seconds_options(seconds, nanoseconds): + yield {"seconds": seconds, "nanoseconds": nanoseconds} + yield {"seconds": seconds, "subseconds": nanoseconds / 1000000000} + yield {"seconds": seconds + Decimal(nanoseconds) / 1000000000} + + +class TestDuration: def test_zero(self): d = Duration() - self.assertEqual(d.months, 0) - self.assertEqual(d.days, 0) - self.assertEqual(d.seconds, 0) - self.assertEqual(d.subseconds, 0.0) - self.assertEqual(d.years_months_days, (0, 0, 0)) - self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.0)) - self.assertFalse(bool(d)) + assert d.months == 0 + assert d.days == 0 + assert d.seconds == 0 + assert d.nanoseconds == 0 + assert d.years_months_days == (0, 0, 0) + assert d.hours_minutes_seconds == (0, 0, Decimal("0E-9")) + assert d.hours_minutes_seconds_nanoseconds == (0, 0, 0, 0) + assert not bool(d) def test_years_only(self): d = Duration(years=2) - self.assertEqual(d.months, 24) - self.assertEqual(d.days, 0) - self.assertEqual(d.seconds, 0) - self.assertEqual(d.subseconds, 0.0) - self.assertEqual(d.years_months_days, (2, 0, 0)) - self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.0)) + assert d.months == 24 + assert d.days == 0 + assert d.seconds == 0 + assert d.nanoseconds == 0 + assert d.years_months_days == (2, 0, 0) + assert d.hours_minutes_seconds == (0, 0, Decimal("0E-9")) + assert d.hours_minutes_seconds_nanoseconds == (0, 0, 0, 0) + assert bool(d) def test_months_only(self): d = Duration(months=20) - self.assertEqual(d.months, 20) - self.assertEqual(d.days, 0) - self.assertEqual(d.seconds, 0) - self.assertEqual(d.subseconds, 0.0) - self.assertEqual(d.years_months_days, (1, 8, 0)) - self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.0)) + assert d.months == 20 + assert d.days == 0 + assert d.seconds == 0 + assert d.nanoseconds == 0 + assert d.years_months_days == (1, 8, 0) + assert d.hours_minutes_seconds == (0, 0, Decimal("0E-9")) + assert d.hours_minutes_seconds_nanoseconds == (0, 0, 0, 0) + assert bool(d) def test_months_out_of_range(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = Duration(months=(2**64)) def test_weeks_only(self): d = Duration(weeks=4) - self.assertEqual(d.months, 0) - self.assertEqual(d.days, 28) - self.assertEqual(d.seconds, 0) - self.assertEqual(d.subseconds, 0.0) - self.assertEqual(d.years_months_days, (0, 0, 28)) - self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.0)) + assert d.months == 0 + assert d.days == 28 + assert d.seconds == 0 + assert d.nanoseconds == 0 + assert d.years_months_days == (0, 0, 28) + assert d.hours_minutes_seconds == (0, 0, Decimal("0E-9")) + assert d.hours_minutes_seconds_nanoseconds == (0, 0, 0, 0) + assert bool(d) def test_days_only(self): d = Duration(days=40) - self.assertEqual(d.months, 0) - self.assertEqual(d.days, 40) - self.assertEqual(d.seconds, 0) - self.assertEqual(d.subseconds, 0.0) - self.assertEqual(d.years_months_days, (0, 0, 40)) - self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.0)) + assert d.months == 0 + assert d.days == 40 + assert d.seconds == 0 + assert d.nanoseconds == 0 + assert d.years_months_days == (0, 0, 40) + assert d.hours_minutes_seconds == (0, 0, Decimal("0E-9")) + assert d.hours_minutes_seconds_nanoseconds == (0, 0, 0, 0) + assert bool(d) def test_days_out_of_range(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = Duration(days=(2**64)) def test_hours_only(self): d = Duration(hours=10) - self.assertEqual(d.months, 0) - self.assertEqual(d.days, 0) - self.assertEqual(d.seconds, 36000) - self.assertEqual(d.subseconds, 0.0) - self.assertEqual(d.years_months_days, (0, 0, 0)) - self.assertEqual(d.hours_minutes_seconds, (10, 0, 0.0)) + assert d.months == 0 + assert d.days == 0 + assert d.seconds == 36000 + assert d.nanoseconds == 0 + assert d.years_months_days == (0, 0, 0) + assert d.hours_minutes_seconds == (10, 0, Decimal("0E-9")) + assert d.hours_minutes_seconds_nanoseconds == (10, 0, 0, 0) + assert bool(d) def test_minutes_only(self): d = Duration(minutes=90.5) - self.assertEqual(d.months, 0) - self.assertEqual(d.days, 0) - self.assertEqual(d.seconds, 5430) - self.assertEqual(d.subseconds, 0.0) - self.assertEqual(d.years_months_days, (0, 0, 0)) - self.assertEqual(d.hours_minutes_seconds, (1, 30, 30.0)) - - def test_seconds_only(self): - d = Duration(seconds=123.456) - self.assertEqual(d.months, 0) - self.assertEqual(d.days, 0) - self.assertEqual(d.seconds, 123) - self.assertEqual(d.subseconds, 0.456) - self.assertEqual(d.years_months_days, (0, 0, 0)) - self.assertEqual(d.hours_minutes_seconds, (0, 2, 3.456)) + assert d.months == 0 + assert d.days == 0 + assert d.seconds == 5430 + assert d.nanoseconds == 0 + assert d.years_months_days == (0, 0, 0) + assert d.hours_minutes_seconds == (1, 30, Decimal("30.000000000")) + assert d.hours_minutes_seconds_nanoseconds == (1, 30, 30, 0) + assert bool(d) + + @pytest.mark.parametrize("sec_kwargs", seconds_options(123, 456000000)) + def test_seconds_only(self, sec_kwargs): + d = Duration(**sec_kwargs) + assert d.months == 0 + assert d.days == 0 + assert d.seconds == 123 + assert d.subseconds == Decimal("0.456000000") + assert d.nanoseconds == 456000000 + assert d.years_months_days == (0, 0, 0) + assert d.hours_minutes_seconds == (0, 2, Decimal("3.456000000")) + assert d.hours_minutes_seconds_nanoseconds == (0, 2, 3, 456000000) + assert bool(d) def test_seconds_out_of_range(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): _ = Duration(seconds=(2**64)) def test_subseconds_only(self): d = Duration(subseconds=123.456) - self.assertEqual(d.months, 0) - self.assertEqual(d.days, 0) - self.assertEqual(d.seconds, 123) - self.assertEqual(d.subseconds, 0.456) - self.assertEqual(d.years_months_days, (0, 0, 0)) - self.assertEqual(d.hours_minutes_seconds, (0, 2, 3.456)) + assert d.months == 0 + assert d.days == 0 + assert d.seconds == 123 + assert d.subseconds == Decimal("0.456") + assert d.nanoseconds == 456000000 + assert d.years_months_days == (0, 0, 0) + assert d.hours_minutes_seconds == (0, 2, Decimal("3.456")) + assert d.hours_minutes_seconds_nanoseconds == (0, 2, 3, 456000000) + assert bool(d) def test_milliseconds_only(self): d = Duration(milliseconds=1234.567) - self.assertEqual(d.months, 0) - self.assertEqual(d.days, 0) - self.assertEqual(d.seconds, 1) - self.assertEqual(d.subseconds, 0.234567) - self.assertEqual(d.years_months_days, (0, 0, 0)) - self.assertEqual(d.hours_minutes_seconds, (0, 0, 1.234567)) + assert d.months == 0 + assert d.days == 0 + assert d.seconds == 1 + assert d.subseconds == Decimal("0.234567000") + assert d.nanoseconds == 234567000 + assert d.years_months_days == (0, 0, 0) + assert d.hours_minutes_seconds == (0, 0, Decimal("1.234567000")) + assert d.hours_minutes_seconds_nanoseconds == (0, 0, 1, 234567000) + assert bool(d) def test_microseconds_only(self): d = Duration(microseconds=1234.567) - self.assertEqual(d.months, 0) - self.assertEqual(d.days, 0) - self.assertEqual(d.seconds, 0) - self.assertEqual(d.subseconds, 0.001234567) - self.assertEqual(d.years_months_days, (0, 0, 0)) - self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.001234567)) + assert d.months == 0 + assert d.days == 0 + assert d.seconds == 0 + assert d.subseconds == Decimal("0.001234567") + assert d.nanoseconds == 1234567 + assert d.years_months_days == (0, 0, 0) + assert d.hours_minutes_seconds == (0, 0, Decimal("0.001234567")) + assert d.hours_minutes_seconds_nanoseconds == (0, 0, 0, 1234567) + assert bool(d) def test_nanoseconds_only(self): d = Duration(nanoseconds=1234.567) - self.assertEqual(d.months, 0) - self.assertEqual(d.days, 0) - self.assertEqual(d.seconds, 0) - self.assertEqual(d.subseconds, 0.000001234) - self.assertEqual(d.years_months_days, (0, 0, 0)) - self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.000001234)) + assert d.months == 0 + assert d.days == 0 + assert d.seconds == 0 + assert d.subseconds == Decimal("0.000001234") + assert d.nanoseconds == 1234 + assert d.years_months_days == (0, 0, 0) + assert d.hours_minutes_seconds == (0, 0, Decimal("0.000001234")) + assert d.hours_minutes_seconds_nanoseconds == (0, 0, 0, 1234) + assert bool(d) def test_can_combine_years_months(self): t = Duration(years=5, months=3) - self.assertEqual(t.months, 63) + assert t.months == 63 def test_can_combine_weeks_and_days(self): t = Duration(weeks=5, days=3) - self.assertEqual(t.days, 38) + assert t.days == 38 def test_can_combine_hours_minutes_seconds(self): t = Duration(hours=5, minutes=4, seconds=3) - self.assertEqual(t.seconds, 18243) + assert t.seconds == 18243 - def test_can_combine_seconds_and_subseconds(self): - t = Duration(seconds=123.456, subseconds=0.321) - self.assertEqual(t.seconds, 123) - self.assertEqual(t.subseconds, 0.777) + def test_can_combine_seconds_and_nanoseconds(self): + t = Duration(seconds=123.456, nanoseconds=321000000) + assert t.seconds == 123 + assert t.nanoseconds == 777000000 + assert t == Duration(seconds=123, nanoseconds=777000000) + assert t == Duration(seconds=123.777) def test_full_positive(self): d = Duration(years=1, months=2, days=3, hours=4, minutes=5, seconds=6.789) - self.assertEqual(d.months, 14) - self.assertEqual(d.days, 3) - self.assertEqual(d.seconds, 14706) - self.assertEqual(d.subseconds, 0.789) - self.assertEqual(d.years_months_days, (1, 2, 3)) - self.assertEqual(d.hours_minutes_seconds, (4, 5, 6.789)) - self.assertTrue(bool(d)) + assert d.months == 14 + assert d.days == 3 + assert d.seconds == 14706 + assert d.nanoseconds == 789000000 + assert d.years_months_days == (1, 2, 3) + assert d.hours_minutes_seconds == (4, 5, Decimal("6.789000000")) + assert d.hours_minutes_seconds_nanoseconds == (4, 5, 6, 789000000) + assert bool(d) def test_full_negative(self): d = Duration(years=-1, months=-2, days=-3, hours=-4, minutes=-5, seconds=-6.789) - self.assertEqual(d.months, -14) - self.assertEqual(d.days, -3) - self.assertEqual(d.seconds, -14706) - self.assertEqual(d.subseconds, -0.789) - self.assertEqual(d.years_months_days, (-1, -2, -3)) - self.assertEqual(d.hours_minutes_seconds, (-4, -5, -6.789)) - self.assertTrue(bool(d)) - - def test_negative_positive_negative(self): + assert d.months == -14 + assert d.days == -3 + assert d.seconds == -14706 + assert d.nanoseconds == -789000000 + assert d.years_months_days == (-1, -2, -3) + assert d.hours_minutes_seconds == (-4, -5, Decimal("-6.789")) + assert d.hours_minutes_seconds_nanoseconds == (-4, -5, -6, -789000000) + assert bool(d) + + def test_negative_positive(self): d = Duration(years=-1, months=-2, days=3, hours=-4, minutes=-5, seconds=-6.789) - self.assertEqual(d.months, -14) - self.assertEqual(d.days, 3) - self.assertEqual(d.seconds, -14706) - self.assertEqual(d.subseconds, -0.789) - self.assertEqual(d.years_months_days, (-1, -2, 3)) - self.assertEqual(d.hours_minutes_seconds, (-4, -5, -6.789)) - - def test_positive_negative_positive(self): + assert d.months == -14 + assert d.days == 3 + assert d.seconds == -14706 + assert d.subseconds == Decimal("-0.789") + assert d.nanoseconds == -789000000 + assert d.years_months_days == (-1, -2, 3) + assert d.hours_minutes_seconds == (-4, -5, Decimal("-6.789")) + assert d.hours_minutes_seconds_nanoseconds == (-4, -5, -6, -789000000) + + def test_positive_negative(self): d = Duration(years=1, months=2, days=-3, hours=4, minutes=5, seconds=6.789) - self.assertEqual(d.months, 14) - self.assertEqual(d.days, -3) - self.assertEqual(d.seconds, 14706) - self.assertEqual(d.subseconds, 0.789) - self.assertEqual(d.years_months_days, (1, 2, -3)) - self.assertEqual(d.hours_minutes_seconds, (4, 5, 6.789)) - - def test_add_duration(self): - d1 = Duration(months=2, days=3, seconds=5.7) - d2 = Duration(months=7, days=5, seconds=3.2) - self.assertEqual(d1 + d2, Duration(months=9, days=8, seconds=8.9)) - - def test_add_timedelta(self): - d1 = Duration(months=2, days=3, seconds=5.7) + assert d.months == 14 + assert d.days == -3 + assert d.seconds == 14706 + assert d.subseconds == Decimal("0.789") + assert d.nanoseconds == 789000000 + assert d.years_months_days == (1, 2, -3) + assert d.hours_minutes_seconds == (4, 5, Decimal("6.789")) + assert d.hours_minutes_seconds_nanoseconds == (4, 5, 6, 789000000) + + @pytest.mark.parametrize("sec1_kwargs", seconds_options(5, 700000000)) + @pytest.mark.parametrize("sec2_kwargs", seconds_options(3, 200000000)) + @pytest.mark.parametrize("sec3_kwargs", seconds_options(8, 900000000)) + def test_add_duration(self, sec1_kwargs, sec2_kwargs, sec3_kwargs): + d1 = Duration(months=2, days=3, **sec1_kwargs) + d2 = Duration(months=7, days=5, **sec2_kwargs) + assert d1 + d2 == Duration(months=9, days=8, **sec3_kwargs) + + @pytest.mark.parametrize("sec1_kwargs", seconds_options(5, 700000000)) + @pytest.mark.parametrize("sec2_kwargs", seconds_options(8, 900000000)) + def test_add_timedelta(self, sec1_kwargs, sec2_kwargs): + d1 = Duration(months=2, days=3, **sec1_kwargs) td = timedelta(days=5, seconds=3.2) - self.assertEqual(d1 + td, Duration(months=2, days=8, seconds=8.9)) + assert d1 + td == Duration(months=2, days=8, **sec2_kwargs) def test_add_object(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): _ = Duration(months=2, days=3, seconds=5.7) + object() - def test_subtract_duration(self): - d1 = Duration(months=2, days=3, seconds=5.7) - d2 = Duration(months=7, days=5, seconds=3.2) - self.assertEqual(d1 - d2, Duration(months=-5, days=-2, seconds=2.5)) - - def test_subtract_timedelta(self): - d1 = Duration(months=2, days=3, seconds=5.7) + @pytest.mark.parametrize("sec1_kwargs", seconds_options(5, 700000000)) + @pytest.mark.parametrize("sec2_kwargs", seconds_options(3, 200000000)) + @pytest.mark.parametrize("sec3_kwargs", seconds_options(2, 500000000)) + def test_subtract_duration(self, sec1_kwargs, sec2_kwargs, sec3_kwargs): + d1 = Duration(months=2, days=3, **sec1_kwargs) + d2 = Duration(months=7, days=5, **sec2_kwargs) + assert d1 - d2 == Duration(months=-5, days=-2, **sec3_kwargs) + + @pytest.mark.parametrize("sec1_kwargs", seconds_options(5, 700000000)) + @pytest.mark.parametrize("sec2_kwargs", seconds_options(2, 500000000)) + def test_subtract_timedelta(self, sec1_kwargs, sec2_kwargs): + d1 = Duration(months=2, days=3, **sec1_kwargs) td = timedelta(days=5, seconds=3.2) - self.assertEqual(d1 - td, Duration(months=2, days=-2, seconds=2.5)) + assert d1 - td == Duration(months=2, days=-2, **sec2_kwargs) def test_subtract_object(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): _ = Duration(months=2, days=3, seconds=5.7) - object() def test_multiplication_by_int(self): d1 = Duration(months=2, days=3, seconds=5.7) i = 11 - self.assertEqual(d1 * i, Duration(months=22, days=33, seconds=62.7)) + assert d1 * i == Duration(months=22, days=33, seconds=62.7) def test_multiplication_by_float(self): d1 = Duration(months=2, days=3, seconds=5.7) f = 5.5 - self.assertEqual(d1 * f, Duration(months=11, days=16, seconds=31.35)) + assert d1 * f == Duration(months=11, days=16, seconds=31.35) def test_multiplication_by_object(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): _ = Duration(months=2, days=3, seconds=5.7) * object() def test_floor_division_by_int(self): d1 = Duration(months=11, days=33, seconds=55.77) i = 2 - self.assertEqual(d1 // i, Duration(months=5, days=16, seconds=27.0)) + assert d1 // i == Duration(months=5, days=16, seconds=27) def test_floor_division_by_object(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): _ = Duration(months=2, days=3, seconds=5.7) // object() def test_modulus_by_int(self): d1 = Duration(months=11, days=33, seconds=55.77) i = 2 - self.assertEqual(d1 % i, Duration(months=1, days=1, seconds=1.77)) + assert d1 % i == Duration(months=1, days=1, seconds=1.77) def test_modulus_by_object(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): _ = Duration(months=2, days=3, seconds=5.7) % object() def test_floor_division_and_modulus_by_int(self): d1 = Duration(months=11, days=33, seconds=55.77) i = 2 - self.assertEqual(divmod(d1, i), (Duration(months=5, days=16, seconds=27.0), - Duration(months=1, days=1, seconds=1.77))) + assert divmod(d1, i) == (Duration(months=5, days=16, seconds=27.0), + Duration(months=1, days=1, seconds=1.77)) def test_floor_division_and_modulus_by_object(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): _ = divmod(Duration(months=2, days=3, seconds=5.7), object()) def test_true_division_by_int(self): d1 = Duration(months=11, days=33, seconds=55.77) i = 2 - self.assertEqual(d1 / i, Duration(months=6, days=16, seconds=27.885)) + assert d1 / i == Duration(months=6, days=16, seconds=27.885) def test_true_division_by_float(self): d1 = Duration(months=11, days=33, seconds=55.77) f = 2.5 - self.assertEqual(d1 / f, Duration(months=4, days=13, seconds=22.308)) + assert d1 / f == Duration(months=4, days=13, seconds=22.308) def test_true_division_by_object(self): - with self.assertRaises(TypeError): + with pytest.raises(TypeError): _ = Duration(months=2, days=3, seconds=5.7) / object() def test_unary_plus(self): d = Duration(months=11, days=33, seconds=55.77) - self.assertEqual(+d, Duration(months=11, days=33, seconds=55.77)) + assert +d == Duration(months=11, days=33, seconds=55.77) def test_unary_minus(self): d = Duration(months=11, days=33, seconds=55.77) - self.assertEqual(-d, Duration(months=-11, days=-33, seconds=-55.77)) + assert -d == Duration(months=-11, days=-33, seconds=-55.77) def test_absolute(self): d = Duration(months=-11, days=-33, seconds=-55.77) - self.assertEqual(abs(d), Duration(months=11, days=33, seconds=55.77)) + assert abs(d) == Duration(months=11, days=33, seconds=55.77) def test_str(self): - self.assertEqual(str(Duration()), "PT0S") - self.assertEqual(str(Duration(years=1, months=2)), "P1Y2M") - self.assertEqual(str(Duration(years=-1, months=2)), "P-10M") - self.assertEqual(str(Duration(months=-13)), "P-1Y-1M") - self.assertEqual(str(Duration(months=2, days=3, seconds=5.7)), "P2M3DT5.7S") - self.assertEqual(str(Duration(hours=12, minutes=34)), "PT12H34M") - self.assertEqual(str(Duration(seconds=59)), "PT59S") - self.assertEqual(str(Duration(seconds=0.123456789)), "PT0.123456789S") - self.assertEqual(str(Duration(seconds=-0.123456789)), "PT-0.123456789S") + assert str(Duration()) == "PT0S" + assert str(Duration(years=1, months=2)) == "P1Y2M" + assert str(Duration(years=-1, months=2)) == "P-10M" + assert str(Duration(months=-13)) == "P-1Y-1M" + assert str(Duration(months=2, days=3, seconds=5.7)) == "P2M3DT5.7S" + assert str(Duration(hours=12, minutes=34)) == "PT12H34M" + assert str(Duration(seconds=59)) == "PT59S" + assert str(Duration(seconds=0.123456789)) == "PT0.123456789S" + assert str(Duration(seconds=-0.123456789)) == "PT-0.123456789S" + assert str(Duration(seconds=-2, nanoseconds=1)) == "PT-1.999999999S" def test_repr(self): d = Duration(months=2, days=3, seconds=5.7) - self.assertEqual(repr(d), "Duration(months=2, days=3, seconds=5, subseconds=0.7)") + assert repr(d) == "Duration(months=2, days=3, seconds=5, nanoseconds=700000000)" def test_iso_format(self): - self.assertEqual(Duration().iso_format(), "PT0S") - self.assertEqual(Duration(years=1, months=2).iso_format(), "P1Y2M") - self.assertEqual(Duration(years=-1, months=2).iso_format(), "P-10M") - self.assertEqual(Duration(months=-13).iso_format(), "P-1Y-1M") - self.assertEqual(Duration(months=2, days=3, seconds=5.7).iso_format(), "P2M3DT5.7S") - self.assertEqual(Duration(hours=12, minutes=34).iso_format(), "PT12H34M") - self.assertEqual(Duration(seconds=59).iso_format(), "PT59S") - self.assertEqual(Duration(seconds=0.123456789).iso_format(), "PT0.123456789S") - self.assertEqual(Duration(seconds=-0.123456789).iso_format(), "PT-0.123456789S") + assert Duration().iso_format() == "PT0S" + assert Duration(years=1, months=2).iso_format() == "P1Y2M" + assert Duration(years=-1, months=2).iso_format() == "P-10M" + assert Duration(months=-13).iso_format() == "P-1Y-1M" + assert Duration(months=2, days=3, seconds=5.7).iso_format() == "P2M3DT5.7S" + assert Duration(hours=12, minutes=34).iso_format() == "PT12H34M" + assert Duration(seconds=59).iso_format() == "PT59S" + assert Duration(seconds=0.123456789).iso_format() == "PT0.123456789S" + assert Duration(seconds=-0.123456789).iso_format() == "PT-0.123456789S" + + def test_copy(self): + d = Duration(years=1, months=2, days=3, hours=4, minutes=5, seconds=6, + milliseconds=7, microseconds=8, nanoseconds=9) + d2 = copy.copy(d) + assert d is not d2 + assert d == d2 + + def test_deep_copy(self): + d = Duration(years=1, months=2, days=3, hours=4, minutes=5, seconds=6, + milliseconds=7, microseconds=8, nanoseconds=9) + d2 = copy.deepcopy(d) + assert d is not d2 + assert d == d2 def test_from_iso_format(self): - self.assertEqual(Duration(), Duration.from_iso_format("PT0S")) - self.assertEqual(Duration(hours=12, minutes=34, seconds=56.789), - Duration.from_iso_format("PT12H34M56.789S")) - self.assertEqual(Duration(years=1, months=2, days=3), - Duration.from_iso_format("P1Y2M3D")) - self.assertEqual(Duration(years=1, months=2, days=3, hours=12, minutes=34, seconds=56.789), - Duration.from_iso_format("P1Y2M3DT12H34M56.789S")) + assert Duration() == Duration.from_iso_format("PT0S") + assert Duration( + hours=12, minutes=34, seconds=56.789 + ) == Duration.from_iso_format("PT12H34M56.789S") + assert Duration( + years=1, months=2, days=3 + ) == Duration.from_iso_format("P1Y2M3D") + assert Duration( + years=1, months=2, days=3, hours=12, minutes=34, seconds=56.789 + ) == Duration.from_iso_format("P1Y2M3DT12H34M56.789S") + # test for float precision issues + for i in range(500006000, 500010000, 1000): + assert Duration( + years=1, months=2, days=3, hours=12, minutes=34, nanoseconds=i + ) == Duration.from_iso_format("P1Y2M3DT12H34M00.%sS" % str(i)) + assert Duration( + years=1, months=2, days=3, hours=12, minutes=34, nanoseconds=i + ) == Duration.from_iso_format("P1Y2M3DT12H34M00.%sS" % str(i)[:-3]) diff --git a/tests/unit/time/test_hydration.py b/tests/unit/time/test_hydration.py index fbe86a296..06cffa781 100644 --- a/tests/unit/time/test_hydration.py +++ b/tests/unit/time/test_hydration.py @@ -19,6 +19,7 @@ # limitations under the License. +from decimal import Decimal from unittest import TestCase from neo4j.data import DataHydrator @@ -38,4 +39,4 @@ def test_can_hydrate_date_time_structure(self): self.assertEqual(dt.day, 12) self.assertEqual(dt.hour, 11) self.assertEqual(dt.minute, 37) - self.assertEqual(dt.second, 41.474716862) + self.assertEqual(dt.second, Decimal("41.474716862")) diff --git a/tests/unit/time/test_time.py b/tests/unit/time/test_time.py index f6e8dce0a..bb5203f60 100644 --- a/tests/unit/time/test_time.py +++ b/tests/unit/time/test_time.py @@ -20,9 +20,11 @@ from datetime import time -from unittest import TestCase +from decimal import Decimal +import pytest from pytz import ( + build_tzinfo, timezone, FixedOffset, ) @@ -38,130 +40,147 @@ timezone_utc = timezone("UTC") -class TimeTestCase(TestCase): +def seconds_options(seconds, nanoseconds): + yield seconds, nanoseconds + yield seconds + nanoseconds / 1000000000, + + +class TimeTestCase: def test_bad_attribute(self): t = Time(12, 34, 56.789) - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): _ = t.x def test_simple_time(self): t = Time(12, 34, 56.789) - self.assertEqual(t.hour_minute_second, (12, 34, 56.789)) - self.assertEqual(t.ticks, 45296.789) - self.assertEqual(t.hour, 12) - self.assertEqual(t.minute, 34) - self.assertEqual(t.second, 56.789) + assert t.hour_minute_second == (12, 34, 56.789) + assert t.hour_minute_second_nanoseconds == (12, 34, 56, 789000000) + assert t.ticks == 45296.789 + assert t.hour == 12 + assert t.minute == 34 + assert t.second == 56.789 def test_midnight(self): t = Time(0, 0, 0) - self.assertEqual(t.hour_minute_second, (0, 0, 0)) - self.assertEqual(t.ticks, 0) - self.assertEqual(t.hour, 0) - self.assertEqual(t.minute, 0) - self.assertEqual(t.second, 0) + assert t.hour_minute_second == (0, 0, 0) + assert t.ticks == 0 + assert t.hour == 0 + assert t.minute == 0 + assert t.second == 0 def test_nanosecond_precision(self): t = Time(12, 34, 56.789123456) - self.assertEqual(t.hour_minute_second, (12, 34, 56.789123456)) - self.assertEqual(t.ticks, 45296.789123456) - self.assertEqual(t.hour, 12) - self.assertEqual(t.minute, 34) - self.assertEqual(t.second, 56.789123456) + assert t.hour_minute_second == (12, 34, 56.789123456) + assert t.ticks == 45296.789123456 + assert t.hour == 12 + assert t.minute == 34 + assert t.second == 56.789123456 def test_str(self): - t = Time(12, 34, 56.789123456) - self.assertEqual(str(t), "12:34:56.789123456") + t = Time(12, 34, 56, 789123456) + assert str(t) == "12:34:56.789123456" def test_now_without_tz(self): t = Time.now() - self.assertIsInstance(t, Time) + assert isinstance(t, Time) def test_now_with_tz(self): t = Time.now(tz=timezone_us_eastern) - self.assertIsInstance(t, Time) - self.assertEqual(t.tzinfo, timezone_us_eastern) + assert isinstance(t, Time) + assert t.tzinfo == timezone_us_eastern def test_utc_now(self): t = Time.utc_now() - self.assertIsInstance(t, Time) + assert isinstance(t, Time) def test_from_native(self): native = time(12, 34, 56, 789123) t = Time.from_native(native) - self.assertEqual(t.hour, native.hour) - self.assertEqual(t.minute, native.minute) - self.assertEqual(t.second, nano_add(native.second, nano_div(native.microsecond, 1000000))) + assert t.hour == native.hour + assert t.minute == native.minute + assert t.second == nano_add(native.second, nano_div(native.microsecond, 1000000)) def test_to_native(self): t = Time(12, 34, 56.789123456) native = t.to_native() - self.assertEqual(t.hour, native.hour) - self.assertEqual(t.minute, native.minute) - self.assertEqual(56.789123, nano_add(native.second, nano_div(native.microsecond, 1000000))) + assert t.hour == native.hour + assert t.minute == native.minute + assert 56.789123 == nano_add(native.second, nano_div(native.microsecond, 1000000)) def test_iso_format(self): - t = Time(12, 34, 56.789123456) - self.assertEqual("12:34:56.789123456", t.iso_format()) + t = Time(12, 34, 56, 789123456) + assert "12:34:56.789123456" == t.iso_format() def test_iso_format_with_trailing_zeroes(self): - t = Time(12, 34, 56.789) - self.assertEqual("12:34:56.789000000", t.iso_format()) + t = Time(12, 34, 56, 789) + assert "12:34:56.789000000" == t.iso_format() def test_from_iso_format_hour_only(self): expected = Time(12, 0, 0) actual = Time.from_iso_format("12") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_hour_and_minute(self): expected = Time(12, 34, 0) actual = Time.from_iso_format("12:34") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_hour_minute_second(self): expected = Time(12, 34, 56) actual = Time.from_iso_format("12:34:56") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_hour_minute_second_milliseconds(self): - expected = Time(12, 34, 56.123) + expected = Time(12, 34, 56, 123) actual = Time.from_iso_format("12:34:56.123") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_hour_minute_second_microseconds(self): - expected = Time(12, 34, 56.123456) + expected = Time(12, 34, 56, 12345600) actual = Time.from_iso_format("12:34:56.123456") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_hour_minute_second_nanoseconds(self): - expected = Time(12, 34, 56.123456789) + expected = Time(12, 34, 56, 123456789) actual = Time.from_iso_format("12:34:56.123456789") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_with_positive_tz(self): - expected = Time(12, 34, 56.123456789, tzinfo=FixedOffset(754)) + expected = Time(12, 34, 56, 123456789, tzinfo=FixedOffset(754)) actual = Time.from_iso_format("12:34:56.123456789+12:34") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_with_negative_tz(self): - expected = Time(12, 34, 56.123456789, tzinfo=FixedOffset(-754)) + expected = Time(12, 34, 56, 123456789, tzinfo=FixedOffset(-754)) actual = Time.from_iso_format("12:34:56.123456789-12:34") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_with_positive_long_tz(self): - expected = Time(12, 34, 56.123456789, tzinfo=FixedOffset(754)) + expected = Time(12, 34, 56, 123456789, tzinfo=FixedOffset(754)) actual = Time.from_iso_format("12:34:56.123456789+12:34:56.123456") - self.assertEqual(expected, actual) + assert expected == actual def test_from_iso_format_with_negative_long_tz(self): - expected = Time(12, 34, 56.123456789, tzinfo=FixedOffset(-754)) + expected = Time(12, 34, 56, 123456789, tzinfo=FixedOffset(-754)) actual = Time.from_iso_format("12:34:56.123456789-12:34:56.123456") - self.assertEqual(expected, actual) + assert expected == actual + + def test_utc_offset_fixed(self): + expected = Time(12, 34, 56, 123456789, tzinfo=FixedOffset(-754)) + actual = -754 * 60 + assert expected.utc_offset().total_seconds() == actual + def test_utc_offset_variable(self): + expected = Time(12, 34, 56, 123456789, + tzinfo=build_tzinfo() + ) + actual = -754 * 60 + assert expected.utc_offset().total_seconds() == actual def test_iso_format_with_time_zone_case_1(): # python -m pytest tests/unit/time/test_time.py -s -v -k test_iso_format_with_time_zone_case_1 - expected = Time(7, 54, 2.129790999, tzinfo=timezone_utc) + expected = Time(7, 54, 2, 129790999, tzinfo=timezone_utc) assert expected.iso_format() == "07:54:02.129790999+00:00" assert expected.tzinfo == FixedOffset(0) actual = Time.from_iso_format("07:54:02.129790999+00:00") @@ -177,7 +196,7 @@ def test_iso_format_with_time_zone_case_2(): def test_to_native_case_1(): # python -m pytest tests/unit/time/test_time.py -s -v -k test_to_native_case_1 - t = Time(12, 34, 56.789123456) + t = Time(12, 34, 56, 789123456) native = t.to_native() assert native.hour == t.hour assert native.minute == t.minute @@ -188,7 +207,7 @@ def test_to_native_case_1(): def test_to_native_case_2(): # python -m pytest tests/unit/time/test_time.py -s -v -k test_to_native_case_2 - t = Time(12, 34, 56.789123456, tzinfo=timezone_utc) + t = Time(12, 34, 56, 789123456, tzinfo=timezone_utc) native = t.to_native() assert native.hour == t.hour assert native.minute == t.minute @@ -203,7 +222,7 @@ def test_from_native_case_1(): t = Time.from_native(native) assert t.hour == native.hour assert t.minute == native.minute - assert t.second == nano_add(native.second, nano_div(native.microsecond, 1000000)) + assert t.second == Decimal(native.microsecond) / 1000000 + native.second assert t.tzinfo is None @@ -213,5 +232,5 @@ def test_from_native_case_2(): t = Time.from_native(native) assert t.hour == native.hour assert t.minute == native.minute - assert t.second == nano_add(native.second, nano_div(native.microsecond, 1000000)) + assert t.second == Decimal(native.microsecond) / 1000000 + native.second assert t.tzinfo == FixedOffset(0)