Skip to content

Commit a68067b

Browse files
msgpack: support datetime interval arithmetic
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
1 parent e50aed2 commit a68067b

File tree

4 files changed

+401
-1
lines changed

4 files changed

+401
-1
lines changed

tarantool/msgpack_ext/types/datetime.py

+56
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import tarantool.msgpack_ext.types.timezones as tt_timezones
77
from tarantool.error import MsgpackError, MsgpackWarning, warn
88

9+
from tarantool.msgpack_ext.types.interval import Interval, Adjust
10+
911
# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
1012
#
1113
# The datetime MessagePack representation looks like this:
@@ -45,8 +47,10 @@
4547
BYTEORDER = 'little'
4648

4749
NSEC_IN_SEC = 1000000000
50+
NSEC_IN_MKSEC = 1000
4851
SEC_IN_MIN = 60
4952
MIN_IN_DAY = 60 * 24
53+
MONTH_IN_YEAR = 12
5054

5155

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

216+
def __add__(self, other):
217+
if not isinstance(other, Interval):
218+
raise TypeError(f"unsupported operand type(s) for -: '{type(self)}' and '{type(other)}'")
219+
220+
self_ts = self._timestamp
221+
222+
# https://github.com/tarantool/tarantool/wiki/Datetime-Internals#date-adjustions-and-leap-years
223+
months = other.year * MONTH_IN_YEAR + other.month
224+
225+
res = self_ts + pandas.DateOffset(months = months)
226+
227+
# pandas.DateOffset works like Adjust.Limit
228+
if other.adjust == Adjust.Excess:
229+
if self_ts.day > res.day:
230+
res = res + pandas.DateOffset(days = self_ts.day - res.day)
231+
elif other.adjust == Adjust.Last:
232+
if self_ts.is_month_end:
233+
# day replaces days
234+
res = res.replace(day = res.days_in_month)
235+
236+
res = res + pandas.Timedelta(weeks = other.week,
237+
days = other.day,
238+
hours = other.hour,
239+
minutes = other.minute,
240+
seconds = other.second,
241+
nanoseconds = other.nanosecond)
242+
243+
if self._tzindex != 0:
244+
return Datetime(res, tarantool_tzindex=self._tzindex)
245+
else:
246+
return Datetime(res)
247+
248+
def __sub__(self, other):
249+
if not isinstance(other, Datetime):
250+
raise TypeError(f"unsupported operand type(s) for -: '{type(self)}' and '{type(other)}'")
251+
252+
self_ts = self._timestamp
253+
other_ts = other._timestamp
254+
255+
self_nsec = self_ts.microsecond * NSEC_IN_MKSEC + self_ts.nanosecond
256+
other_nsec = other_ts.microsecond * NSEC_IN_MKSEC + other_ts.nanosecond
257+
258+
return Interval(
259+
year = self_ts.year - other_ts.year,
260+
month = self_ts.month - other_ts.month,
261+
day = self_ts.day - other_ts.day,
262+
hour = self_ts.hour - other_ts.hour,
263+
minute = self_ts.minute - other_ts.minute,
264+
second = self_ts.second - other_ts.second,
265+
nanosecond = self_nsec - other_nsec,
266+
)
267+
212268
def __eq__(self, other):
213269
if isinstance(other, Datetime):
214270
return self._timestamp == other._timestamp

tarantool/msgpack_ext/types/interval.py

+64
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,70 @@ def __init__(self, data=None, *, year=0, month=0, week=0,
102102
self.nanosecond = nanosecond
103103
self.adjust = adjust
104104

105+
def __add__(self, other):
106+
if not isinstance(other, Interval):
107+
raise TypeError(f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'")
108+
109+
# Tarantool saves adjust of the first argument
110+
#
111+
# Tarantool 2.10.1-0-g482d91c66
112+
#
113+
# tarantool> dt1 = datetime.interval.new{year = 2, adjust='last'}
114+
# ---
115+
# ...
116+
#
117+
# tarantool> dt2 = datetime.interval.new{year = 1, adjust='excess'}
118+
# ---
119+
# ...
120+
#
121+
# tarantool> (dt1 + dt2).adjust
122+
# ---
123+
# - 'cdata<enum 112>: 2'
124+
# ...
125+
126+
return Interval(
127+
year = self.year + other.year,
128+
month = self.month + other.month,
129+
day = self.day + other.day,
130+
hour = self.hour + other.hour,
131+
minute = self.minute + other.minute,
132+
second = self.second + other.second,
133+
nanosecond = self.nanosecond + other.nanosecond,
134+
adjust = self.adjust,
135+
)
136+
137+
def __sub__(self, other):
138+
if not isinstance(other, Interval):
139+
raise TypeError(f"unsupported operand type(s) for -: '{type(self)}' and '{type(other)}'")
140+
141+
# Tarantool saves adjust of the first argument
142+
#
143+
# Tarantool 2.10.1-0-g482d91c66
144+
#
145+
# tarantool> dt1 = datetime.interval.new{year = 2, adjust='last'}
146+
# ---
147+
# ...
148+
#
149+
# tarantool> dt2 = datetime.interval.new{year = 1, adjust='excess'}
150+
# ---
151+
# ...
152+
#
153+
# tarantool> (dt1 - dt2).adjust
154+
# ---
155+
# - 'cdata<enum 112>: 2'
156+
# ...
157+
158+
return Interval(
159+
year = self.year - other.year,
160+
month = self.month - other.month,
161+
day = self.day - other.day,
162+
hour = self.hour - other.hour,
163+
minute = self.minute - other.minute,
164+
second = self.second - other.second,
165+
nanosecond = self.nanosecond - other.nanosecond,
166+
adjust = self.adjust,
167+
)
168+
105169
def __eq__(self, other):
106170
if not isinstance(other, Interval):
107171
return False

test/suites/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@
1919
from .test_uuid import TestSuite_UUID
2020
from .test_datetime import TestSuite_Datetime
2121
from .test_interval import TestSuite_Interval
22+
from .test_datetime_arithmetic import TestSuite_DatetimeArithmetic
2223

2324
test_cases = (TestSuite_Schema_UnicodeConnection,
2425
TestSuite_Schema_BinaryConnection,
2526
TestSuite_Request, TestSuite_Protocol, TestSuite_Reconnect,
2627
TestSuite_Mesh, TestSuite_Execute, TestSuite_DBAPI,
2728
TestSuite_Encoding, TestSuite_Pool, TestSuite_Ssl,
2829
TestSuite_Decimal, TestSuite_UUID, TestSuite_Datetime,
29-
TestSuite_Interval)
30+
TestSuite_Interval, TestSuite_DatetimeArithmetic)
31+
3032

3133
def load_tests(loader, tests, pattern):
3234
suite = unittest.TestSuite()

0 commit comments

Comments
 (0)