11"""
22Tarantool `datetime`_ extension type support module.
33
4- Refer to :mod:`~tarantool.msgpack_ext.types.datetime`.
4+ The datetime MessagePack representation looks like this:
55
6- .. _datetime: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
6+ .. code-block:: text
7+
8+ +---------+----------------+==========+-----------------+
9+ | MP_EXT | MP_DATETIME | seconds | nsec; tzoffset; |
10+ | = d7/d8 | = 4 | | tzindex; |
11+ +---------+----------------+==========+-----------------+
12+
13+ MessagePack data contains:
14+
15+ * Seconds (8 bytes) as an unencoded 64-bit signed integer stored in the
16+ little-endian order.
17+ * The optional fields (8 bytes), if any of them have a non-zero value.
18+ The fields include nsec (4 bytes), tzoffset (2 bytes), and
19+ tzindex (2 bytes) packed in the little-endian order.
20+
21+ ``seconds`` is seconds since Epoch, where the epoch is the point where
22+ the time starts, and is platform dependent. For Unix, the epoch is
23+ January 1, 1970, 00:00:00 (UTC). Tarantool uses a ``double`` type, see a
24+ structure definition in src/lib/core/datetime.h and reasons in
25+ `datetime RFC`_.
26+
27+ ``nsec`` is nanoseconds, fractional part of seconds. Tarantool uses
28+ ``int32_t``, see a definition in src/lib/core/datetime.h.
29+
30+ ``tzoffset`` is timezone offset in minutes from UTC. Tarantool uses
31+ ``int16_t`` type, see a structure definition in src/lib/core/datetime.h.
32+
33+ ``tzindex`` is Olson timezone id. Tarantool uses ``int16_t`` type, see
34+ a structure definition in src/lib/core/datetime.h. If both
35+ ``tzoffset`` and ``tzindex`` are specified, ``tzindex`` has the
36+ preference and the ``tzoffset`` value is ignored.
37+
38+ .. _datetime RFC: https://github.com/tarantool/tarantool/wiki/Datetime-internals#intervals-in-c
739"""
840
9- from tarantool .msgpack_ext .types .datetime import Datetime
41+ from tarantool .msgpack_ext .types .datetime import (
42+ NSEC_IN_SEC ,
43+ SEC_IN_MIN ,
44+ Datetime ,
45+ )
46+ import tarantool .msgpack_ext .types .timezones as tt_timezones
47+
48+ from tarantool .error import MsgpackError
1049
1150EXT_ID = 4
1251"""
1352`datetime`_ type id.
1453"""
1554
55+ BYTEORDER = 'little'
56+
57+ SECONDS_SIZE_BYTES = 8
58+ NSEC_SIZE_BYTES = 4
59+ TZOFFSET_SIZE_BYTES = 2
60+ TZINDEX_SIZE_BYTES = 2
61+
62+
63+ def get_int_as_bytes (data , size ):
64+ """
65+ Get binary representation of integer value.
66+
67+ :param data: Integer value.
68+ :type data: :obj:`int`
69+
70+ :param size: Integer size, in bytes.
71+ :type size: :obj:`int`
72+
73+ :return: Encoded integer.
74+ :rtype: :obj:`bytes`
75+
76+ :meta private:
77+ """
78+
79+ return data .to_bytes (size , byteorder = BYTEORDER , signed = True )
80+
1681def encode (obj ):
1782 """
1883 Encode a datetime object.
@@ -26,7 +91,48 @@ def encode(obj):
2691 :raise: :exc:`tarantool.Datetime.msgpack_encode` exceptions
2792 """
2893
29- return obj .msgpack_encode ()
94+ seconds = obj .value // NSEC_IN_SEC
95+ nsec = obj .nsec
96+ tzoffset = obj .tzoffset
97+
98+ tz = obj .tz
99+ if tz != '' :
100+ tzindex = tt_timezones .timezoneToIndex [tz ]
101+ else :
102+ tzindex = 0
103+
104+ buf = get_int_as_bytes (seconds , SECONDS_SIZE_BYTES )
105+
106+ if (nsec != 0 ) or (tzoffset != 0 ) or (tzindex != 0 ):
107+ buf = buf + get_int_as_bytes (nsec , NSEC_SIZE_BYTES )
108+ buf = buf + get_int_as_bytes (tzoffset , TZOFFSET_SIZE_BYTES )
109+ buf = buf + get_int_as_bytes (tzindex , TZINDEX_SIZE_BYTES )
110+
111+ return buf
112+
113+
114+ def get_bytes_as_int (data , cursor , size ):
115+ """
116+ Get integer value from binary data.
117+
118+ :param data: MessagePack binary data.
119+ :type data: :obj:`bytes`
120+
121+ :param cursor: Index after last parsed byte.
122+ :type cursor: :obj:`int`
123+
124+ :param size: Integer size, in bytes.
125+ :type size: :obj:`int`
126+
127+ :return: First value: parsed integer, second value: new cursor
128+ position.
129+ :rtype: first value: :obj:`int`, second value: :obj:`int`
130+
131+ :meta private:
132+ """
133+
134+ part = data [cursor :cursor + size ]
135+ return int .from_bytes (part , BYTEORDER , signed = True ), cursor + size
30136
31137def decode (data ):
32138 """
@@ -38,7 +144,35 @@ def decode(data):
38144 :return: Decoded datetime.
39145 :rtype: :class:`tarantool.Datetime`
40146
41- :raise: :exc:`tarantool.Datetime` exceptions
147+ :raise: :exc:`~tarantool.error.MsgpackError`,
148+ :exc:`tarantool.Datetime` exceptions
42149 """
43150
44- return Datetime (data )
151+ cursor = 0
152+ seconds , cursor = get_bytes_as_int (data , cursor , SECONDS_SIZE_BYTES )
153+
154+ data_len = len (data )
155+ if data_len == (SECONDS_SIZE_BYTES + NSEC_SIZE_BYTES + \
156+ TZOFFSET_SIZE_BYTES + TZINDEX_SIZE_BYTES ):
157+ nsec , cursor = get_bytes_as_int (data , cursor , NSEC_SIZE_BYTES )
158+ tzoffset , cursor = get_bytes_as_int (data , cursor , TZOFFSET_SIZE_BYTES )
159+ tzindex , cursor = get_bytes_as_int (data , cursor , TZINDEX_SIZE_BYTES )
160+ elif data_len == SECONDS_SIZE_BYTES :
161+ nsec = 0
162+ tzoffset = 0
163+ tzindex = 0
164+ else :
165+ raise MsgpackError (f'Unexpected datetime payload length { data_len } ' )
166+
167+ if tzindex != 0 :
168+ if tzindex not in tt_timezones .indexToTimezone :
169+ raise MsgpackError (f'Failed to decode datetime with unknown tzindex "{ tzindex } "' )
170+ tz = tt_timezones .indexToTimezone [tzindex ]
171+ return Datetime (timestamp = seconds , nsec = nsec , tz = tz ,
172+ timestamp_since_utc_epoch = True )
173+ elif tzoffset != 0 :
174+ return Datetime (timestamp = seconds , nsec = nsec , tzoffset = tzoffset ,
175+ timestamp_since_utc_epoch = True )
176+ else :
177+ return Datetime (timestamp = seconds , nsec = nsec ,
178+ timestamp_since_utc_epoch = True )
0 commit comments