Skip to content

Commit bd40cb1

Browse files
committed
Fix comparison operators in neo4j.time library
1 parent 9553730 commit bd40cb1

File tree

4 files changed

+822
-83
lines changed

4 files changed

+822
-83
lines changed

neo4j/time/__init__.py

Lines changed: 136 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,73 +1657,81 @@ def tzinfo(self):
16571657

16581658
# OPERATIONS #
16591659

1660+
def _get_both_normalized_ticks(self, other, strict=True):
1661+
if (isinstance(other, (time, Time))
1662+
and ((self.utc_offset() is None)
1663+
^ (other.utcoffset() is None))):
1664+
if strict:
1665+
raise TypeError("can't compare offset-naive and offset-aware "
1666+
"times")
1667+
else:
1668+
return None, None
1669+
if isinstance(other, Time):
1670+
other_ticks = other.__ticks
1671+
elif isinstance(other, time):
1672+
other_ticks = int(3600000000000 * other.hour
1673+
+ 60000000000 * other.minute
1674+
+ NANO_SECONDS * other.second
1675+
+ 1000 * other.microsecond)
1676+
else:
1677+
return None, None
1678+
utc_offset = other.utcoffset()
1679+
if utc_offset is not None:
1680+
other_ticks -= utc_offset.total_seconds() * NANO_SECONDS
1681+
self_ticks = self.__ticks
1682+
utc_offset = self.utc_offset()
1683+
if utc_offset is not None:
1684+
self_ticks -= utc_offset.total_seconds() * NANO_SECONDS
1685+
return self_ticks, other_ticks
1686+
16601687
def __hash__(self):
16611688
""""""
1662-
return hash(self.__ticks) ^ hash(self.tzinfo)
1689+
if self.__nanosecond % 1000 == 0:
1690+
return hash(self.to_native())
1691+
self_ticks = self.__ticks
1692+
if self.utc_offset() is not None:
1693+
self_ticks -= self.utc_offset().total_seconds() * NANO_SECONDS
1694+
return hash(self_ticks)
16631695

16641696
def __eq__(self, other):
16651697
"""`==` comparison with :class:`.Time` or :class:`datetime.time`."""
1666-
if isinstance(other, Time):
1667-
return self.__ticks == other.__ticks and self.tzinfo == other.tzinfo
1668-
if isinstance(other, time):
1669-
other_ticks = (3600000000000 * other.hour
1670-
+ 60000000000 * other.minute
1671-
+ NANO_SECONDS * other.second
1672-
+ 1000 * other.microsecond)
1673-
return self.ticks == other_ticks and self.tzinfo == other.tzinfo
1674-
return False
1698+
self_ticks, other_ticks = self._get_both_normalized_ticks(other,
1699+
strict=False)
1700+
if self_ticks is None:
1701+
return False
1702+
return self_ticks == other_ticks
16751703

16761704
def __ne__(self, other):
16771705
"""`!=` comparison with :class:`.Time` or :class:`datetime.time`."""
16781706
return not self.__eq__(other)
16791707

16801708
def __lt__(self, other):
16811709
"""`<` comparison with :class:`.Time` or :class:`datetime.time`."""
1682-
if isinstance(other, Time):
1683-
return (self.tzinfo == other.tzinfo
1684-
and self.ticks < other.ticks)
1685-
if isinstance(other, time):
1686-
if self.tzinfo != other.tzinfo:
1687-
return False
1688-
other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1689-
return self.ticks < other_ticks
1690-
return NotImplemented
1710+
self_ticks, other_ticks = self._get_both_normalized_ticks(other)
1711+
if self_ticks is None:
1712+
return NotImplemented
1713+
return self_ticks < other_ticks
16911714

16921715
def __le__(self, other):
16931716
"""`<=` comparison with :class:`.Time` or :class:`datetime.time`."""
1694-
if isinstance(other, Time):
1695-
return (self.tzinfo == other.tzinfo
1696-
and self.ticks <= other.ticks)
1697-
if isinstance(other, time):
1698-
if self.tzinfo != other.tzinfo:
1699-
return False
1700-
other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1701-
return self.ticks <= other_ticks
1702-
return NotImplemented
1717+
self_ticks, other_ticks = self._get_both_normalized_ticks(other)
1718+
if self_ticks is None:
1719+
return NotImplemented
1720+
return self_ticks <= other_ticks
17031721

17041722
def __ge__(self, other):
17051723
"""`>=` comparison with :class:`.Time` or :class:`datetime.time`."""
1706-
if isinstance(other, Time):
1707-
return (self.tzinfo == other.tzinfo
1708-
and self.ticks >= other.ticks)
1709-
if isinstance(other, time):
1710-
if self.tzinfo != other.tzinfo:
1711-
return False
1712-
other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1713-
return self.ticks >= other_ticks
1714-
return NotImplemented
1724+
self_ticks, other_ticks = self._get_both_normalized_ticks(other)
1725+
if self_ticks is None:
1726+
return NotImplemented
1727+
return self_ticks >= other_ticks
17151728

17161729
def __gt__(self, other):
17171730
"""`>` comparison with :class:`.Time` or :class:`datetime.time`."""
1718-
if isinstance(other, Time):
1719-
return (self.tzinfo == other.tzinfo
1720-
and self.ticks >= other.ticks)
1721-
if isinstance(other, time):
1722-
if self.tzinfo != other.tzinfo:
1723-
return False
1724-
other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1725-
return self.ticks >= other_ticks
1726-
return NotImplemented
1731+
self_ticks, other_ticks = self._get_both_normalized_ticks(other)
1732+
if self_ticks is None:
1733+
return NotImplemented
1734+
return self_ticks > other_ticks
17271735

17281736
def __copy__(self):
17291737
return self.__new(self.__ticks, self.__hour, self.__minute,
@@ -1757,6 +1765,21 @@ def replace(self, **kwargs):
17571765
nanosecond=kwargs.get("nanosecond", self.__nanosecond),
17581766
tzinfo=kwargs.get("tzinfo", self.__tzinfo))
17591767

1768+
def _utc_offset(self, dt=None):
1769+
if self.tzinfo is None:
1770+
return None
1771+
value = self.tzinfo.utcoffset(dt)
1772+
if value is None:
1773+
return None
1774+
if isinstance(value, timedelta):
1775+
s = value.total_seconds()
1776+
if not (-86400 < s < 86400):
1777+
raise ValueError("utcoffset must be less than a day")
1778+
if s % 60 != 0 or value.microseconds != 0:
1779+
raise ValueError("utcoffset must be a whole number of minutes")
1780+
return value
1781+
raise TypeError("utcoffset must be a timedelta")
1782+
17601783
def utc_offset(self):
17611784
"""Return the UTC offset of this time.
17621785
@@ -1770,19 +1793,7 @@ def utc_offset(self):
17701793
:raises TypeError: if `self.tzinfo.utcoffset(self)` does return anything but
17711794
None or a :class:`datetime.timedelta`.
17721795
"""
1773-
if self.tzinfo is None:
1774-
return None
1775-
value = self.tzinfo.utcoffset(self)
1776-
if value is None:
1777-
return None
1778-
if isinstance(value, timedelta):
1779-
s = value.total_seconds()
1780-
if not (-86400 < s < 86400):
1781-
raise ValueError("utcoffset must be less than a day")
1782-
if s % 60 != 0 or value.microseconds != 0:
1783-
raise ValueError("utcoffset must be a whole number of minutes")
1784-
return value
1785-
raise TypeError("utcoffset must be a timedelta")
1796+
return self._utc_offset()
17861797

17871798
def dst(self):
17881799
"""Get the daylight saving time adjustment (DST).
@@ -2194,17 +2205,52 @@ def hour_minute_second_nanosecond(self):
21942205

21952206
# OPERATIONS #
21962207

2208+
def _get_both_normalized(self, other, strict=True):
2209+
if (isinstance(other, (datetime, DateTime))
2210+
and ((self.utc_offset() is None)
2211+
^ (other.utcoffset() is None))):
2212+
if strict:
2213+
raise TypeError("can't compare offset-naive and offset-aware "
2214+
"datetimes")
2215+
else:
2216+
return None, None
2217+
self_norm = self
2218+
utc_offset = self.utc_offset()
2219+
if utc_offset is not None:
2220+
self_norm -= utc_offset
2221+
self_norm = self_norm.replace(tzinfo=None)
2222+
other_norm = other
2223+
if isinstance(other, (datetime, DateTime)):
2224+
utc_offset = other.utcoffset()
2225+
if utc_offset is not None:
2226+
other_norm -= utc_offset
2227+
other_norm = other_norm.replace(tzinfo=None)
2228+
else:
2229+
return None, None
2230+
return self_norm, other_norm
2231+
21972232
def __hash__(self):
21982233
""""""
2199-
return hash(self.date()) ^ hash(self.time())
2234+
if self.nanosecond % 1000 == 0:
2235+
return hash(self.to_native())
2236+
self_norm = self
2237+
utc_offset = self.utc_offset()
2238+
if utc_offset is not None:
2239+
self_norm -= utc_offset
2240+
return hash(self_norm.date()) ^ hash(self_norm.time())
22002241

22012242
def __eq__(self, other):
22022243
"""
22032244
`==` comparison with :class:`.DateTime` or :class:`datetime.datetime`.
22042245
"""
2205-
if isinstance(other, (DateTime, datetime)):
2246+
if not isinstance(other, (datetime, DateTime)):
2247+
return NotImplemented
2248+
if self.utc_offset() == other.utcoffset():
22062249
return self.date() == other.date() and self.time() == other.time()
2207-
return False
2250+
self_norm, other_norm = self._get_both_normalized(other, strict=False)
2251+
if self_norm is None:
2252+
return False
2253+
return self_norm == other_norm
22082254

22092255
def __ne__(self, other):
22102256
"""
@@ -2216,45 +2262,55 @@ def __lt__(self, other):
22162262
"""
22172263
`<` comparison with :class:`.DateTime` or :class:`datetime.datetime`.
22182264
"""
2219-
if isinstance(other, (DateTime, datetime)):
2265+
if not isinstance(other, (datetime, DateTime)):
2266+
return NotImplemented
2267+
if self.utc_offset() == other.utcoffset():
22202268
if self.date() == other.date():
22212269
return self.time() < other.time()
2222-
else:
2223-
return self.date() < other.date()
2224-
return NotImplemented
2270+
return self.date() < other.date()
2271+
self_norm, other_norm = self._get_both_normalized(other)
2272+
return (self_norm.date() < other_norm.date()
2273+
or self_norm.time() < other_norm.time())
22252274

22262275
def __le__(self, other):
22272276
"""
22282277
`<=` comparison with :class:`.DateTime` or :class:`datetime.datetime`.
22292278
"""
2230-
if isinstance(other, (DateTime, datetime)):
2279+
if not isinstance(other, (datetime, DateTime)):
2280+
return NotImplemented
2281+
if self.utc_offset() == other.utcoffset():
22312282
if self.date() == other.date():
22322283
return self.time() <= other.time()
2233-
else:
2234-
return self.date() < other.date()
2235-
return NotImplemented
2284+
return self.date() <= other.date()
2285+
self_norm, other_norm = self._get_both_normalized(other)
2286+
return self_norm <= other_norm
22362287

22372288
def __ge__(self, other):
22382289
"""
22392290
`>=` comparison with :class:`.DateTime` or :class:`datetime.datetime`.
22402291
"""
2241-
if isinstance(other, (DateTime, datetime)):
2292+
if not isinstance(other, (datetime, DateTime)):
2293+
return NotImplemented
2294+
if self.utc_offset() == other.utcoffset():
22422295
if self.date() == other.date():
22432296
return self.time() >= other.time()
2244-
else:
2245-
return self.date() > other.date()
2246-
return NotImplemented
2297+
return self.date() >= other.date()
2298+
self_norm, other_norm = self._get_both_normalized(other)
2299+
return self_norm >= other_norm
22472300

22482301
def __gt__(self, other):
22492302
"""
22502303
`>` comparison with :class:`.DateTime` or :class:`datetime.datetime`.
22512304
"""
2252-
if isinstance(other, (DateTime, datetime)):
2305+
if not isinstance(other, (datetime, DateTime)):
2306+
return NotImplemented
2307+
if self.utc_offset() == other.utcoffset():
22532308
if self.date() == other.date():
22542309
return self.time() > other.time()
2255-
else:
2256-
return self.date() > other.date()
2257-
return NotImplemented
2310+
return self.date() > other.date()
2311+
self_norm, other_norm = self._get_both_normalized(other)
2312+
return (self_norm.date() > other_norm.date()
2313+
or self_norm.time() > other_norm.time())
22582314

22592315
def __add__(self, other):
22602316
"""Add a :class:`datetime.timedelta`.
@@ -2358,7 +2414,7 @@ def as_timezone(self, tz):
23582414
"""
23592415
if self.tzinfo is None:
23602416
return self
2361-
utc = (self - self.utcoffset()).replace(tzinfo=tz)
2417+
utc = (self - self.utc_offset()).replace(tzinfo=tz)
23622418
return tz.fromutc(utc)
23632419

23642420
def utc_offset(self):
@@ -2367,7 +2423,7 @@ def utc_offset(self):
23672423
See :meth:`.Time.utc_offset`.
23682424
"""
23692425

2370-
return self.__time.utc_offset()
2426+
return self.__time._utc_offset(self)
23712427

23722428
def dst(self):
23732429
"""Get the daylight saving time adjustment (DST).

0 commit comments

Comments
 (0)