Skip to content

Commit

Permalink
msgpack: support datetime interval arithmetic
Browse files Browse the repository at this point in the history
Support datetime and interval arithmetic with the same rules as in
Tarantool [1].

Valid operations:
- tarantool.Datetime +  tarantool.Interval = tarantool.Datetime
- tarantool.Datetime -  tarantool.Datetime = tarantool.Interval
- tarantool.Interval +  tarantool.Interval = tarantool.Interval
- tarantool.Interval -  tarantool.Interval = tarantool.Interval

1. https://github.com/tarantool/tarantool/wiki/Datetime-Internals#interval-arithmetic

Closes #229
  • Loading branch information
DifferentialOrange committed Sep 9, 2022
1 parent e50aed2 commit 1728868
Show file tree
Hide file tree
Showing 5 changed files with 402 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
is raised in this case.

- Datetime interval type support and tarantool.Interval type (#229).
- Datetime interval arithmetic support (#229).

### Changed
- Bump msgpack requirement to 1.0.4 (PR #223).
Expand Down
56 changes: 56 additions & 0 deletions tarantool/msgpack_ext/types/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import tarantool.msgpack_ext.types.timezones as tt_timezones
from tarantool.error import MsgpackError, MsgpackWarning, warn

from tarantool.msgpack_ext.types.interval import Interval, Adjust

# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
#
# The datetime MessagePack representation looks like this:
Expand Down Expand Up @@ -45,8 +47,10 @@
BYTEORDER = 'little'

NSEC_IN_SEC = 1000000000
NSEC_IN_MKSEC = 1000
SEC_IN_MIN = 60
MIN_IN_DAY = 60 * 24
MONTH_IN_YEAR = 12


def get_bytes_as_int(data, cursor, size):
Expand Down Expand Up @@ -209,6 +213,58 @@ def __init__(self, *args, tarantool_tzindex=None, **kwargs):
self._tzoffset = tzoffset
self._tzindex = tzindex

def __add__(self, other):
if not isinstance(other, Interval):
raise TypeError(f"unsupported operand type(s) for -: '{type(self)}' and '{type(other)}'")

self_ts = self._timestamp

# https://github.com/tarantool/tarantool/wiki/Datetime-Internals#date-adjustions-and-leap-years
months = other.year * MONTH_IN_YEAR + other.month

res = self_ts + pandas.DateOffset(months = months)

# pandas.DateOffset works like Adjust.Limit
if other.adjust == Adjust.Excess:
if self_ts.day > res.day:
res = res + pandas.DateOffset(days = self_ts.day - res.day)
elif other.adjust == Adjust.Last:
if self_ts.is_month_end:
# day replaces days
res = res.replace(day = res.days_in_month)

res = res + pandas.Timedelta(weeks = other.week,
days = other.day,
hours = other.hour,
minutes = other.minute,
seconds = other.second,
nanoseconds = other.nanosecond)

if self._tzindex != 0:
return Datetime(res, tarantool_tzindex=self._tzindex)
else:
return Datetime(res)

def __sub__(self, other):
if not isinstance(other, Datetime):
raise TypeError(f"unsupported operand type(s) for -: '{type(self)}' and '{type(other)}'")

self_ts = self._timestamp
other_ts = other._timestamp

self_nsec = self_ts.microsecond * NSEC_IN_MKSEC + self_ts.nanosecond
other_nsec = other_ts.microsecond * NSEC_IN_MKSEC + other_ts.nanosecond

return Interval(
year = self_ts.year - other_ts.year,
month = self_ts.month - other_ts.month,
day = self_ts.day - other_ts.day,
hour = self_ts.hour - other_ts.hour,
minute = self_ts.minute - other_ts.minute,
second = self_ts.second - other_ts.second,
nanosecond = self_nsec - other_nsec,
)

def __eq__(self, other):
if isinstance(other, Datetime):
return self._timestamp == other._timestamp
Expand Down
64 changes: 64 additions & 0 deletions tarantool/msgpack_ext/types/interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,70 @@ def __init__(self, data=None, *, year=0, month=0, week=0,
self.nanosecond = nanosecond
self.adjust = adjust

def __add__(self, other):
if not isinstance(other, Interval):
raise TypeError(f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'")

# Tarantool saves adjust of the first argument
#
# Tarantool 2.10.1-0-g482d91c66
#
# tarantool> dt1 = datetime.interval.new{year = 2, adjust='last'}
# ---
# ...
#
# tarantool> dt2 = datetime.interval.new{year = 1, adjust='excess'}
# ---
# ...
#
# tarantool> (dt1 + dt2).adjust
# ---
# - 'cdata<enum 112>: 2'
# ...

return Interval(
year = self.year + other.year,
month = self.month + other.month,
day = self.day + other.day,
hour = self.hour + other.hour,
minute = self.minute + other.minute,
second = self.second + other.second,
nanosecond = self.nanosecond + other.nanosecond,
adjust = self.adjust,
)

def __sub__(self, other):
if not isinstance(other, Interval):
raise TypeError(f"unsupported operand type(s) for -: '{type(self)}' and '{type(other)}'")

# Tarantool saves adjust of the first argument
#
# Tarantool 2.10.1-0-g482d91c66
#
# tarantool> dt1 = datetime.interval.new{year = 2, adjust='last'}
# ---
# ...
#
# tarantool> dt2 = datetime.interval.new{year = 1, adjust='excess'}
# ---
# ...
#
# tarantool> (dt1 - dt2).adjust
# ---
# - 'cdata<enum 112>: 2'
# ...

return Interval(
year = self.year - other.year,
month = self.month - other.month,
day = self.day - other.day,
hour = self.hour - other.hour,
minute = self.minute - other.minute,
second = self.second - other.second,
nanosecond = self.nanosecond - other.nanosecond,
adjust = self.adjust,
)

def __eq__(self, other):
if not isinstance(other, Interval):
return False
Expand Down
4 changes: 3 additions & 1 deletion test/suites/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@
from .test_uuid import TestSuite_UUID
from .test_datetime import TestSuite_Datetime
from .test_interval import TestSuite_Interval
from .test_datetime_arithmetic import TestSuite_DatetimeArithmetic

test_cases = (TestSuite_Schema_UnicodeConnection,
TestSuite_Schema_BinaryConnection,
TestSuite_Request, TestSuite_Protocol, TestSuite_Reconnect,
TestSuite_Mesh, TestSuite_Execute, TestSuite_DBAPI,
TestSuite_Encoding, TestSuite_Pool, TestSuite_Ssl,
TestSuite_Decimal, TestSuite_UUID, TestSuite_Datetime,
TestSuite_Interval)
TestSuite_Interval, TestSuite_DatetimeArithmetic)


def load_tests(loader, tests, pattern):
suite = unittest.TestSuite()
Expand Down
Loading

0 comments on commit 1728868

Please sign in to comment.