Skip to content

Commit 0ed5039

Browse files
msgpack: support tzoffset in datetime
Support non-zero tzoffset in datetime extended type. If tzoffset and tzindex are not specified, return object with timezone-naive `pandas.Timestamp` internals. If tzoffset is specified, return object with timezone-aware `pandas.Timestamp` with in-built datetime.timezone info. Part of #204
1 parent 05358d2 commit 0ed5039

File tree

3 files changed

+67
-6
lines changed

3 files changed

+67
-6
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
convert to a `pandas.Timestamp` and then use `to_datetime64()`
3232
or `to_datetime()` converter.
3333

34+
- Offset in datetime type support (#204).
35+
36+
In-built `datetime.timezone(datetime.timedelta(minutes=offset))`
37+
is used to store offset timezones.
38+
3439
### Changed
3540
- Bump msgpack requirement to 1.0.4 (PR #223).
3641
The only reason of this bump is various vulnerability fixes,

tarantool/msgpack_ext/types/datetime.py

+25-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from copy import deepcopy
22

3+
import datetime
34
import pandas
45

56
# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
@@ -41,6 +42,8 @@
4142
BYTEORDER = 'little'
4243

4344
NSEC_IN_SEC = 1000000000
45+
SEC_IN_MIN = 60
46+
MIN_IN_DAY = 60 * 24
4447

4548

4649
def get_bytes_as_int(data, cursor, size):
@@ -50,6 +53,14 @@ def get_bytes_as_int(data, cursor, size):
5053
def get_int_as_bytes(data, size):
5154
return data.to_bytes(size, byteorder=BYTEORDER, signed=True)
5255

56+
def compute_offset(timestamp):
57+
if timestamp.tz is None:
58+
return 0
59+
60+
utc_offset = timestamp.tz.utcoffset(timestamp)
61+
# There is no precision loss since offset is in minutes
62+
return utc_offset.days * MIN_IN_DAY + utc_offset.seconds // SEC_IN_MIN
63+
5364
def msgpack_decode(data):
5465
cursor = 0
5566
seconds, cursor = get_bytes_as_int(data, cursor, SECONDS_SIZE_BYTES)
@@ -65,12 +76,20 @@ def msgpack_decode(data):
6576
else:
6677
raise MsgpackError('Unexpected datetime payload length')
6778

68-
if (tzoffset != 0) or (tzindex != 0):
69-
raise NotImplementedError
70-
7179
total_nsec = seconds * NSEC_IN_SEC + nsec
7280

73-
return pandas.to_datetime(total_nsec, unit='ns')
81+
if (tzindex != 0):
82+
raise NotImplementedError
83+
elif (tzoffset != 0):
84+
tzinfo = datetime.timezone(datetime.timedelta(minutes=tzoffset))
85+
return pandas.to_datetime(total_nsec, unit='ns')\
86+
.replace(tzinfo=datetime.timezone.utc)\
87+
.tz_convert(tzinfo)
88+
else:
89+
# return timezone-naive pandas.Timestamp
90+
return pandas.to_datetime(total_nsec, unit='ns')
91+
92+
return timestamp
7493

7594
class Datetime():
7695
def __init__(self, *args, **kwargs):
@@ -81,7 +100,7 @@ def __init__(self, *args, **kwargs):
81100
return
82101

83102
if isinstance(data, pandas.Timestamp):
84-
self._timestamp = = deepcopy(data)
103+
self._timestamp = deepcopy(data)
85104
return
86105

87106
if isinstance(data, Datetime):
@@ -113,7 +132,7 @@ def msgpack_encode(self):
113132

114133
seconds = ts_value // NSEC_IN_SEC
115134
nsec = ts_value % NSEC_IN_SEC
116-
tzoffset = 0
135+
tzoffset = compute_offset(self._timestamp)
117136
tzindex = 0
118137

119138
buf = get_int_as_bytes(seconds, SECONDS_SIZE_BYTES)

test/suites/test_datetime.py

+37
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import sys
66
import unittest
7+
import datetime
78
import msgpack
89
import warnings
910
import tarantool
@@ -99,6 +100,42 @@ def setUp(self):
99100
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
100101
r"nsec=308543321})",
101102
},
103+
'datetime_with_positive_offset': {
104+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, second=54,
105+
microsecond=308543, nanosecond=321,
106+
tzinfo=datetime.timezone(datetime.timedelta(minutes=180))),
107+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
108+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
109+
r"nsec=308543321, tzoffset=180})",
110+
},
111+
'datetime_with_negative_offset': {
112+
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, second=54,
113+
microsecond=308543, nanosecond=321,
114+
tzinfo=datetime.timezone(datetime.timedelta(minutes=-60))),
115+
'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xc4\xff\x00\x00'),
116+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
117+
r"nsec=308543321, tzoffset=-60})",
118+
},
119+
'pandas_timestamp_with_positive_offset': {
120+
'python': tarantool.Datetime(pandas.Timestamp(
121+
year=2022, month=8, day=31, hour=18, minute=7, second=54,
122+
microsecond=308543, nanosecond=321,
123+
tzinfo=datetime.timezone(datetime.timedelta(minutes=180))
124+
)),
125+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
126+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
127+
r"nsec=308543321, tzoffset=180})",
128+
},
129+
'pandas_timestamp_with_negative_offset': {
130+
'python': tarantool.Datetime(pandas.Timestamp(
131+
year=2022, month=8, day=31, hour=18, minute=7, second=54,
132+
microsecond=308543, nanosecond=321,
133+
tzinfo=datetime.timezone(datetime.timedelta(minutes=-60))
134+
)),
135+
'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xc4\xff\x00\x00'),
136+
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
137+
r"nsec=308543321, tzoffset=-60})",
138+
},
102139
}
103140

104141
def test_msgpack_decode(self):

0 commit comments

Comments
 (0)