Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 42 additions & 11 deletions neo4j/time/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,42 @@
a number of utility functions.
"""

from datetime import timedelta, date, time, datetime
from datetime import (
timedelta,
date,
time,
datetime,
)
from functools import total_ordering
from re import compile as re_compile
from time import gmtime, mktime, struct_time

from neo4j.time.arithmetic import (nano_add, nano_sub, nano_mul, nano_div,
nano_mod, nano_divmod,
symmetric_divmod, round_half_to_even)
from neo4j.time.metaclasses import DateType, TimeType, DateTimeType
from time import (
gmtime,
mktime,
struct_time,
)
from decimal import Decimal

from neo4j.time.arithmetic import (
nano_add,
nano_sub,
nano_mul,
nano_div,
nano_mod,
nano_divmod,
symmetric_divmod,
round_half_to_even,
)
from neo4j.time.metaclasses import (
DateType,
TimeType,
DateTimeType,
)

import logging
from neo4j.debug import watch
watch("neo4j")

log = logging.getLogger("neo4j")


MIN_INT64 = -(2 ** 63)
Expand All @@ -49,6 +76,8 @@
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


def _is_leap_year(year):
if year % 4 != 0:
Expand Down Expand Up @@ -1118,8 +1147,9 @@ def tzname(self):
return self.tzinfo.tzname(self)

def to_clock_time(self):
seconds, nanoseconds = nano_divmod(self.ticks, 1)
return ClockTime(seconds, 1000000000 * nanoseconds)
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)

def to_native(self):
""" Convert to a native Python `datetime.time` value.
Expand Down Expand Up @@ -1437,8 +1467,9 @@ 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)
return ClockTime(total_seconds + seconds, 1000000000 * nanoseconds)
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)

def to_native(self):
""" Convert to a native Python `datetime.datetime` value.
Expand Down
4 changes: 3 additions & 1 deletion neo4j/time/hydration.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ def seconds_and_nanoseconds(dt):
if isinstance(dt, datetime):
dt = DateTime.from_native(dt)
zone_epoch = DateTime(1970, 1, 1, tzinfo=dt.tzinfo)
t = dt.to_clock_time() - zone_epoch.to_clock_time()
dt_clock_time = dt.to_clock_time()
zone_epoch_clock_time = zone_epoch.to_clock_time()
t = dt_clock_time - zone_epoch_clock_time
return t.seconds, t.nanoseconds

tz = value.tzinfo
Expand Down
73 changes: 73 additions & 0 deletions tests/integration/test_temporal_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@


import pytest
import datetime

from pytz import (
FixedOffset,
timezone,
Expand Down Expand Up @@ -330,3 +332,74 @@ def test_nanosecond_resolution_duration_output(cypher_eval):
assert isinstance(value, Duration)
assert value == Duration(years=1, months=2, days=3, hours=4,
minutes=5, seconds=6.789123456)


def test_datetime_parameter_case1(session):
# python -m pytest tests/integration/test_temporal_types.py -s -v -k test_datetime_parameter_case1
dt1 = session.run("RETURN datetime('2019-10-30T07:54:02.129790001+00:00')").single().value()
assert isinstance(dt1, DateTime)

dt2 = session.run("RETURN $date_time", date_time=dt1).single().value()
assert isinstance(dt2, DateTime)

assert dt1 == dt2


def test_datetime_parameter_case2(session):
# python -m pytest tests/integration/test_temporal_types.py -s -v -k test_datetime_parameter_case2
dt1 = session.run("RETURN datetime('2019-10-30T07:54:02.129790999[UTC]')").single().value()
assert isinstance(dt1, DateTime)
assert dt1.iso_format() == "2019-10-30T07:54:02.129790999+00:00"

dt2 = session.run("RETURN $date_time", date_time=dt1).single().value()
assert isinstance(dt2, DateTime)

assert dt1 == dt2


def test_datetime_parameter_case3(session):
# python -m pytest tests/integration/test_temporal_types.py -s -v -k test_datetime_parameter_case1
dt1 = session.run("RETURN datetime('2019-10-30T07:54:02.129790+00:00')").single().value()
assert isinstance(dt1, DateTime)

dt2 = session.run("RETURN $date_time", date_time=dt1).single().value()
assert isinstance(dt2, DateTime)

assert dt1 == dt2


def test_time_parameter_case1(session):
# python -m pytest tests/integration/test_temporal_types.py -s -v -k test_time_parameter_case1
t1 = session.run("RETURN time('07:54:02.129790001+00:00')").single().value()
assert isinstance(t1, Time)

t2 = session.run("RETURN $time", time=t1).single().value()
assert isinstance(t2, Time)

assert t1 == t2


def test_time_parameter_case2(session):
# python -m pytest tests/integration/test_temporal_types.py -s -v -k test_time_parameter_case2
t1 = session.run("RETURN time('07:54:02.129790999+00:00')").single().value()
assert isinstance(t1, Time)
# assert t1.iso_format() == "07:54:02.129790999+00:00" # TODO: Broken, does not show time_zone_delta +00:00
time_zone_delta = t1.utc_offset()
assert isinstance(time_zone_delta, datetime.timedelta)
assert time_zone_delta == datetime.timedelta(0)

t2 = session.run("RETURN $time", time=t1).single().value()
assert isinstance(t2, Time)

assert t1 == t2


def test_time_parameter_case3(session):
# python -m pytest tests/integration/test_temporal_types.py -s -v -k test_time_parameter_case3
t1 = session.run("RETURN time('07:54:02.129790+00:00')").single().value()
assert isinstance(t1, Time)

t2 = session.run("RETURN $time", time=t1).single().value()
assert isinstance(t2, Time)

assert t1 == t2
72 changes: 54 additions & 18 deletions tests/unit/time/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,44 @@
# limitations under the License.


from datetime import datetime, timedelta
from unittest import TestCase

from pytz import timezone, FixedOffset

from neo4j.time import DateTime, MIN_YEAR, MAX_YEAR, Duration
from neo4j.time.arithmetic import nano_add, nano_div
from neo4j.time.clock_implementations import Clock, ClockTime


eastern = timezone("US/Eastern")
from datetime import (
datetime,
timedelta,
)
from pytz import (
timezone,
FixedOffset,
)

from neo4j.time import (
DateTime,
MIN_YEAR,
MAX_YEAR,
Duration,
)
from neo4j.time.arithmetic import (
nano_add,
nano_div,
)
from neo4j.time.clock_implementations import (
Clock,
ClockTime,
)
from neo4j.time.hydration import (
hydrate_date,
dehydrate_date,
hydrate_time,
dehydrate_time,
hydrate_datetime,
dehydrate_datetime,
hydrate_duration,
dehydrate_duration,
dehydrate_timedelta,
)

timezone_us_eastern = timezone("US/Eastern")
timezone_utc = timezone("UTC")


class FixedClock(Clock):
Expand Down Expand Up @@ -132,7 +159,7 @@ def test_now_without_tz(self):
self.assertIsNone(t.tzinfo)

def test_now_with_tz(self):
t = DateTime.now(eastern)
t = DateTime.now(timezone_us_eastern)
self.assertEqual(t.year, 1970)
self.assertEqual(t.month, 1)
self.assertEqual(t.day, 1)
Expand Down Expand Up @@ -168,7 +195,7 @@ def test_from_overflowing_timestamp(self):
_ = DateTime.from_timestamp(999999999999999999)

def test_from_timestamp_with_tz(self):
t = DateTime.from_timestamp(0, eastern)
t = DateTime.from_timestamp(0, timezone_us_eastern)
self.assertEqual(t.year, 1969)
self.assertEqual(t.month, 12)
self.assertEqual(t.day, 31)
Expand Down Expand Up @@ -215,13 +242,13 @@ def test_subtract_native_datetime_2(self):
self.assertEqual(t, timedelta(days=65, hours=23, seconds=17.914390409))

def test_normalization(self):
ndt1 = eastern.normalize(DateTime(2018, 4, 27, 23, 0, 17, tzinfo=eastern))
ndt2 = eastern.normalize(datetime(2018, 4, 27, 23, 0, 17, tzinfo=eastern))
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)

def test_localization(self):
ldt1 = eastern.localize(datetime(2018, 4, 27, 23, 0, 17))
ldt2 = eastern.localize(DateTime(2018, 4, 27, 23, 0, 17))
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)

def test_from_native(self):
Expand Down Expand Up @@ -253,11 +280,11 @@ def test_iso_format_with_trailing_zeroes(self):
self.assertEqual("2018-10-01T12:34:56.789000000", dt.iso_format())

def test_iso_format_with_tz(self):
dt = eastern.localize(DateTime(2018, 10, 1, 12, 34, 56.789123456))
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())

def test_iso_format_with_tz_and_trailing_zeroes(self):
dt = eastern.localize(DateTime(2018, 10, 1, 12, 34, 56.789))
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())

def test_from_iso_format_hour_only(self):
Expand Down Expand Up @@ -309,3 +336,12 @@ def test_from_iso_format_with_negative_long_tz(self):
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)


def test_potential_rounding_error():
# python -m pytest tests/unit/time/test_datetime.py -s -v -k test_potential_rounding_error
expected = DateTime(2019, 10, 30, 7, 54, 2.129790999, tzinfo=timezone_utc)
assert expected.iso_format() == "2019-10-30T07:54:02.129790999+00:00"

actual = DateTime.from_iso_format("2019-10-30T07:54:02.129790999+00:00")
assert expected == actual