From e5292f8a86ec5f88c70ba36b3a508eeeb6d2583f Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 6 Sep 2022 13:36:49 +0300 Subject: [PATCH 1/3] msgpack: support datetime extended type Tarantool supports datetime type since version 2.10.0 [1]. This patch introduced the support of Tarantool datetime type in msgpack decoders and encoders. Tarantool datetime objects are decoded to `tarantool.Datetime` type. `tarantool.Datetime` may be encoded to Tarantool datetime objects. `tarantool.Datetime` stores data in a `pandas.Timestamp` object. You can create `tarantool.Datetime` objects either from msgpack data or by using the same API as in Tarantool: ``` dt1 = tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, nsec=308543321) dt2 = tarantool.Datetime(timestamp=1661969274) dt3 = tarantool.Datetime(timestamp=1661969274, nsec=308543321) ``` `tarantool.Datetime` exposes `year`, `month`, `day`, `hour`, `minute`, `sec`, `nsec`, `timestamp` and `value` (integer epoch time with nanoseconds precision) properties if you need to convert `tarantool.Datetime` to any other kind of datetime object: ``` pdt = pandas.Timestamp(year=dt.year, month=dt.month, day=dt.day, hour=dt.hour, minute=dt.minute, second=dt.sec, microsecond=(dt.nsec // 1000), nanosecond=(dt.nsec % 1000)) ``` `pandas.Timestamp` was chosen to store data because it could be used to store both nanoseconds and timezone information. In-build Python `datetime.datetime` supports microseconds at most, `numpy.datetime64` do not support timezones. Tarantool datetime interval type is planned to be stored in custom type `tarantool.Interval` and we'll need a way to support arithmetic between datetime and interval. This is the main reason we use custom class instead of plain `pandas.Timestamp`. It is also hard to implement Tarantool-compatible timezones with full conversion support without custom classes. This patch does not yet introduce the support of timezones in datetime. 1. https://github.com/tarantool/tarantool/issues/5941 2. https://pandas.pydata.org/docs/reference/api/pandas.Timestamp.html Part of #204 --- CHANGELOG.md | 30 ++++ requirements.txt | 1 + tarantool/__init__.py | 6 +- tarantool/msgpack_ext/datetime.py | 9 + tarantool/msgpack_ext/packer.py | 8 +- tarantool/msgpack_ext/types/datetime.py | 196 +++++++++++++++++++++ tarantool/msgpack_ext/unpacker.py | 6 +- test/suites/__init__.py | 3 +- test/suites/lib/skip.py | 11 ++ test/suites/test_datetime.py | 219 ++++++++++++++++++++++++ 10 files changed, 483 insertions(+), 6 deletions(-) create mode 100644 tarantool/msgpack_ext/datetime.py create mode 100644 tarantool/msgpack_ext/types/datetime.py create mode 100644 test/suites/test_datetime.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 226909d8..6e2705d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Decimal type support (#203). - UUID type support (#202). +- Datetime type support and tarantool.Datetime type (#204). + + Tarantool datetime objects are decoded to `tarantool.Datetime` + type. `tarantool.Datetime` may be encoded to Tarantool datetime + objects. + + You can create `tarantool.Datetime` objects either from msgpack + data or by using the same API as in Tarantool: + + ```python + dt1 = tarantool.Datetime(year=2022, month=8, day=31, + hour=18, minute=7, sec=54, + nsec=308543321) + + dt2 = tarantool.Datetime(timestamp=1661969274) + + dt3 = tarantool.Datetime(timestamp=1661969274, nsec=308543321) + ``` + + `tarantool.Datetime` exposes `year`, `month`, `day`, `hour`, + `minute`, `sec`, `nsec`, `timestamp` and `value` (integer epoch time + with nanoseconds precision) properties if you need to convert + `tarantool.Datetime` to any other kind of datetime object: + + ```python + pdt = pandas.Timestamp(year=dt.year, month=dt.month, day=dt.day, + hour=dt.hour, minute=dt.minute, second=dt.sec, + microsecond=(dt.nsec // 1000), + nanosecond=(dt.nsec % 1000)) + ``` ### Changed - Bump msgpack requirement to 1.0.4 (PR #223). diff --git a/requirements.txt b/requirements.txt index 46dff380..cdf505c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ msgpack>=1.0.4 +pandas diff --git a/tarantool/__init__.py b/tarantool/__init__.py index 3d4a19a8..6625b4eb 100644 --- a/tarantool/__init__.py +++ b/tarantool/__init__.py @@ -32,6 +32,10 @@ ENCODING_DEFAULT, ) +from tarantool.msgpack_ext.types.datetime import ( + Datetime, +) + __version__ = "0.9.0" @@ -91,7 +95,7 @@ def connectmesh(addrs=({'host': 'localhost', 'port': 3301},), user=None, __all__ = ['connect', 'Connection', 'connectmesh', 'MeshConnection', 'Schema', 'Error', 'DatabaseError', 'NetworkError', 'NetworkWarning', - 'SchemaError', 'dbapi'] + 'SchemaError', 'dbapi', 'Datetime'] # ConnectionPool is supported only for Python 3.7 or newer. if sys.version_info.major >= 3 and sys.version_info.minor >= 7: diff --git a/tarantool/msgpack_ext/datetime.py b/tarantool/msgpack_ext/datetime.py new file mode 100644 index 00000000..70f56dc9 --- /dev/null +++ b/tarantool/msgpack_ext/datetime.py @@ -0,0 +1,9 @@ +from tarantool.msgpack_ext.types.datetime import Datetime + +EXT_ID = 4 + +def encode(obj): + return obj.msgpack_encode() + +def decode(data): + return Datetime(data) diff --git a/tarantool/msgpack_ext/packer.py b/tarantool/msgpack_ext/packer.py index e8dd74db..bff2b821 100644 --- a/tarantool/msgpack_ext/packer.py +++ b/tarantool/msgpack_ext/packer.py @@ -2,12 +2,16 @@ from uuid import UUID from msgpack import ExtType +from tarantool.msgpack_ext.types.datetime import Datetime + import tarantool.msgpack_ext.decimal as ext_decimal import tarantool.msgpack_ext.uuid as ext_uuid +import tarantool.msgpack_ext.datetime as ext_datetime encoders = [ - {'type': Decimal, 'ext': ext_decimal}, - {'type': UUID, 'ext': ext_uuid }, + {'type': Decimal, 'ext': ext_decimal }, + {'type': UUID, 'ext': ext_uuid }, + {'type': Datetime, 'ext': ext_datetime}, ] def default(obj): diff --git a/tarantool/msgpack_ext/types/datetime.py b/tarantool/msgpack_ext/types/datetime.py new file mode 100644 index 00000000..75fc50cb --- /dev/null +++ b/tarantool/msgpack_ext/types/datetime.py @@ -0,0 +1,196 @@ +from copy import deepcopy + +import pandas + +# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type +# +# The datetime MessagePack representation looks like this: +# +---------+----------------+==========+-----------------+ +# | MP_EXT | MP_DATETIME | seconds | nsec; tzoffset; | +# | = d7/d8 | = 4 | | tzindex; | +# +---------+----------------+==========+-----------------+ +# MessagePack data contains: +# +# * Seconds (8 bytes) as an unencoded 64-bit signed integer stored in the +# little-endian order. +# * The optional fields (8 bytes), if any of them have a non-zero value. +# The fields include nsec (4 bytes), tzoffset (2 bytes), and +# tzindex (2 bytes) packed in the little-endian order. +# +# seconds is seconds since Epoch, where the epoch is the point where the time +# starts, and is platform dependent. For Unix, the epoch is January 1, +# 1970, 00:00:00 (UTC). Tarantool uses a double type, see a structure +# definition in src/lib/core/datetime.h and reasons in +# https://github.com/tarantool/tarantool/wiki/Datetime-internals#intervals-in-c +# +# nsec is nanoseconds, fractional part of seconds. Tarantool uses int32_t, see +# a definition in src/lib/core/datetime.h. +# +# tzoffset is timezone offset in minutes from UTC. Tarantool uses a int16_t type, +# see a structure definition in src/lib/core/datetime.h. +# +# tzindex is Olson timezone id. Tarantool uses a int16_t type, see a structure +# definition in src/lib/core/datetime.h. If both tzoffset and tzindex are +# specified, tzindex has the preference and the tzoffset value is ignored. + +SECONDS_SIZE_BYTES = 8 +NSEC_SIZE_BYTES = 4 +TZOFFSET_SIZE_BYTES = 2 +TZINDEX_SIZE_BYTES = 2 + +BYTEORDER = 'little' + +NSEC_IN_SEC = 1000000000 +NSEC_IN_MKSEC = 1000 + +def get_bytes_as_int(data, cursor, size): + part = data[cursor:cursor + size] + return int.from_bytes(part, BYTEORDER, signed=True), cursor + size + +def get_int_as_bytes(data, size): + return data.to_bytes(size, byteorder=BYTEORDER, signed=True) + +def msgpack_decode(data): + cursor = 0 + seconds, cursor = get_bytes_as_int(data, cursor, SECONDS_SIZE_BYTES) + + data_len = len(data) + if data_len == (SECONDS_SIZE_BYTES + NSEC_SIZE_BYTES + \ + TZOFFSET_SIZE_BYTES + TZINDEX_SIZE_BYTES): + nsec, cursor = get_bytes_as_int(data, cursor, NSEC_SIZE_BYTES) + tzoffset, cursor = get_bytes_as_int(data, cursor, TZOFFSET_SIZE_BYTES) + tzindex, cursor = get_bytes_as_int(data, cursor, TZINDEX_SIZE_BYTES) + elif data_len == SECONDS_SIZE_BYTES: + nsec = 0 + tzoffset = 0 + tzindex = 0 + else: + raise MsgpackError(f'Unexpected datetime payload length {data_len}') + + if (tzoffset != 0) or (tzindex != 0): + raise NotImplementedError + + total_nsec = seconds * NSEC_IN_SEC + nsec + + return pandas.to_datetime(total_nsec, unit='ns') + +class Datetime(): + def __init__(self, data=None, *, timestamp=None, year=None, month=None, + day=None, hour=None, minute=None, sec=None, nsec=None): + if data is not None: + if not isinstance(data, bytes): + raise ValueError('data argument (first positional argument) ' + + 'expected to be a "bytes" instance') + + self._datetime = msgpack_decode(data) + return + + # The logic is same as in Tarantool, refer to datetime API. + # https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/new/ + if timestamp is not None: + if ((year is not None) or (month is not None) or \ + (day is not None) or (hour is not None) or \ + (minute is not None) or (sec is not None)): + raise ValueError('Cannot provide both timestamp and year, month, ' + + 'day, hour, minute, sec') + + if nsec is not None: + if not isinstance(timestamp, int): + raise ValueError('timestamp must be int if nsec provided') + + total_nsec = timestamp * NSEC_IN_SEC + nsec + self._datetime = pandas.to_datetime(total_nsec, unit='ns') + else: + self._datetime = pandas.to_datetime(timestamp, unit='s') + else: + if nsec is not None: + microsecond = nsec // NSEC_IN_MKSEC + nanosecond = nsec % NSEC_IN_MKSEC + else: + microsecond = 0 + nanosecond = 0 + + self._datetime = pandas.Timestamp(year=year, month=month, day=day, + hour=hour, minute=minute, second=sec, + microsecond=microsecond, + nanosecond=nanosecond) + + def __eq__(self, other): + if isinstance(other, Datetime): + return self._datetime == other._datetime + elif isinstance(other, pandas.Timestamp): + return self._datetime == other + else: + return False + + def __str__(self): + return self._datetime.__str__() + + def __repr__(self): + return f'datetime: {self._datetime.__repr__()}' + + def __copy__(self): + cls = self.__class__ + result = cls.__new__(cls) + result.__dict__.update(self.__dict__) + return result + + def __deepcopy__(self, memo): + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + setattr(result, k, deepcopy(v, memo)) + return result + + @property + def year(self): + return self._datetime.year + + @property + def month(self): + return self._datetime.month + + @property + def day(self): + return self._datetime.day + + @property + def hour(self): + return self._datetime.hour + + @property + def minute(self): + return self._datetime.minute + + @property + def sec(self): + return self._datetime.second + + @property + def nsec(self): + # microseconds + nanoseconds + return self._datetime.value % NSEC_IN_SEC + + @property + def timestamp(self): + return self._datetime.timestamp() + + @property + def value(self): + return self._datetime.value + + def msgpack_encode(self): + seconds = self.value // NSEC_IN_SEC + nsec = self.nsec + tzoffset = 0 + tzindex = 0 + + buf = get_int_as_bytes(seconds, SECONDS_SIZE_BYTES) + + if (nsec != 0) or (tzoffset != 0) or (tzindex != 0): + buf = buf + get_int_as_bytes(nsec, NSEC_SIZE_BYTES) + buf = buf + get_int_as_bytes(tzoffset, TZOFFSET_SIZE_BYTES) + buf = buf + get_int_as_bytes(tzindex, TZINDEX_SIZE_BYTES) + + return buf diff --git a/tarantool/msgpack_ext/unpacker.py b/tarantool/msgpack_ext/unpacker.py index 44bfdb63..b303e18d 100644 --- a/tarantool/msgpack_ext/unpacker.py +++ b/tarantool/msgpack_ext/unpacker.py @@ -1,9 +1,11 @@ import tarantool.msgpack_ext.decimal as ext_decimal import tarantool.msgpack_ext.uuid as ext_uuid +import tarantool.msgpack_ext.datetime as ext_datetime decoders = { - ext_decimal.EXT_ID: ext_decimal.decode, - ext_uuid.EXT_ID : ext_uuid.decode , + ext_decimal.EXT_ID : ext_decimal.decode , + ext_uuid.EXT_ID : ext_uuid.decode , + ext_datetime.EXT_ID: ext_datetime.decode, } def ext_hook(code, data): diff --git a/test/suites/__init__.py b/test/suites/__init__.py index 94357c8e..c5792bdd 100644 --- a/test/suites/__init__.py +++ b/test/suites/__init__.py @@ -17,13 +17,14 @@ from .test_ssl import TestSuite_Ssl from .test_decimal import TestSuite_Decimal from .test_uuid import TestSuite_UUID +from .test_datetime import TestSuite_Datetime 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_Decimal, TestSuite_UUID, TestSuite_Datetime) def load_tests(loader, tests, pattern): suite = unittest.TestSuite() diff --git a/test/suites/lib/skip.py b/test/suites/lib/skip.py index 9ce76991..71bfce13 100644 --- a/test/suites/lib/skip.py +++ b/test/suites/lib/skip.py @@ -154,3 +154,14 @@ def skip_or_run_UUID_test(func): return skip_or_run_test_tarantool(func, '2.4.1', 'does not support UUID type') + +def skip_or_run_datetime_test(func): + """Decorator to skip or run datetime-related tests depending on + the tarantool version. + + Tarantool supports datetime type only since 2.10.0 version. + See https://github.com/tarantool/tarantool/issues/5941 + """ + + return skip_or_run_test_pcall_require(func, 'datetime', + 'does not support datetime type') diff --git a/test/suites/test_datetime.py b/test/suites/test_datetime.py new file mode 100644 index 00000000..ecefda15 --- /dev/null +++ b/test/suites/test_datetime.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import sys +import re +import unittest +import msgpack +import warnings +import tarantool +import pandas + +from tarantool.msgpack_ext.packer import default as packer_default +from tarantool.msgpack_ext.unpacker import ext_hook as unpacker_ext_hook + +from .lib.tarantool_server import TarantoolServer +from .lib.skip import skip_or_run_datetime_test +from tarantool.error import MsgpackError, MsgpackWarning + +class TestSuite_Datetime(unittest.TestCase): + @classmethod + def setUpClass(self): + print(' DATETIME EXT TYPE '.center(70, '='), file=sys.stderr) + print('-' * 70, file=sys.stderr) + self.srv = TarantoolServer() + self.srv.script = 'test/suites/box.lua' + self.srv.start() + + self.adm = self.srv.admin + self.adm(r""" + _, datetime = pcall(require, 'datetime') + + box.schema.space.create('test') + box.space['test']:create_index('primary', { + type = 'tree', + parts = {1, 'string'}, + unique = true}) + + box.schema.user.create('test', {password = 'test', if_not_exists = true}) + box.schema.user.grant('test', 'read,write,execute', 'universe') + """) + + self.con = tarantool.Connection(self.srv.host, self.srv.args['primary'], + user='test', password='test') + + def setUp(self): + # prevent a remote tarantool from clean our session + if self.srv.is_started(): + self.srv.touch_lock() + + self.adm("box.space['test']:truncate()") + + + def test_Datetime_class_API(self): + dt = tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, + nsec=308543321) + + self.assertEqual(dt.year, 2022) + self.assertEqual(dt.month, 8) + self.assertEqual(dt.day, 31) + self.assertEqual(dt.hour, 18) + self.assertEqual(dt.minute, 7) + self.assertEqual(dt.sec, 54) + self.assertEqual(dt.nsec, 308543321) + # Both Tarantool and pandas prone to precision loss for timestamp() floats + self.assertEqual(dt.timestamp, 1661969274.308543) + self.assertEqual(dt.value, 1661969274308543321) + + + datetime_class_invalid_init_cases = { + 'positional_year': { + 'args': [2022], + 'kwargs': {}, + 'type': ValueError, + 'msg': 'data argument (first positional argument) expected to be a "bytes" instance' + }, + 'positional_date': { + 'args': [2022, 8, 31], + 'kwargs': {}, + 'type': TypeError, + 'msg': '__init__() takes from 1 to 2 positional arguments but 4 were given' + }, + 'mixing_date_and_timestamp': { + 'args': [], + 'kwargs': {'year': 2022, 'timestamp': 1661969274}, + 'type': ValueError, + 'msg': 'Cannot provide both timestamp and year, month, day, hour, minute, sec' + }, + 'mixing_float_timestamp_and_nsec': { + 'args': [], + 'kwargs': {'timestamp': 1661969274.308543, 'nsec': 308543321}, + 'type': ValueError, + 'msg': 'timestamp must be int if nsec provided' + }, + } + + def test_Datetime_class_invalid_init(self): + for name in self.datetime_class_invalid_init_cases.keys(): + with self.subTest(msg=name): + case = self.datetime_class_invalid_init_cases[name] + self.assertRaisesRegex( + case['type'], re.escape(case['msg']), + lambda: tarantool.Datetime(*case['args'], **case['kwargs'])) + + + integration_cases = { + 'date': { + 'python': tarantool.Datetime(year=2022, month=8, day=31), + 'msgpack': (b'\x80\xa4\x0e\x63\x00\x00\x00\x00'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31})", + }, + 'date_unix_start': { + 'python': tarantool.Datetime(year=1970, month=1, day=1), + 'msgpack': (b'\x00\x00\x00\x00\x00\x00\x00\x00'), + 'tarantool': r"datetime.new({year=1970, month=1, day=1})", + }, + 'date_before_1970': { + 'python': tarantool.Datetime(year=1900, month=1, day=1), + 'msgpack': (b'\x80\x81\x55\x7c\xff\xff\xff\xff'), + 'tarantool': r"datetime.new({year=1900, month=1, day=1})", + }, + 'datetime_with_minutes': { + 'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7), + 'msgpack': (b'\x44\xa3\x0f\x63\x00\x00\x00\x00'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7})", + }, + 'datetime_with_seconds': { + 'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54), + 'msgpack': (b'\x7a\xa3\x0f\x63\x00\x00\x00\x00'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54})", + }, + 'datetime_with_microseconds': { + 'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, + nsec=308543000), + 'msgpack': (b'\x7a\xa3\x0f\x63\x00\x00\x00\x00\x18\xfe\x63\x12\x00\x00\x00\x00'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " + + r"nsec=308543000})", + }, + 'datetime_with_nanoseconds': { + 'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, + nsec=308543321), + 'msgpack': (b'\x7a\xa3\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\x00\x00\x00\x00'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " + + r"nsec=308543321})", + }, + 'date_before_1970_with_nanoseconds': { + 'python': tarantool.Datetime(year=1900, month=1, day=1, nsec=308543321), + 'msgpack': (b'\x80\x81\x55\x7c\xff\xff\xff\xff\x59\xff\x63\x12\x00\x00\x00\x00'), + 'tarantool': r"datetime.new({year=1900, month=1, day=1, nsec=308543321})", + }, + 'timestamp': { + 'python': tarantool.Datetime(timestamp=1661969274), + 'msgpack': (b'\x7a\xa3\x0f\x63\x00\x00\x00\x00'), + 'tarantool': r"datetime.new({timestamp=1661969274})", + }, + 'timestamp_with_nanoseconds': { + 'python': tarantool.Datetime(timestamp=1661969274, nsec=308543321), + 'msgpack': (b'\x7a\xa3\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\x00\x00\x00\x00'), + 'tarantool': r"datetime.new({timestamp=1661969274, nsec=308543321})", + }, + } + + def test_msgpack_decode(self): + for name in self.integration_cases.keys(): + with self.subTest(msg=name): + case = self.integration_cases[name] + + self.assertEqual(unpacker_ext_hook(4, case['msgpack']), + case['python']) + + @skip_or_run_datetime_test + def test_tarantool_decode(self): + for name in self.integration_cases.keys(): + with self.subTest(msg=name): + case = self.integration_cases[name] + + self.adm(f"box.space['test']:replace{{'{name}', {case['tarantool']}, 'field'}}") + + self.assertSequenceEqual(self.con.select('test', name), + [[name, case['python'], 'field']]) + + def test_msgpack_encode(self): + for name in self.integration_cases.keys(): + with self.subTest(msg=name): + case = self.integration_cases[name] + + self.assertEqual(packer_default(case['python']), + msgpack.ExtType(code=4, data=case['msgpack'])) + + @skip_or_run_datetime_test + def test_tarantool_encode(self): + for name in self.integration_cases.keys(): + with self.subTest(msg=name): + case = self.integration_cases[name] + + self.con.insert('test', [name, case['python'], 'field']) + + lua_eval = f""" + local dt = {case['tarantool']} + + local tuple = box.space['test']:get('{name}') + assert(tuple ~= nil) + + if tuple[2] == dt then + return true + else + return nil, ('%s is not equal to expected %s'):format( + tostring(tuple[2]), tostring(dt)) + end + """ + + self.assertSequenceEqual(self.adm(lua_eval), [True]) + + + @classmethod + def tearDownClass(self): + self.con.close() + self.srv.stop() + self.srv.clean() From b2d1e0084f8845196bb332160344c6c72c19e861 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 19 Sep 2022 17:18:05 +0300 Subject: [PATCH 2/3] msgpack: support tzoffset in datetime Support non-zero tzoffset in datetime extended type. Use `tzoffset` parameter to set up offset timezone: ``` dt = tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, nsec=308543321, tzoffset=180) ``` You may use `tzoffset` property to get timezone offset of a datetime object. Offset timezone is built with pytz.FixedOffset(). pytz module is already a dependency of pandas, but this patch adds it as a requirement just in case something will change in the future. This patch doesn't yet introduce the support of named timezones (tzindex). Part of #204 --- CHANGELOG.md | 13 ++++++ requirements.txt | 1 + tarantool/msgpack_ext/types/datetime.py | 54 +++++++++++++++++++------ test/suites/test_datetime.py | 31 ++++++++++++-- 4 files changed, 84 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2705d6..ec6d9838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 nanosecond=(dt.nsec % 1000)) ``` +- Offset in datetime type support (#204). + + Use `tzoffset` parameter to set up offset timezone: + + ```python + dt = tarantool.Datetime(year=2022, month=8, day=31, + hour=18, minute=7, sec=54, + nsec=308543321, tzoffset=180) + ``` + + You may use `tzoffset` property to get timezone offset of a datetime + object. + ### Changed - Bump msgpack requirement to 1.0.4 (PR #223). The only reason of this bump is various vulnerability fixes, diff --git a/requirements.txt b/requirements.txt index cdf505c7..2a9c9c38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ msgpack>=1.0.4 pandas +pytz diff --git a/tarantool/msgpack_ext/types/datetime.py b/tarantool/msgpack_ext/types/datetime.py index 75fc50cb..1a1d3750 100644 --- a/tarantool/msgpack_ext/types/datetime.py +++ b/tarantool/msgpack_ext/types/datetime.py @@ -1,6 +1,7 @@ from copy import deepcopy import pandas +import pytz # https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type # @@ -42,6 +43,7 @@ NSEC_IN_SEC = 1000000000 NSEC_IN_MKSEC = 1000 +SEC_IN_MIN = 60 def get_bytes_as_int(data, cursor, size): part = data[cursor:cursor + size] @@ -50,6 +52,17 @@ def get_bytes_as_int(data, cursor, size): def get_int_as_bytes(data, size): return data.to_bytes(size, byteorder=BYTEORDER, signed=True) +def compute_offset(timestamp): + utc_offset = timestamp.tzinfo.utcoffset(timestamp) + + # `None` offset is a valid utcoffset implementation, + # but it seems that pytz timezones never return `None`: + # https://github.com/pandas-dev/pandas/issues/15986 + assert utc_offset is not None + + # There is no precision loss since offset is in minutes + return int(utc_offset.total_seconds()) // SEC_IN_MIN + def msgpack_decode(data): cursor = 0 seconds, cursor = get_bytes_as_int(data, cursor, SECONDS_SIZE_BYTES) @@ -67,16 +80,21 @@ def msgpack_decode(data): else: raise MsgpackError(f'Unexpected datetime payload length {data_len}') - if (tzoffset != 0) or (tzindex != 0): - raise NotImplementedError - total_nsec = seconds * NSEC_IN_SEC + nsec + datetime = pandas.to_datetime(total_nsec, unit='ns') - return pandas.to_datetime(total_nsec, unit='ns') + if tzindex != 0: + raise NotImplementedError + elif tzoffset != 0: + tzinfo = pytz.FixedOffset(tzoffset) + return datetime.replace(tzinfo=pytz.UTC).tz_convert(tzinfo) + else: + return datetime class Datetime(): def __init__(self, data=None, *, timestamp=None, year=None, month=None, - day=None, hour=None, minute=None, sec=None, nsec=None): + day=None, hour=None, minute=None, sec=None, nsec=None, + tzoffset=0): if data is not None: if not isinstance(data, bytes): raise ValueError('data argument (first positional argument) ' + @@ -99,9 +117,9 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None, raise ValueError('timestamp must be int if nsec provided') total_nsec = timestamp * NSEC_IN_SEC + nsec - self._datetime = pandas.to_datetime(total_nsec, unit='ns') + datetime = pandas.to_datetime(total_nsec, unit='ns') else: - self._datetime = pandas.to_datetime(timestamp, unit='s') + datetime = pandas.to_datetime(timestamp, unit='s') else: if nsec is not None: microsecond = nsec // NSEC_IN_MKSEC @@ -110,10 +128,16 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None, microsecond = 0 nanosecond = 0 - self._datetime = pandas.Timestamp(year=year, month=month, day=day, - hour=hour, minute=minute, second=sec, - microsecond=microsecond, - nanosecond=nanosecond) + datetime = pandas.Timestamp(year=year, month=month, day=day, + hour=hour, minute=minute, second=sec, + microsecond=microsecond, + nanosecond=nanosecond) + + if tzoffset != 0: + tzinfo = pytz.FixedOffset(tzoffset) + datetime = datetime.replace(tzinfo=tzinfo) + + self._datetime = datetime def __eq__(self, other): if isinstance(other, Datetime): @@ -176,6 +200,12 @@ def nsec(self): def timestamp(self): return self._datetime.timestamp() + @property + def tzoffset(self): + if self._datetime.tzinfo is not None: + return compute_offset(self._datetime) + return 0 + @property def value(self): return self._datetime.value @@ -183,7 +213,7 @@ def value(self): def msgpack_encode(self): seconds = self.value // NSEC_IN_SEC nsec = self.nsec - tzoffset = 0 + tzoffset = self.tzoffset tzindex = 0 buf = get_int_as_bytes(seconds, SECONDS_SIZE_BYTES) diff --git a/test/suites/test_datetime.py b/test/suites/test_datetime.py index ecefda15..ecd806a2 100644 --- a/test/suites/test_datetime.py +++ b/test/suites/test_datetime.py @@ -53,7 +53,7 @@ def setUp(self): def test_Datetime_class_API(self): dt = tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, - nsec=308543321) + nsec=308543321, tzoffset=180) self.assertEqual(dt.year, 2022) self.assertEqual(dt.month, 8) @@ -63,8 +63,9 @@ def test_Datetime_class_API(self): self.assertEqual(dt.sec, 54) self.assertEqual(dt.nsec, 308543321) # Both Tarantool and pandas prone to precision loss for timestamp() floats - self.assertEqual(dt.timestamp, 1661969274.308543) - self.assertEqual(dt.value, 1661969274308543321) + self.assertEqual(dt.timestamp, 1661958474.308543) + self.assertEqual(dt.tzoffset, 180) + self.assertEqual(dt.value, 1661958474308543321) datetime_class_invalid_init_cases = { @@ -158,6 +159,30 @@ def test_Datetime_class_invalid_init(self): 'msgpack': (b'\x7a\xa3\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\x00\x00\x00\x00'), 'tarantool': r"datetime.new({timestamp=1661969274, nsec=308543321})", }, + 'datetime_with_positive_offset': { + 'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, + nsec=308543321, tzoffset=180), + 'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " + + r"nsec=308543321, tzoffset=180})", + }, + 'datetime_with_negative_offset': { + 'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, + nsec=308543321, tzoffset=-60), + 'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xc4\xff\x00\x00'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " + + r"nsec=308543321, tzoffset=-60})", + }, + 'timestamp_with_positive_offset': { + 'python': tarantool.Datetime(timestamp=1661969274, tzoffset=180), + 'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x00\x00\x00'), + 'tarantool': r"datetime.new({timestamp=1661969274, tzoffset=180})", + }, + 'timestamp_with_negative_offset': { + 'python': tarantool.Datetime(timestamp=1661969274, tzoffset=-60), + 'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x00\x00\x00\x00\xc4\xff\x00\x00'), + 'tarantool': r"datetime.new({timestamp=1661969274, tzoffset=-60})", + }, } def test_msgpack_decode(self): From aa7302a39c31b1fd69052c36af82eadf25aaa0f6 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 19 Sep 2022 17:54:59 +0300 Subject: [PATCH 3/3] msgpack: support tzindex in datetime Support non-zero tzindex in datetime extended type. If both tzoffset and tzindex are specified, tzindex is prior (same as in Tarantool [1]). Use `tz` parameter to set up timezone name: ``` dt = tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, nsec=308543321, tz='Europe/Moscow') ``` You may use `tz` property to get timezone name of a datetime object. pytz is used to build timezone info. Tarantool index to Olson name map and inverted one are built with gen_timezones.sh script based on tarantool/go-tarantool script [2]. All Tarantool unique and alias timezones present in pytz.all_timezones list. Only the following abbreviated timezones from Tarantool presents in pytz.all_timezones (version 2022.2.1): - CET - EET - EST - GMT - HST - MST - UTC - WET pytz does not natively support work with abbreviated timezones due to its possibly ambiguous nature [3-5]. Tarantool itself do not support work with ambiguous abbreviated timezones: ``` Tarantool 2.10.1-0-g482d91c66 tarantool> datetime.new({tz = 'BST'}) --- - error: 'builtin/datetime.lua:477: could not parse ''BST'' - ambiguous timezone' ... ``` If ambiguous timezone is specified, the exception is raised. Tarantool header timezones.h [6] provides a map for all abbreviated timezones with category info (all ambiguous timezones are marked with TZ_AMBIGUOUS flag) and offset info. We parse this info to build pytz.FixedOffset() timezone for each Tarantool abbreviated timezone not supported natively by pytz. 1. https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/new/ 2. https://github.com/tarantool/go-tarantool/blob/5801dc6f5ce69db7c8bc0c0d0fe4fb6042d5ecbc/datetime/gen-timezones.sh 3. https://stackoverflow.com/questions/37109945/how-to-use-abbreviated-timezone-namepst-ist-in-pytz 4. https://stackoverflow.com/questions/27531718/datetime-timezone-conversion-using-pytz 5. https://stackoverflow.com/questions/30315485/pytz-return-olson-timezone-name-from-only-a-gmt-offset 6. https://github.com/tarantool/tarantool/9ee45289e01232b8df1413efea11db170ae3b3b4/src/lib/tzcode/timezones.h Closes #204 --- CHANGELOG.md | 14 + tarantool/msgpack_ext/types/datetime.py | 60 +- .../msgpack_ext/types/timezones/__init__.py | 9 + .../types/timezones/gen-timezones.sh | 69 + .../msgpack_ext/types/timezones/timezones.py | 1784 +++++++++++++++++ .../types/timezones/validate_timezones.py | 12 + test/suites/test_datetime.py | 84 + 7 files changed, 2021 insertions(+), 11 deletions(-) create mode 100644 tarantool/msgpack_ext/types/timezones/__init__.py create mode 100755 tarantool/msgpack_ext/types/timezones/gen-timezones.sh create mode 100644 tarantool/msgpack_ext/types/timezones/timezones.py create mode 100644 tarantool/msgpack_ext/types/timezones/validate_timezones.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ec6d9838..ad7c2cb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 You may use `tzoffset` property to get timezone offset of a datetime object. +- Timezone in datetime type support (#204). + + Use `tz` parameter to set up timezone name: + + ```python + dt = tarantool.Datetime(year=2022, month=8, day=31, + hour=18, minute=7, sec=54, + nsec=308543321, tz='Europe/Moscow') + ``` + + If both `tz` and `tzoffset` is specified, `tz` is used. + + You may use `tz` property to get timezone name of a datetime object. + ### Changed - Bump msgpack requirement to 1.0.4 (PR #223). The only reason of this bump is various vulnerability fixes, diff --git a/tarantool/msgpack_ext/types/datetime.py b/tarantool/msgpack_ext/types/datetime.py index 1a1d3750..62774217 100644 --- a/tarantool/msgpack_ext/types/datetime.py +++ b/tarantool/msgpack_ext/types/datetime.py @@ -3,6 +3,9 @@ import pandas import pytz +import tarantool.msgpack_ext.types.timezones as tt_timezones +from tarantool.error import MsgpackError + # https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type # # The datetime MessagePack representation looks like this: @@ -63,6 +66,17 @@ def compute_offset(timestamp): # There is no precision loss since offset is in minutes return int(utc_offset.total_seconds()) // SEC_IN_MIN +def get_python_tzinfo(tz, error_class): + if tz in pytz.all_timezones: + return pytz.timezone(tz) + + # Checked with timezones/validate_timezones.py + tt_tzinfo = tt_timezones.timezoneAbbrevInfo[tz] + if (tt_tzinfo['category'] & tt_timezones.TZ_AMBIGUOUS) != 0: + raise error_class(f'Failed to create datetime with ambiguous timezone "{tz}"') + + return pytz.FixedOffset(tt_tzinfo['offset']) + def msgpack_decode(data): cursor = 0 seconds, cursor = get_bytes_as_int(data, cursor, SECONDS_SIZE_BYTES) @@ -84,23 +98,29 @@ def msgpack_decode(data): datetime = pandas.to_datetime(total_nsec, unit='ns') if tzindex != 0: - raise NotImplementedError + if tzindex not in tt_timezones.indexToTimezone: + raise MsgpackError(f'Failed to decode datetime with unknown tzindex "{tzindex}"') + tz = tt_timezones.indexToTimezone[tzindex] + tzinfo = get_python_tzinfo(tz, MsgpackError) + return datetime.replace(tzinfo=pytz.UTC).tz_convert(tzinfo), tz elif tzoffset != 0: tzinfo = pytz.FixedOffset(tzoffset) - return datetime.replace(tzinfo=pytz.UTC).tz_convert(tzinfo) + return datetime.replace(tzinfo=pytz.UTC).tz_convert(tzinfo), '' else: - return datetime + return datetime, '' class Datetime(): def __init__(self, data=None, *, timestamp=None, year=None, month=None, day=None, hour=None, minute=None, sec=None, nsec=None, - tzoffset=0): + tzoffset=0, tz=''): if data is not None: if not isinstance(data, bytes): raise ValueError('data argument (first positional argument) ' + 'expected to be a "bytes" instance') - self._datetime = msgpack_decode(data) + datetime, tz = msgpack_decode(data) + self._datetime = datetime + self._tz = tz return # The logic is same as in Tarantool, refer to datetime API. @@ -133,11 +153,20 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None, microsecond=microsecond, nanosecond=nanosecond) - if tzoffset != 0: - tzinfo = pytz.FixedOffset(tzoffset) - datetime = datetime.replace(tzinfo=tzinfo) + if tz != '': + if tz not in tt_timezones.timezoneToIndex: + raise ValueError(f'Unknown Tarantool timezone "{tz}"') - self._datetime = datetime + tzinfo = get_python_tzinfo(tz, ValueError) + self._datetime = datetime.replace(tzinfo=tzinfo) + self._tz = tz + elif tzoffset != 0: + tzinfo = pytz.FixedOffset(tzoffset) + self._datetime = datetime.replace(tzinfo=tzinfo) + self._tz = '' + else: + self._datetime = datetime + self._tz = '' def __eq__(self, other): if isinstance(other, Datetime): @@ -151,7 +180,7 @@ def __str__(self): return self._datetime.__str__() def __repr__(self): - return f'datetime: {self._datetime.__repr__()}' + return f'datetime: {self._datetime.__repr__()}, tz: "{self.tz}"' def __copy__(self): cls = self.__class__ @@ -206,6 +235,10 @@ def tzoffset(self): return compute_offset(self._datetime) return 0 + @property + def tz(self): + return self._tz + @property def value(self): return self._datetime.value @@ -214,7 +247,12 @@ def msgpack_encode(self): seconds = self.value // NSEC_IN_SEC nsec = self.nsec tzoffset = self.tzoffset - tzindex = 0 + + tz = self.tz + if tz != '': + tzindex = tt_timezones.timezoneToIndex[tz] + else: + tzindex = 0 buf = get_int_as_bytes(seconds, SECONDS_SIZE_BYTES) diff --git a/tarantool/msgpack_ext/types/timezones/__init__.py b/tarantool/msgpack_ext/types/timezones/__init__.py new file mode 100644 index 00000000..b5f2faf1 --- /dev/null +++ b/tarantool/msgpack_ext/types/timezones/__init__.py @@ -0,0 +1,9 @@ +from tarantool.msgpack_ext.types.timezones.timezones import ( + TZ_AMBIGUOUS, + indexToTimezone, + timezoneToIndex, + timezoneAbbrevInfo, +) + +__all__ = ['TZ_AMBIGUOUS', 'indexToTimezone', 'timezoneToIndex', + 'timezoneAbbrevInfo'] diff --git a/tarantool/msgpack_ext/types/timezones/gen-timezones.sh b/tarantool/msgpack_ext/types/timezones/gen-timezones.sh new file mode 100755 index 00000000..66610c4c --- /dev/null +++ b/tarantool/msgpack_ext/types/timezones/gen-timezones.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -xeuo pipefail + +SRC_COMMIT="9ee45289e01232b8df1413efea11db170ae3b3b4" +SRC_FILE=timezones.h +DST_FILE=timezones.py + +[ -e ${SRC_FILE} ] && rm ${SRC_FILE} +wget -O ${SRC_FILE} \ + https://raw.githubusercontent.com/tarantool/tarantool/${SRC_COMMIT}/src/lib/tzcode/timezones.h + +# We don't need aliases in indexToTimezone because Tarantool always replace it: +# +# tarantool> T = date.parse '2022-01-01T00:00 Pacific/Enderbury' +# --- +# ... +# tarantool> T +# --- +# - 2022-01-01T00:00:00 Pacific/Kanton +# ... +# +# So we can do the same and don't worry, be happy. + +cat < ${DST_FILE} +# Automatically generated by gen-timezones.sh + +TZ_UTC = 0x01 +TZ_RFC = 0x02 +TZ_MILITARY = 0x04 +TZ_AMBIGUOUS = 0x08 +TZ_NYI = 0x10 +TZ_OLSON = 0x20 +TZ_ALIAS = 0x40 +TZ_DST = 0x80 + +indexToTimezone = { +EOF + +grep ZONE_ABBREV ${SRC_FILE} | sed "s/ZONE_ABBREV( *//g" | sed "s/[),]//g" \ + | awk '{printf("\t%s : %s,\n", $1, $3)}' >> ${DST_FILE} +grep ZONE_UNIQUE ${SRC_FILE} | sed "s/ZONE_UNIQUE( *//g" | sed "s/[),]//g" \ + | awk '{printf("\t%s : %s,\n", $1, $2)}' >> ${DST_FILE} + +cat <> ${DST_FILE} +} + +timezoneToIndex = { +EOF + +grep ZONE_ABBREV ${SRC_FILE} | sed "s/ZONE_ABBREV( *//g" | sed "s/[),]//g" \ + | awk '{printf("\t%s : %s,\n", $3, $1)}' >> ${DST_FILE} +grep ZONE_UNIQUE ${SRC_FILE} | sed "s/ZONE_UNIQUE( *//g" | sed "s/[),]//g" \ + | awk '{printf("\t%s : %s,\n", $2, $1)}' >> ${DST_FILE} +grep ZONE_ALIAS ${SRC_FILE} | sed "s/ZONE_ALIAS( *//g" | sed "s/[),]//g" \ + | awk '{printf("\t%s : %s,\n", $2, $1)}' >> ${DST_FILE} + +cat <> ${DST_FILE} +} + +timezoneAbbrevInfo = { +EOF + +grep ZONE_ABBREV ${SRC_FILE} | sed "s/ZONE_ABBREV( *//g" | sed "s/[),]//g" \ + | awk '{printf("\t%s : {\"offset\" : %d, \"category\" : %s},\n", $3, $2, $4)}' >> ${DST_FILE} +echo "}" >> ${DST_FILE} + +rm timezones.h + +python validate_timezones.py diff --git a/tarantool/msgpack_ext/types/timezones/timezones.py b/tarantool/msgpack_ext/types/timezones/timezones.py new file mode 100644 index 00000000..bbb5df5c --- /dev/null +++ b/tarantool/msgpack_ext/types/timezones/timezones.py @@ -0,0 +1,1784 @@ +# Automatically generated by gen-timezones.sh + +TZ_UTC = 0x01 +TZ_RFC = 0x02 +TZ_MILITARY = 0x04 +TZ_AMBIGUOUS = 0x08 +TZ_NYI = 0x10 +TZ_OLSON = 0x20 +TZ_ALIAS = 0x40 +TZ_DST = 0x80 + +indexToTimezone = { + 1 : "A", + 2 : "B", + 3 : "C", + 4 : "D", + 5 : "E", + 6 : "F", + 7 : "G", + 8 : "H", + 9 : "I", + 10 : "K", + 11 : "L", + 12 : "M", + 13 : "N", + 14 : "O", + 15 : "P", + 16 : "Q", + 17 : "R", + 18 : "S", + 19 : "T", + 20 : "U", + 21 : "V", + 22 : "W", + 23 : "X", + 24 : "Y", + 25 : "Z", + 32 : "AT", + 40 : "BT", + 48 : "CT", + 56 : "ET", + 64 : "GT", + 72 : "IT", + 80 : "KT", + 88 : "MT", + 96 : "PT", + 104 : "ST", + 112 : "UT", + 120 : "WT", + 128 : "ACT", + 129 : "ADT", + 130 : "AET", + 131 : "AFT", + 132 : "AMT", + 133 : "AoE", + 134 : "ART", + 135 : "AST", + 136 : "AZT", + 144 : "BDT", + 145 : "BNT", + 146 : "BOT", + 147 : "BRT", + 148 : "BST", + 149 : "BTT", + 152 : "CAT", + 153 : "CCT", + 154 : "CDT", + 155 : "CET", + 156 : "CIT", + 157 : "CKT", + 158 : "CLT", + 159 : "COT", + 160 : "CST", + 161 : "CVT", + 162 : "CXT", + 168 : "EAT", + 169 : "ECT", + 170 : "EDT", + 171 : "EET", + 172 : "EGT", + 173 : "EST", + 176 : "FET", + 177 : "FJT", + 178 : "FKT", + 179 : "FNT", + 184 : "GET", + 185 : "GFT", + 186 : "GMT", + 187 : "GST", + 188 : "GYT", + 192 : "HAA", + 193 : "HAC", + 194 : "HAE", + 195 : "HAP", + 196 : "HAR", + 197 : "HAT", + 198 : "HDT", + 199 : "HKT", + 200 : "HLV", + 201 : "HNA", + 202 : "HNC", + 203 : "HNE", + 204 : "HNP", + 205 : "HNR", + 206 : "HNT", + 207 : "HST", + 208 : "ICT", + 209 : "IDT", + 210 : "IOT", + 211 : "IST", + 216 : "JST", + 224 : "KGT", + 225 : "KIT", + 226 : "KST", + 232 : "MCK", + 233 : "MDT", + 234 : "MEZ", + 235 : "MHT", + 236 : "MMT", + 237 : "MSD", + 238 : "MSK", + 239 : "MST", + 240 : "MUT", + 241 : "MVT", + 242 : "MYT", + 248 : "NCT", + 249 : "NDT", + 250 : "NFT", + 251 : "NPT", + 252 : "NRT", + 253 : "NST", + 254 : "NUT", + 256 : "OEZ", + 264 : "PDT", + 265 : "PET", + 266 : "PGT", + 267 : "PHT", + 268 : "PKT", + 269 : "PST", + 270 : "PWT", + 271 : "PYT", + 272 : "RET", + 280 : "SBT", + 281 : "SCT", + 282 : "SGT", + 283 : "SRT", + 284 : "SST", + 288 : "TFT", + 289 : "TJT", + 290 : "TKT", + 291 : "TLT", + 292 : "TMT", + 293 : "TOT", + 294 : "TRT", + 295 : "TVT", + 296 : "UTC", + 297 : "UYT", + 298 : "UZT", + 304 : "VET", + 305 : "VUT", + 312 : "WAT", + 313 : "WDT", + 314 : "WET", + 315 : "WEZ", + 316 : "WFT", + 317 : "WGT", + 318 : "WIB", + 319 : "WIT", + 320 : "WST", + 328 : "ACDT", + 329 : "ACST", + 330 : "ADST", + 331 : "AEDT", + 332 : "AEST", + 333 : "AKDT", + 334 : "AKST", + 335 : "ALMT", + 336 : "AMDT", + 337 : "AMST", + 338 : "ANAT", + 339 : "AQTT", + 340 : "AWDT", + 341 : "AWST", + 342 : "AZOT", + 343 : "AZST", + 344 : "BDST", + 345 : "BRST", + 352 : "CAST", + 353 : "CDST", + 354 : "CEDT", + 355 : "CEST", + 356 : "CHOT", + 357 : "ChST", + 358 : "CHUT", + 359 : "CIST", + 360 : "CLDT", + 361 : "CLST", + 368 : "DAVT", + 369 : "DDUT", + 376 : "EADT", + 377 : "EAST", + 378 : "ECST", + 379 : "EDST", + 380 : "EEDT", + 381 : "EEST", + 382 : "EGST", + 384 : "FJDT", + 385 : "FJST", + 386 : "FKDT", + 387 : "FKST", + 392 : "GALT", + 393 : "GAMT", + 394 : "GILT", + 400 : "HADT", + 401 : "HAST", + 402 : "HOVT", + 408 : "IRDT", + 409 : "IRKT", + 410 : "IRST", + 416 : "KOST", + 417 : "KRAT", + 418 : "KUYT", + 424 : "LHDT", + 425 : "LHST", + 426 : "LINT", + 432 : "MAGT", + 433 : "MART", + 434 : "MAWT", + 435 : "MDST", + 436 : "MESZ", + 440 : "NFDT", + 441 : "NOVT", + 442 : "NZDT", + 443 : "NZST", + 448 : "OESZ", + 449 : "OMST", + 450 : "ORAT", + 456 : "PDST", + 457 : "PETT", + 458 : "PHOT", + 459 : "PMDT", + 460 : "PMST", + 461 : "PONT", + 462 : "PYST", + 464 : "QYZT", + 472 : "ROTT", + 480 : "SAKT", + 481 : "SAMT", + 482 : "SAST", + 483 : "SRET", + 484 : "SYOT", + 488 : "TAHT", + 489 : "TOST", + 496 : "ULAT", + 497 : "UYST", + 504 : "VLAT", + 505 : "VOST", + 512 : "WAKT", + 513 : "WAST", + 514 : "WEDT", + 515 : "WEST", + 516 : "WESZ", + 517 : "WGST", + 518 : "WITA", + 520 : "YAKT", + 521 : "YAPT", + 522 : "YEKT", + 528 : "ACWST", + 529 : "ANAST", + 530 : "AZODT", + 531 : "AZOST", + 536 : "CHADT", + 537 : "CHAST", + 538 : "CHODT", + 539 : "CHOST", + 540 : "CIDST", + 544 : "EASST", + 545 : "EFATE", + 552 : "HOVDT", + 553 : "HOVST", + 560 : "IRKST", + 568 : "KRAST", + 576 : "MAGST", + 584 : "NACDT", + 585 : "NACST", + 586 : "NAEDT", + 587 : "NAEST", + 588 : "NAMDT", + 589 : "NAMST", + 590 : "NAPDT", + 591 : "NAPST", + 592 : "NOVST", + 600 : "OMSST", + 608 : "PETST", + 616 : "SAMST", + 624 : "ULAST", + 632 : "VLAST", + 640 : "WARST", + 648 : "YAKST", + 649 : "YEKST", + 656 : "CHODST", + 664 : "HOVDST", + 672 : "Africa/Abidjan", + 673 : "Africa/Algiers", + 674 : "Africa/Bissau", + 675 : "Africa/Cairo", + 676 : "Africa/Casablanca", + 677 : "Africa/Ceuta", + 678 : "Africa/El_Aaiun", + 679 : "Africa/Johannesburg", + 680 : "Africa/Juba", + 681 : "Africa/Khartoum", + 682 : "Africa/Lagos", + 683 : "Africa/Maputo", + 684 : "Africa/Monrovia", + 685 : "Africa/Nairobi", + 686 : "Africa/Ndjamena", + 687 : "Africa/Sao_Tome", + 688 : "Africa/Tripoli", + 689 : "Africa/Tunis", + 690 : "Africa/Windhoek", + 691 : "America/Adak", + 692 : "America/Anchorage", + 693 : "America/Araguaina", + 694 : "America/Argentina/Buenos_Aires", + 695 : "America/Argentina/Catamarca", + 696 : "America/Argentina/Cordoba", + 697 : "America/Argentina/Jujuy", + 698 : "America/Argentina/La_Rioja", + 699 : "America/Argentina/Mendoza", + 700 : "America/Argentina/Rio_Gallegos", + 701 : "America/Argentina/Salta", + 702 : "America/Argentina/San_Juan", + 703 : "America/Argentina/San_Luis", + 704 : "America/Argentina/Tucuman", + 705 : "America/Argentina/Ushuaia", + 706 : "America/Asuncion", + 707 : "America/Bahia", + 708 : "America/Bahia_Banderas", + 709 : "America/Barbados", + 710 : "America/Belem", + 711 : "America/Belize", + 712 : "America/Boa_Vista", + 713 : "America/Bogota", + 714 : "America/Boise", + 715 : "America/Cambridge_Bay", + 716 : "America/Campo_Grande", + 717 : "America/Cancun", + 718 : "America/Caracas", + 719 : "America/Cayenne", + 720 : "America/Chicago", + 721 : "America/Chihuahua", + 722 : "America/Costa_Rica", + 723 : "America/Cuiaba", + 724 : "America/Danmarkshavn", + 725 : "America/Dawson", + 726 : "America/Dawson_Creek", + 727 : "America/Denver", + 728 : "America/Detroit", + 729 : "America/Edmonton", + 730 : "America/Eirunepe", + 731 : "America/El_Salvador", + 732 : "America/Fort_Nelson", + 733 : "America/Fortaleza", + 734 : "America/Glace_Bay", + 735 : "America/Goose_Bay", + 736 : "America/Grand_Turk", + 737 : "America/Guatemala", + 738 : "America/Guayaquil", + 739 : "America/Guyana", + 740 : "America/Halifax", + 741 : "America/Havana", + 742 : "America/Hermosillo", + 743 : "America/Indiana/Indianapolis", + 744 : "America/Indiana/Knox", + 745 : "America/Indiana/Marengo", + 746 : "America/Indiana/Petersburg", + 747 : "America/Indiana/Tell_City", + 748 : "America/Indiana/Vevay", + 749 : "America/Indiana/Vincennes", + 750 : "America/Indiana/Winamac", + 751 : "America/Inuvik", + 752 : "America/Iqaluit", + 753 : "America/Jamaica", + 754 : "America/Juneau", + 755 : "America/Kentucky/Louisville", + 756 : "America/Kentucky/Monticello", + 757 : "America/La_Paz", + 758 : "America/Lima", + 759 : "America/Los_Angeles", + 760 : "America/Maceio", + 761 : "America/Managua", + 762 : "America/Manaus", + 763 : "America/Martinique", + 764 : "America/Matamoros", + 765 : "America/Mazatlan", + 766 : "America/Menominee", + 767 : "America/Merida", + 768 : "America/Metlakatla", + 769 : "America/Mexico_City", + 770 : "America/Miquelon", + 771 : "America/Moncton", + 772 : "America/Monterrey", + 773 : "America/Montevideo", + 774 : "America/New_York", + 775 : "America/Nipigon", + 776 : "America/Nome", + 777 : "America/Noronha", + 778 : "America/North_Dakota/Beulah", + 779 : "America/North_Dakota/Center", + 780 : "America/North_Dakota/New_Salem", + 781 : "America/Nuuk", + 782 : "America/Ojinaga", + 783 : "America/Panama", + 784 : "America/Pangnirtung", + 785 : "America/Paramaribo", + 786 : "America/Phoenix", + 787 : "America/Port-au-Prince", + 788 : "America/Porto_Velho", + 789 : "America/Puerto_Rico", + 790 : "America/Punta_Arenas", + 791 : "America/Rainy_River", + 792 : "America/Rankin_Inlet", + 793 : "America/Recife", + 794 : "America/Regina", + 795 : "America/Resolute", + 796 : "America/Rio_Branco", + 797 : "America/Santarem", + 798 : "America/Santiago", + 799 : "America/Santo_Domingo", + 800 : "America/Sao_Paulo", + 801 : "America/Scoresbysund", + 802 : "America/Sitka", + 803 : "America/St_Johns", + 804 : "America/Swift_Current", + 805 : "America/Tegucigalpa", + 806 : "America/Thule", + 807 : "America/Thunder_Bay", + 808 : "America/Tijuana", + 809 : "America/Toronto", + 810 : "America/Vancouver", + 811 : "America/Whitehorse", + 812 : "America/Winnipeg", + 813 : "America/Yakutat", + 814 : "America/Yellowknife", + 815 : "Antarctica/Casey", + 816 : "Antarctica/Davis", + 817 : "Antarctica/Macquarie", + 818 : "Antarctica/Mawson", + 819 : "Antarctica/Palmer", + 820 : "Antarctica/Rothera", + 821 : "Antarctica/Troll", + 822 : "Antarctica/Vostok", + 823 : "Asia/Almaty", + 824 : "Asia/Amman", + 825 : "Asia/Anadyr", + 826 : "Asia/Aqtau", + 827 : "Asia/Aqtobe", + 828 : "Asia/Ashgabat", + 829 : "Asia/Atyrau", + 830 : "Asia/Baghdad", + 831 : "Asia/Baku", + 832 : "Asia/Bangkok", + 833 : "Asia/Barnaul", + 834 : "Asia/Beirut", + 835 : "Asia/Bishkek", + 836 : "Asia/Brunei", + 837 : "Asia/Chita", + 838 : "Asia/Choibalsan", + 839 : "Asia/Colombo", + 840 : "Asia/Damascus", + 841 : "Asia/Dhaka", + 842 : "Asia/Dili", + 843 : "Asia/Dubai", + 844 : "Asia/Dushanbe", + 845 : "Asia/Famagusta", + 846 : "Asia/Gaza", + 847 : "Asia/Hebron", + 848 : "Asia/Ho_Chi_Minh", + 849 : "Asia/Hong_Kong", + 850 : "Asia/Hovd", + 851 : "Asia/Irkutsk", + 852 : "Asia/Jakarta", + 853 : "Asia/Jayapura", + 854 : "Asia/Jerusalem", + 855 : "Asia/Kabul", + 856 : "Asia/Kamchatka", + 857 : "Asia/Karachi", + 858 : "Asia/Kathmandu", + 859 : "Asia/Khandyga", + 860 : "Asia/Kolkata", + 861 : "Asia/Krasnoyarsk", + 862 : "Asia/Kuala_Lumpur", + 863 : "Asia/Kuching", + 864 : "Asia/Macau", + 865 : "Asia/Magadan", + 866 : "Asia/Makassar", + 867 : "Asia/Manila", + 868 : "Asia/Nicosia", + 869 : "Asia/Novokuznetsk", + 870 : "Asia/Novosibirsk", + 871 : "Asia/Omsk", + 872 : "Asia/Oral", + 873 : "Asia/Pontianak", + 874 : "Asia/Pyongyang", + 875 : "Asia/Qatar", + 876 : "Asia/Qostanay", + 877 : "Asia/Qyzylorda", + 878 : "Asia/Riyadh", + 879 : "Asia/Sakhalin", + 880 : "Asia/Samarkand", + 881 : "Asia/Seoul", + 882 : "Asia/Shanghai", + 883 : "Asia/Singapore", + 884 : "Asia/Srednekolymsk", + 885 : "Asia/Taipei", + 886 : "Asia/Tashkent", + 887 : "Asia/Tbilisi", + 888 : "Asia/Tehran", + 889 : "Asia/Thimphu", + 890 : "Asia/Tokyo", + 891 : "Asia/Tomsk", + 892 : "Asia/Ulaanbaatar", + 893 : "Asia/Urumqi", + 894 : "Asia/Ust-Nera", + 895 : "Asia/Vladivostok", + 896 : "Asia/Yakutsk", + 897 : "Asia/Yangon", + 898 : "Asia/Yekaterinburg", + 899 : "Asia/Yerevan", + 900 : "Atlantic/Azores", + 901 : "Atlantic/Bermuda", + 902 : "Atlantic/Canary", + 903 : "Atlantic/Cape_Verde", + 904 : "Atlantic/Faroe", + 905 : "Atlantic/Madeira", + 906 : "Atlantic/Reykjavik", + 907 : "Atlantic/South_Georgia", + 908 : "Atlantic/Stanley", + 909 : "Australia/Adelaide", + 910 : "Australia/Brisbane", + 911 : "Australia/Broken_Hill", + 912 : "Australia/Darwin", + 913 : "Australia/Eucla", + 914 : "Australia/Hobart", + 915 : "Australia/Lindeman", + 916 : "Australia/Lord_Howe", + 917 : "Australia/Melbourne", + 918 : "Australia/Perth", + 919 : "Australia/Sydney", + 920 : "Etc/GMT", + 921 : "Etc/UTC", + 922 : "Europe/Amsterdam", + 923 : "Europe/Andorra", + 924 : "Europe/Astrakhan", + 925 : "Europe/Athens", + 926 : "Europe/Belgrade", + 927 : "Europe/Berlin", + 928 : "Europe/Brussels", + 929 : "Europe/Bucharest", + 930 : "Europe/Budapest", + 931 : "Europe/Chisinau", + 932 : "Europe/Copenhagen", + 933 : "Europe/Dublin", + 934 : "Europe/Gibraltar", + 935 : "Europe/Helsinki", + 936 : "Europe/Istanbul", + 937 : "Europe/Kaliningrad", + 938 : "Europe/Kiev", + 939 : "Europe/Kirov", + 940 : "Europe/Lisbon", + 941 : "Europe/London", + 942 : "Europe/Luxembourg", + 943 : "Europe/Madrid", + 944 : "Europe/Malta", + 945 : "Europe/Minsk", + 946 : "Europe/Monaco", + 947 : "Europe/Moscow", + 948 : "Europe/Oslo", + 949 : "Europe/Paris", + 950 : "Europe/Prague", + 951 : "Europe/Riga", + 952 : "Europe/Rome", + 953 : "Europe/Samara", + 954 : "Europe/Saratov", + 955 : "Europe/Simferopol", + 956 : "Europe/Sofia", + 957 : "Europe/Stockholm", + 958 : "Europe/Tallinn", + 959 : "Europe/Tirane", + 960 : "Europe/Ulyanovsk", + 961 : "Europe/Uzhgorod", + 962 : "Europe/Vienna", + 963 : "Europe/Vilnius", + 964 : "Europe/Volgograd", + 965 : "Europe/Warsaw", + 966 : "Europe/Zaporozhye", + 967 : "Europe/Zurich", + 968 : "Indian/Chagos", + 969 : "Indian/Christmas", + 970 : "Indian/Cocos", + 971 : "Indian/Kerguelen", + 972 : "Indian/Mahe", + 973 : "Indian/Maldives", + 974 : "Indian/Mauritius", + 975 : "Indian/Reunion", + 976 : "Pacific/Apia", + 977 : "Pacific/Auckland", + 978 : "Pacific/Bougainville", + 979 : "Pacific/Chatham", + 980 : "Pacific/Chuuk", + 981 : "Pacific/Easter", + 982 : "Pacific/Efate", + 983 : "Pacific/Fakaofo", + 984 : "Pacific/Fiji", + 985 : "Pacific/Funafuti", + 986 : "Pacific/Galapagos", + 987 : "Pacific/Gambier", + 988 : "Pacific/Guadalcanal", + 989 : "Pacific/Guam", + 990 : "Pacific/Honolulu", + 991 : "Pacific/Kanton", + 992 : "Pacific/Kiritimati", + 993 : "Pacific/Kosrae", + 994 : "Pacific/Kwajalein", + 995 : "Pacific/Majuro", + 996 : "Pacific/Marquesas", + 997 : "Pacific/Nauru", + 998 : "Pacific/Niue", + 999 : "Pacific/Norfolk", + 1000 : "Pacific/Noumea", + 1001 : "Pacific/Pago_Pago", + 1002 : "Pacific/Palau", + 1003 : "Pacific/Pitcairn", + 1004 : "Pacific/Pohnpei", + 1005 : "Pacific/Port_Moresby", + 1006 : "Pacific/Rarotonga", + 1007 : "Pacific/Tahiti", + 1008 : "Pacific/Tarawa", + 1009 : "Pacific/Tongatapu", + 1010 : "Pacific/Wake", + 1011 : "Pacific/Wallis", +} + +timezoneToIndex = { + "A" : 1, + "B" : 2, + "C" : 3, + "D" : 4, + "E" : 5, + "F" : 6, + "G" : 7, + "H" : 8, + "I" : 9, + "K" : 10, + "L" : 11, + "M" : 12, + "N" : 13, + "O" : 14, + "P" : 15, + "Q" : 16, + "R" : 17, + "S" : 18, + "T" : 19, + "U" : 20, + "V" : 21, + "W" : 22, + "X" : 23, + "Y" : 24, + "Z" : 25, + "AT" : 32, + "BT" : 40, + "CT" : 48, + "ET" : 56, + "GT" : 64, + "IT" : 72, + "KT" : 80, + "MT" : 88, + "PT" : 96, + "ST" : 104, + "UT" : 112, + "WT" : 120, + "ACT" : 128, + "ADT" : 129, + "AET" : 130, + "AFT" : 131, + "AMT" : 132, + "AoE" : 133, + "ART" : 134, + "AST" : 135, + "AZT" : 136, + "BDT" : 144, + "BNT" : 145, + "BOT" : 146, + "BRT" : 147, + "BST" : 148, + "BTT" : 149, + "CAT" : 152, + "CCT" : 153, + "CDT" : 154, + "CET" : 155, + "CIT" : 156, + "CKT" : 157, + "CLT" : 158, + "COT" : 159, + "CST" : 160, + "CVT" : 161, + "CXT" : 162, + "EAT" : 168, + "ECT" : 169, + "EDT" : 170, + "EET" : 171, + "EGT" : 172, + "EST" : 173, + "FET" : 176, + "FJT" : 177, + "FKT" : 178, + "FNT" : 179, + "GET" : 184, + "GFT" : 185, + "GMT" : 186, + "GST" : 187, + "GYT" : 188, + "HAA" : 192, + "HAC" : 193, + "HAE" : 194, + "HAP" : 195, + "HAR" : 196, + "HAT" : 197, + "HDT" : 198, + "HKT" : 199, + "HLV" : 200, + "HNA" : 201, + "HNC" : 202, + "HNE" : 203, + "HNP" : 204, + "HNR" : 205, + "HNT" : 206, + "HST" : 207, + "ICT" : 208, + "IDT" : 209, + "IOT" : 210, + "IST" : 211, + "JST" : 216, + "KGT" : 224, + "KIT" : 225, + "KST" : 226, + "MCK" : 232, + "MDT" : 233, + "MEZ" : 234, + "MHT" : 235, + "MMT" : 236, + "MSD" : 237, + "MSK" : 238, + "MST" : 239, + "MUT" : 240, + "MVT" : 241, + "MYT" : 242, + "NCT" : 248, + "NDT" : 249, + "NFT" : 250, + "NPT" : 251, + "NRT" : 252, + "NST" : 253, + "NUT" : 254, + "OEZ" : 256, + "PDT" : 264, + "PET" : 265, + "PGT" : 266, + "PHT" : 267, + "PKT" : 268, + "PST" : 269, + "PWT" : 270, + "PYT" : 271, + "RET" : 272, + "SBT" : 280, + "SCT" : 281, + "SGT" : 282, + "SRT" : 283, + "SST" : 284, + "TFT" : 288, + "TJT" : 289, + "TKT" : 290, + "TLT" : 291, + "TMT" : 292, + "TOT" : 293, + "TRT" : 294, + "TVT" : 295, + "UTC" : 296, + "UYT" : 297, + "UZT" : 298, + "VET" : 304, + "VUT" : 305, + "WAT" : 312, + "WDT" : 313, + "WET" : 314, + "WEZ" : 315, + "WFT" : 316, + "WGT" : 317, + "WIB" : 318, + "WIT" : 319, + "WST" : 320, + "ACDT" : 328, + "ACST" : 329, + "ADST" : 330, + "AEDT" : 331, + "AEST" : 332, + "AKDT" : 333, + "AKST" : 334, + "ALMT" : 335, + "AMDT" : 336, + "AMST" : 337, + "ANAT" : 338, + "AQTT" : 339, + "AWDT" : 340, + "AWST" : 341, + "AZOT" : 342, + "AZST" : 343, + "BDST" : 344, + "BRST" : 345, + "CAST" : 352, + "CDST" : 353, + "CEDT" : 354, + "CEST" : 355, + "CHOT" : 356, + "ChST" : 357, + "CHUT" : 358, + "CIST" : 359, + "CLDT" : 360, + "CLST" : 361, + "DAVT" : 368, + "DDUT" : 369, + "EADT" : 376, + "EAST" : 377, + "ECST" : 378, + "EDST" : 379, + "EEDT" : 380, + "EEST" : 381, + "EGST" : 382, + "FJDT" : 384, + "FJST" : 385, + "FKDT" : 386, + "FKST" : 387, + "GALT" : 392, + "GAMT" : 393, + "GILT" : 394, + "HADT" : 400, + "HAST" : 401, + "HOVT" : 402, + "IRDT" : 408, + "IRKT" : 409, + "IRST" : 410, + "KOST" : 416, + "KRAT" : 417, + "KUYT" : 418, + "LHDT" : 424, + "LHST" : 425, + "LINT" : 426, + "MAGT" : 432, + "MART" : 433, + "MAWT" : 434, + "MDST" : 435, + "MESZ" : 436, + "NFDT" : 440, + "NOVT" : 441, + "NZDT" : 442, + "NZST" : 443, + "OESZ" : 448, + "OMST" : 449, + "ORAT" : 450, + "PDST" : 456, + "PETT" : 457, + "PHOT" : 458, + "PMDT" : 459, + "PMST" : 460, + "PONT" : 461, + "PYST" : 462, + "QYZT" : 464, + "ROTT" : 472, + "SAKT" : 480, + "SAMT" : 481, + "SAST" : 482, + "SRET" : 483, + "SYOT" : 484, + "TAHT" : 488, + "TOST" : 489, + "ULAT" : 496, + "UYST" : 497, + "VLAT" : 504, + "VOST" : 505, + "WAKT" : 512, + "WAST" : 513, + "WEDT" : 514, + "WEST" : 515, + "WESZ" : 516, + "WGST" : 517, + "WITA" : 518, + "YAKT" : 520, + "YAPT" : 521, + "YEKT" : 522, + "ACWST" : 528, + "ANAST" : 529, + "AZODT" : 530, + "AZOST" : 531, + "CHADT" : 536, + "CHAST" : 537, + "CHODT" : 538, + "CHOST" : 539, + "CIDST" : 540, + "EASST" : 544, + "EFATE" : 545, + "HOVDT" : 552, + "HOVST" : 553, + "IRKST" : 560, + "KRAST" : 568, + "MAGST" : 576, + "NACDT" : 584, + "NACST" : 585, + "NAEDT" : 586, + "NAEST" : 587, + "NAMDT" : 588, + "NAMST" : 589, + "NAPDT" : 590, + "NAPST" : 591, + "NOVST" : 592, + "OMSST" : 600, + "PETST" : 608, + "SAMST" : 616, + "ULAST" : 624, + "VLAST" : 632, + "WARST" : 640, + "YAKST" : 648, + "YEKST" : 649, + "CHODST" : 656, + "HOVDST" : 664, + "Africa/Abidjan" : 672, + "Africa/Algiers" : 673, + "Africa/Bissau" : 674, + "Africa/Cairo" : 675, + "Africa/Casablanca" : 676, + "Africa/Ceuta" : 677, + "Africa/El_Aaiun" : 678, + "Africa/Johannesburg" : 679, + "Africa/Juba" : 680, + "Africa/Khartoum" : 681, + "Africa/Lagos" : 682, + "Africa/Maputo" : 683, + "Africa/Monrovia" : 684, + "Africa/Nairobi" : 685, + "Africa/Ndjamena" : 686, + "Africa/Sao_Tome" : 687, + "Africa/Tripoli" : 688, + "Africa/Tunis" : 689, + "Africa/Windhoek" : 690, + "America/Adak" : 691, + "America/Anchorage" : 692, + "America/Araguaina" : 693, + "America/Argentina/Buenos_Aires" : 694, + "America/Argentina/Catamarca" : 695, + "America/Argentina/Cordoba" : 696, + "America/Argentina/Jujuy" : 697, + "America/Argentina/La_Rioja" : 698, + "America/Argentina/Mendoza" : 699, + "America/Argentina/Rio_Gallegos" : 700, + "America/Argentina/Salta" : 701, + "America/Argentina/San_Juan" : 702, + "America/Argentina/San_Luis" : 703, + "America/Argentina/Tucuman" : 704, + "America/Argentina/Ushuaia" : 705, + "America/Asuncion" : 706, + "America/Bahia" : 707, + "America/Bahia_Banderas" : 708, + "America/Barbados" : 709, + "America/Belem" : 710, + "America/Belize" : 711, + "America/Boa_Vista" : 712, + "America/Bogota" : 713, + "America/Boise" : 714, + "America/Cambridge_Bay" : 715, + "America/Campo_Grande" : 716, + "America/Cancun" : 717, + "America/Caracas" : 718, + "America/Cayenne" : 719, + "America/Chicago" : 720, + "America/Chihuahua" : 721, + "America/Costa_Rica" : 722, + "America/Cuiaba" : 723, + "America/Danmarkshavn" : 724, + "America/Dawson" : 725, + "America/Dawson_Creek" : 726, + "America/Denver" : 727, + "America/Detroit" : 728, + "America/Edmonton" : 729, + "America/Eirunepe" : 730, + "America/El_Salvador" : 731, + "America/Fort_Nelson" : 732, + "America/Fortaleza" : 733, + "America/Glace_Bay" : 734, + "America/Goose_Bay" : 735, + "America/Grand_Turk" : 736, + "America/Guatemala" : 737, + "America/Guayaquil" : 738, + "America/Guyana" : 739, + "America/Halifax" : 740, + "America/Havana" : 741, + "America/Hermosillo" : 742, + "America/Indiana/Indianapolis" : 743, + "America/Indiana/Knox" : 744, + "America/Indiana/Marengo" : 745, + "America/Indiana/Petersburg" : 746, + "America/Indiana/Tell_City" : 747, + "America/Indiana/Vevay" : 748, + "America/Indiana/Vincennes" : 749, + "America/Indiana/Winamac" : 750, + "America/Inuvik" : 751, + "America/Iqaluit" : 752, + "America/Jamaica" : 753, + "America/Juneau" : 754, + "America/Kentucky/Louisville" : 755, + "America/Kentucky/Monticello" : 756, + "America/La_Paz" : 757, + "America/Lima" : 758, + "America/Los_Angeles" : 759, + "America/Maceio" : 760, + "America/Managua" : 761, + "America/Manaus" : 762, + "America/Martinique" : 763, + "America/Matamoros" : 764, + "America/Mazatlan" : 765, + "America/Menominee" : 766, + "America/Merida" : 767, + "America/Metlakatla" : 768, + "America/Mexico_City" : 769, + "America/Miquelon" : 770, + "America/Moncton" : 771, + "America/Monterrey" : 772, + "America/Montevideo" : 773, + "America/New_York" : 774, + "America/Nipigon" : 775, + "America/Nome" : 776, + "America/Noronha" : 777, + "America/North_Dakota/Beulah" : 778, + "America/North_Dakota/Center" : 779, + "America/North_Dakota/New_Salem" : 780, + "America/Nuuk" : 781, + "America/Ojinaga" : 782, + "America/Panama" : 783, + "America/Pangnirtung" : 784, + "America/Paramaribo" : 785, + "America/Phoenix" : 786, + "America/Port-au-Prince" : 787, + "America/Porto_Velho" : 788, + "America/Puerto_Rico" : 789, + "America/Punta_Arenas" : 790, + "America/Rainy_River" : 791, + "America/Rankin_Inlet" : 792, + "America/Recife" : 793, + "America/Regina" : 794, + "America/Resolute" : 795, + "America/Rio_Branco" : 796, + "America/Santarem" : 797, + "America/Santiago" : 798, + "America/Santo_Domingo" : 799, + "America/Sao_Paulo" : 800, + "America/Scoresbysund" : 801, + "America/Sitka" : 802, + "America/St_Johns" : 803, + "America/Swift_Current" : 804, + "America/Tegucigalpa" : 805, + "America/Thule" : 806, + "America/Thunder_Bay" : 807, + "America/Tijuana" : 808, + "America/Toronto" : 809, + "America/Vancouver" : 810, + "America/Whitehorse" : 811, + "America/Winnipeg" : 812, + "America/Yakutat" : 813, + "America/Yellowknife" : 814, + "Antarctica/Casey" : 815, + "Antarctica/Davis" : 816, + "Antarctica/Macquarie" : 817, + "Antarctica/Mawson" : 818, + "Antarctica/Palmer" : 819, + "Antarctica/Rothera" : 820, + "Antarctica/Troll" : 821, + "Antarctica/Vostok" : 822, + "Asia/Almaty" : 823, + "Asia/Amman" : 824, + "Asia/Anadyr" : 825, + "Asia/Aqtau" : 826, + "Asia/Aqtobe" : 827, + "Asia/Ashgabat" : 828, + "Asia/Atyrau" : 829, + "Asia/Baghdad" : 830, + "Asia/Baku" : 831, + "Asia/Bangkok" : 832, + "Asia/Barnaul" : 833, + "Asia/Beirut" : 834, + "Asia/Bishkek" : 835, + "Asia/Brunei" : 836, + "Asia/Chita" : 837, + "Asia/Choibalsan" : 838, + "Asia/Colombo" : 839, + "Asia/Damascus" : 840, + "Asia/Dhaka" : 841, + "Asia/Dili" : 842, + "Asia/Dubai" : 843, + "Asia/Dushanbe" : 844, + "Asia/Famagusta" : 845, + "Asia/Gaza" : 846, + "Asia/Hebron" : 847, + "Asia/Ho_Chi_Minh" : 848, + "Asia/Hong_Kong" : 849, + "Asia/Hovd" : 850, + "Asia/Irkutsk" : 851, + "Asia/Jakarta" : 852, + "Asia/Jayapura" : 853, + "Asia/Jerusalem" : 854, + "Asia/Kabul" : 855, + "Asia/Kamchatka" : 856, + "Asia/Karachi" : 857, + "Asia/Kathmandu" : 858, + "Asia/Khandyga" : 859, + "Asia/Kolkata" : 860, + "Asia/Krasnoyarsk" : 861, + "Asia/Kuala_Lumpur" : 862, + "Asia/Kuching" : 863, + "Asia/Macau" : 864, + "Asia/Magadan" : 865, + "Asia/Makassar" : 866, + "Asia/Manila" : 867, + "Asia/Nicosia" : 868, + "Asia/Novokuznetsk" : 869, + "Asia/Novosibirsk" : 870, + "Asia/Omsk" : 871, + "Asia/Oral" : 872, + "Asia/Pontianak" : 873, + "Asia/Pyongyang" : 874, + "Asia/Qatar" : 875, + "Asia/Qostanay" : 876, + "Asia/Qyzylorda" : 877, + "Asia/Riyadh" : 878, + "Asia/Sakhalin" : 879, + "Asia/Samarkand" : 880, + "Asia/Seoul" : 881, + "Asia/Shanghai" : 882, + "Asia/Singapore" : 883, + "Asia/Srednekolymsk" : 884, + "Asia/Taipei" : 885, + "Asia/Tashkent" : 886, + "Asia/Tbilisi" : 887, + "Asia/Tehran" : 888, + "Asia/Thimphu" : 889, + "Asia/Tokyo" : 890, + "Asia/Tomsk" : 891, + "Asia/Ulaanbaatar" : 892, + "Asia/Urumqi" : 893, + "Asia/Ust-Nera" : 894, + "Asia/Vladivostok" : 895, + "Asia/Yakutsk" : 896, + "Asia/Yangon" : 897, + "Asia/Yekaterinburg" : 898, + "Asia/Yerevan" : 899, + "Atlantic/Azores" : 900, + "Atlantic/Bermuda" : 901, + "Atlantic/Canary" : 902, + "Atlantic/Cape_Verde" : 903, + "Atlantic/Faroe" : 904, + "Atlantic/Madeira" : 905, + "Atlantic/Reykjavik" : 906, + "Atlantic/South_Georgia" : 907, + "Atlantic/Stanley" : 908, + "Australia/Adelaide" : 909, + "Australia/Brisbane" : 910, + "Australia/Broken_Hill" : 911, + "Australia/Darwin" : 912, + "Australia/Eucla" : 913, + "Australia/Hobart" : 914, + "Australia/Lindeman" : 915, + "Australia/Lord_Howe" : 916, + "Australia/Melbourne" : 917, + "Australia/Perth" : 918, + "Australia/Sydney" : 919, + "Etc/GMT" : 920, + "Etc/UTC" : 921, + "Europe/Amsterdam" : 922, + "Europe/Andorra" : 923, + "Europe/Astrakhan" : 924, + "Europe/Athens" : 925, + "Europe/Belgrade" : 926, + "Europe/Berlin" : 927, + "Europe/Brussels" : 928, + "Europe/Bucharest" : 929, + "Europe/Budapest" : 930, + "Europe/Chisinau" : 931, + "Europe/Copenhagen" : 932, + "Europe/Dublin" : 933, + "Europe/Gibraltar" : 934, + "Europe/Helsinki" : 935, + "Europe/Istanbul" : 936, + "Europe/Kaliningrad" : 937, + "Europe/Kiev" : 938, + "Europe/Kirov" : 939, + "Europe/Lisbon" : 940, + "Europe/London" : 941, + "Europe/Luxembourg" : 942, + "Europe/Madrid" : 943, + "Europe/Malta" : 944, + "Europe/Minsk" : 945, + "Europe/Monaco" : 946, + "Europe/Moscow" : 947, + "Europe/Oslo" : 948, + "Europe/Paris" : 949, + "Europe/Prague" : 950, + "Europe/Riga" : 951, + "Europe/Rome" : 952, + "Europe/Samara" : 953, + "Europe/Saratov" : 954, + "Europe/Simferopol" : 955, + "Europe/Sofia" : 956, + "Europe/Stockholm" : 957, + "Europe/Tallinn" : 958, + "Europe/Tirane" : 959, + "Europe/Ulyanovsk" : 960, + "Europe/Uzhgorod" : 961, + "Europe/Vienna" : 962, + "Europe/Vilnius" : 963, + "Europe/Volgograd" : 964, + "Europe/Warsaw" : 965, + "Europe/Zaporozhye" : 966, + "Europe/Zurich" : 967, + "Indian/Chagos" : 968, + "Indian/Christmas" : 969, + "Indian/Cocos" : 970, + "Indian/Kerguelen" : 971, + "Indian/Mahe" : 972, + "Indian/Maldives" : 973, + "Indian/Mauritius" : 974, + "Indian/Reunion" : 975, + "Pacific/Apia" : 976, + "Pacific/Auckland" : 977, + "Pacific/Bougainville" : 978, + "Pacific/Chatham" : 979, + "Pacific/Chuuk" : 980, + "Pacific/Easter" : 981, + "Pacific/Efate" : 982, + "Pacific/Fakaofo" : 983, + "Pacific/Fiji" : 984, + "Pacific/Funafuti" : 985, + "Pacific/Galapagos" : 986, + "Pacific/Gambier" : 987, + "Pacific/Guadalcanal" : 988, + "Pacific/Guam" : 989, + "Pacific/Honolulu" : 990, + "Pacific/Kanton" : 991, + "Pacific/Kiritimati" : 992, + "Pacific/Kosrae" : 993, + "Pacific/Kwajalein" : 994, + "Pacific/Majuro" : 995, + "Pacific/Marquesas" : 996, + "Pacific/Nauru" : 997, + "Pacific/Niue" : 998, + "Pacific/Norfolk" : 999, + "Pacific/Noumea" : 1000, + "Pacific/Pago_Pago" : 1001, + "Pacific/Palau" : 1002, + "Pacific/Pitcairn" : 1003, + "Pacific/Pohnpei" : 1004, + "Pacific/Port_Moresby" : 1005, + "Pacific/Rarotonga" : 1006, + "Pacific/Tahiti" : 1007, + "Pacific/Tarawa" : 1008, + "Pacific/Tongatapu" : 1009, + "Pacific/Wake" : 1010, + "Pacific/Wallis" : 1011, + "Africa/Accra" : 672, + "Africa/Addis_Ababa" : 685, + "Africa/Asmara" : 685, + "Africa/Asmera" : 685, + "Africa/Bamako" : 672, + "Africa/Bangui" : 682, + "Africa/Banjul" : 672, + "Africa/Blantyre" : 683, + "Africa/Brazzaville" : 682, + "Africa/Bujumbura" : 683, + "Africa/Conakry" : 672, + "Africa/Dakar" : 672, + "Africa/Dar_es_Salaam" : 685, + "Africa/Djibouti" : 685, + "Africa/Douala" : 682, + "Africa/Freetown" : 672, + "Africa/Gaborone" : 683, + "Africa/Harare" : 683, + "Africa/Kampala" : 685, + "Africa/Kigali" : 683, + "Africa/Kinshasa" : 682, + "Africa/Libreville" : 682, + "Africa/Lome" : 672, + "Africa/Luanda" : 682, + "Africa/Lubumbashi" : 683, + "Africa/Lusaka" : 683, + "Africa/Malabo" : 682, + "Africa/Maseru" : 679, + "Africa/Mbabane" : 679, + "Africa/Mogadishu" : 685, + "Africa/Niamey" : 682, + "Africa/Nouakchott" : 672, + "Africa/Ouagadougou" : 672, + "Africa/Porto-Novo" : 682, + "Africa/Timbuktu" : 672, + "America/Anguilla" : 789, + "America/Antigua" : 789, + "America/Argentina/ComodRivadavia" : 695, + "America/Aruba" : 789, + "America/Atikokan" : 783, + "America/Atka" : 691, + "America/Blanc-Sablon" : 789, + "America/Buenos_Aires" : 694, + "America/Catamarca" : 695, + "America/Cayman" : 783, + "America/Coral_Harbour" : 783, + "America/Cordoba" : 696, + "America/Creston" : 786, + "America/Curacao" : 789, + "America/Dominica" : 789, + "America/Ensenada" : 808, + "America/Fort_Wayne" : 743, + "America/Godthab" : 781, + "America/Grenada" : 789, + "America/Guadeloupe" : 789, + "America/Indianapolis" : 743, + "America/Jujuy" : 697, + "America/Knox_IN" : 744, + "America/Kralendijk" : 789, + "America/Louisville" : 755, + "America/Lower_Princes" : 789, + "America/Marigot" : 789, + "America/Mendoza" : 699, + "America/Montreal" : 809, + "America/Montserrat" : 789, + "America/Nassau" : 809, + "America/Port_of_Spain" : 789, + "America/Porto_Acre" : 796, + "America/Rosario" : 696, + "America/Santa_Isabel" : 808, + "America/Shiprock" : 727, + "America/St_Barthelemy" : 789, + "America/St_Kitts" : 789, + "America/St_Lucia" : 789, + "America/St_Thomas" : 789, + "America/St_Vincent" : 789, + "America/Tortola" : 789, + "America/Virgin" : 789, + "Antarctica/DumontDUrville" : 1005, + "Antarctica/McMurdo" : 977, + "Antarctica/South_Pole" : 977, + "Antarctica/Syowa" : 878, + "Arctic/Longyearbyen" : 948, + "Asia/Aden" : 878, + "Asia/Ashkhabad" : 828, + "Asia/Bahrain" : 875, + "Asia/Calcutta" : 860, + "Asia/Chongqing" : 882, + "Asia/Chungking" : 882, + "Asia/Dacca" : 841, + "Asia/Harbin" : 882, + "Asia/Istanbul" : 936, + "Asia/Kashgar" : 893, + "Asia/Katmandu" : 858, + "Asia/Kuwait" : 878, + "Asia/Macao" : 864, + "Asia/Muscat" : 843, + "Asia/Phnom_Penh" : 832, + "Asia/Rangoon" : 897, + "Asia/Saigon" : 848, + "Asia/Tel_Aviv" : 854, + "Asia/Thimbu" : 889, + "Asia/Ujung_Pandang" : 866, + "Asia/Ulan_Bator" : 892, + "Asia/Vientiane" : 832, + "Atlantic/Faeroe" : 904, + "Atlantic/Jan_Mayen" : 948, + "Atlantic/St_Helena" : 672, + "Australia/ACT" : 919, + "Australia/Canberra" : 919, + "Australia/Currie" : 914, + "Australia/LHI" : 916, + "Australia/NSW" : 919, + "Australia/North" : 912, + "Australia/Queensland" : 910, + "Australia/South" : 909, + "Australia/Tasmania" : 914, + "Australia/Victoria" : 917, + "Australia/West" : 918, + "Australia/Yancowinna" : 911, + "Brazil/Acre" : 796, + "Brazil/DeNoronha" : 777, + "Brazil/East" : 800, + "Brazil/West" : 762, + "Canada/Atlantic" : 740, + "Canada/Central" : 812, + "Canada/Eastern" : 809, + "Canada/Mountain" : 729, + "Canada/Newfoundland" : 803, + "Canada/Pacific" : 810, + "Canada/Saskatchewan" : 794, + "Canada/Yukon" : 811, + "Chile/Continental" : 798, + "Chile/EasterIsland" : 981, + "Cuba" : 741, + "Egypt" : 675, + "Eire" : 933, + "Etc/GMT+0" : 920, + "Etc/GMT-0" : 920, + "Etc/GMT0" : 920, + "Etc/Greenwich" : 920, + "Etc/UCT" : 921, + "Etc/Universal" : 921, + "Etc/Zulu" : 921, + "Europe/Belfast" : 941, + "Europe/Bratislava" : 950, + "Europe/Busingen" : 967, + "Europe/Guernsey" : 941, + "Europe/Isle_of_Man" : 941, + "Europe/Jersey" : 941, + "Europe/Ljubljana" : 926, + "Europe/Mariehamn" : 935, + "Europe/Nicosia" : 868, + "Europe/Podgorica" : 926, + "Europe/San_Marino" : 952, + "Europe/Sarajevo" : 926, + "Europe/Skopje" : 926, + "Europe/Tiraspol" : 931, + "Europe/Vaduz" : 967, + "Europe/Vatican" : 952, + "Europe/Zagreb" : 926, + "GB" : 941, + "GB-Eire" : 941, + "GMT+0" : 920, + "GMT-0" : 920, + "GMT0" : 920, + "Greenwich" : 920, + "Hongkong" : 849, + "Iceland" : 906, + "Indian/Antananarivo" : 685, + "Indian/Comoro" : 685, + "Indian/Mayotte" : 685, + "Iran" : 888, + "Israel" : 854, + "Jamaica" : 753, + "Japan" : 890, + "Kwajalein" : 994, + "Libya" : 688, + "Mexico/BajaNorte" : 808, + "Mexico/BajaSur" : 765, + "Mexico/General" : 769, + "NZ" : 977, + "NZ-CHAT" : 979, + "Navajo" : 727, + "PRC" : 882, + "Pacific/Enderbury" : 991, + "Pacific/Johnston" : 990, + "Pacific/Midway" : 1001, + "Pacific/Ponape" : 1004, + "Pacific/Saipan" : 989, + "Pacific/Samoa" : 1001, + "Pacific/Truk" : 980, + "Pacific/Yap" : 980, + "Poland" : 965, + "Portugal" : 940, + "ROC" : 885, + "ROK" : 881, + "Singapore" : 883, + "Turkey" : 936, + "UCT" : 921, + "US/Alaska" : 692, + "US/Aleutian" : 691, + "US/Arizona" : 786, + "US/Central" : 720, + "US/East-Indiana" : 743, + "US/Eastern" : 774, + "US/Hawaii" : 990, + "US/Indiana-Starke" : 744, + "US/Michigan" : 728, + "US/Mountain" : 727, + "US/Pacific" : 759, + "US/Samoa" : 1001, + "Universal" : 921, + "W-SU" : 947, + "Zulu" : 921, +} + +timezoneAbbrevInfo = { + "A" : {"offset" : 60, "category" : TZ_MILITARY}, + "B" : {"offset" : 120, "category" : TZ_MILITARY}, + "C" : {"offset" : 180, "category" : TZ_MILITARY}, + "D" : {"offset" : 240, "category" : TZ_MILITARY}, + "E" : {"offset" : 300, "category" : TZ_MILITARY}, + "F" : {"offset" : 360, "category" : TZ_MILITARY}, + "G" : {"offset" : 420, "category" : TZ_MILITARY}, + "H" : {"offset" : 480, "category" : TZ_MILITARY}, + "I" : {"offset" : 540, "category" : TZ_MILITARY}, + "K" : {"offset" : 600, "category" : TZ_MILITARY}, + "L" : {"offset" : 660, "category" : TZ_MILITARY}, + "M" : {"offset" : 720, "category" : TZ_MILITARY}, + "N" : {"offset" : -60, "category" : TZ_MILITARY}, + "O" : {"offset" : -120, "category" : TZ_MILITARY}, + "P" : {"offset" : -180, "category" : TZ_MILITARY}, + "Q" : {"offset" : -240, "category" : TZ_MILITARY}, + "R" : {"offset" : -300, "category" : TZ_MILITARY}, + "S" : {"offset" : -360, "category" : TZ_MILITARY}, + "T" : {"offset" : -420, "category" : TZ_MILITARY}, + "U" : {"offset" : -480, "category" : TZ_MILITARY}, + "V" : {"offset" : -540, "category" : TZ_MILITARY}, + "W" : {"offset" : -600, "category" : TZ_MILITARY}, + "X" : {"offset" : -660, "category" : TZ_MILITARY}, + "Y" : {"offset" : -720, "category" : TZ_MILITARY}, + "Z" : {"offset" : 0, "category" : TZ_MILITARY}, + "AT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "BT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "CT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "ET" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "GT" : {"offset" : 0, "category" : 0}, + "IT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "KT" : {"offset" : 540, "category" : 0}, + "MT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "PT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "ST" : {"offset" : 780, "category" : 0}, + "UT" : {"offset" : 0, "category" : TZ_UTC|TZ_RFC}, + "WT" : {"offset" : 0, "category" : 0}, + "ACT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "ADT" : {"offset" : 0, "category" : TZ_AMBIGUOUS|TZ_DST}, + "AET" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "AFT" : {"offset" : 270, "category" : 0}, + "AMT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "AoE" : {"offset" : -720, "category" : 0}, + "ART" : {"offset" : -180, "category" : 0}, + "AST" : {"offset" : 0, "category" : TZ_AMBIGUOUS|TZ_DST}, + "AZT" : {"offset" : 240, "category" : 0}, + "BDT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "BNT" : {"offset" : 480, "category" : 0}, + "BOT" : {"offset" : -240, "category" : 0}, + "BRT" : {"offset" : -180, "category" : 0}, + "BST" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "BTT" : {"offset" : 360, "category" : 0}, + "CAT" : {"offset" : 120, "category" : 0}, + "CCT" : {"offset" : 390, "category" : 0}, + "CDT" : {"offset" : -300, "category" : TZ_RFC|TZ_AMBIGUOUS|TZ_DST}, + "CET" : {"offset" : 60, "category" : 0}, + "CIT" : {"offset" : -300, "category" : 0}, + "CKT" : {"offset" : -600, "category" : 0}, + "CLT" : {"offset" : -180, "category" : 0}, + "COT" : {"offset" : -300, "category" : 0}, + "CST" : {"offset" : -360, "category" : TZ_RFC|TZ_AMBIGUOUS}, + "CVT" : {"offset" : -60, "category" : 0}, + "CXT" : {"offset" : 420, "category" : 0}, + "EAT" : {"offset" : 180, "category" : 0}, + "ECT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "EDT" : {"offset" : -240, "category" : TZ_RFC|TZ_AMBIGUOUS|TZ_DST}, + "EET" : {"offset" : 120, "category" : 0}, + "EGT" : {"offset" : -60, "category" : 0}, + "EST" : {"offset" : -300, "category" : TZ_RFC|TZ_AMBIGUOUS}, + "FET" : {"offset" : 180, "category" : 0}, + "FJT" : {"offset" : 720, "category" : 0}, + "FKT" : {"offset" : -240, "category" : 0}, + "FNT" : {"offset" : -120, "category" : 0}, + "GET" : {"offset" : 240, "category" : 0}, + "GFT" : {"offset" : -180, "category" : 0}, + "GMT" : {"offset" : 0, "category" : TZ_UTC|TZ_RFC}, + "GST" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "GYT" : {"offset" : -240, "category" : 0}, + "HAA" : {"offset" : -180, "category" : 0}, + "HAC" : {"offset" : -300, "category" : 0}, + "HAE" : {"offset" : -240, "category" : 0}, + "HAP" : {"offset" : -420, "category" : 0}, + "HAR" : {"offset" : -360, "category" : 0}, + "HAT" : {"offset" : -90, "category" : 0}, + "HDT" : {"offset" : -540, "category" : TZ_DST}, + "HKT" : {"offset" : 480, "category" : 0}, + "HLV" : {"offset" : -210, "category" : 0}, + "HNA" : {"offset" : -240, "category" : 0}, + "HNC" : {"offset" : -360, "category" : 0}, + "HNE" : {"offset" : -300, "category" : 0}, + "HNP" : {"offset" : -480, "category" : 0}, + "HNR" : {"offset" : -420, "category" : 0}, + "HNT" : {"offset" : -150, "category" : 0}, + "HST" : {"offset" : -600, "category" : 0}, + "ICT" : {"offset" : 420, "category" : 0}, + "IDT" : {"offset" : 0, "category" : TZ_AMBIGUOUS|TZ_DST}, + "IOT" : {"offset" : 360, "category" : 0}, + "IST" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "JST" : {"offset" : 540, "category" : 0}, + "KGT" : {"offset" : 360, "category" : 0}, + "KIT" : {"offset" : 300, "category" : 0}, + "KST" : {"offset" : 540, "category" : 0}, + "MCK" : {"offset" : 180, "category" : 0}, + "MDT" : {"offset" : -360, "category" : TZ_RFC|TZ_DST}, + "MEZ" : {"offset" : 60, "category" : 0}, + "MHT" : {"offset" : 720, "category" : 0}, + "MMT" : {"offset" : 390, "category" : 0}, + "MSD" : {"offset" : 240, "category" : TZ_DST}, + "MSK" : {"offset" : 180, "category" : 0}, + "MST" : {"offset" : -420, "category" : TZ_RFC|TZ_AMBIGUOUS}, + "MUT" : {"offset" : 240, "category" : 0}, + "MVT" : {"offset" : 300, "category" : 0}, + "MYT" : {"offset" : 480, "category" : 0}, + "NCT" : {"offset" : 660, "category" : 0}, + "NDT" : {"offset" : -90, "category" : TZ_DST}, + "NFT" : {"offset" : 660, "category" : 0}, + "NPT" : {"offset" : 345, "category" : 0}, + "NRT" : {"offset" : 720, "category" : 0}, + "NST" : {"offset" : -150, "category" : 0}, + "NUT" : {"offset" : -660, "category" : 0}, + "OEZ" : {"offset" : 120, "category" : 0}, + "PDT" : {"offset" : -420, "category" : TZ_RFC|TZ_DST}, + "PET" : {"offset" : -300, "category" : 0}, + "PGT" : {"offset" : 600, "category" : 0}, + "PHT" : {"offset" : 480, "category" : 0}, + "PKT" : {"offset" : 300, "category" : 0}, + "PST" : {"offset" : -480, "category" : TZ_RFC|TZ_AMBIGUOUS}, + "PWT" : {"offset" : 540, "category" : 0}, + "PYT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "RET" : {"offset" : 240, "category" : 0}, + "SBT" : {"offset" : 660, "category" : 0}, + "SCT" : {"offset" : 240, "category" : 0}, + "SGT" : {"offset" : 480, "category" : 0}, + "SRT" : {"offset" : -180, "category" : 0}, + "SST" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "TFT" : {"offset" : 300, "category" : 0}, + "TJT" : {"offset" : 300, "category" : 0}, + "TKT" : {"offset" : 780, "category" : 0}, + "TLT" : {"offset" : 540, "category" : 0}, + "TMT" : {"offset" : 300, "category" : 0}, + "TOT" : {"offset" : 780, "category" : 0}, + "TRT" : {"offset" : 180, "category" : 0}, + "TVT" : {"offset" : 720, "category" : 0}, + "UTC" : {"offset" : 0, "category" : TZ_UTC|TZ_RFC}, + "UYT" : {"offset" : -180, "category" : 0}, + "UZT" : {"offset" : 300, "category" : 0}, + "VET" : {"offset" : -210, "category" : 0}, + "VUT" : {"offset" : 660, "category" : 0}, + "WAT" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "WDT" : {"offset" : 540, "category" : TZ_DST}, + "WET" : {"offset" : 0, "category" : 0}, + "WEZ" : {"offset" : 0, "category" : 0}, + "WFT" : {"offset" : 720, "category" : 0}, + "WGT" : {"offset" : -180, "category" : 0}, + "WIB" : {"offset" : 420, "category" : 0}, + "WIT" : {"offset" : 540, "category" : 0}, + "WST" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "ACDT" : {"offset" : 630, "category" : TZ_DST}, + "ACST" : {"offset" : 570, "category" : 0}, + "ADST" : {"offset" : 0, "category" : TZ_AMBIGUOUS|TZ_DST}, + "AEDT" : {"offset" : 660, "category" : TZ_DST}, + "AEST" : {"offset" : 600, "category" : 0}, + "AKDT" : {"offset" : -480, "category" : TZ_DST}, + "AKST" : {"offset" : -540, "category" : 0}, + "ALMT" : {"offset" : 360, "category" : 0}, + "AMDT" : {"offset" : 300, "category" : TZ_DST}, + "AMST" : {"offset" : 0, "category" : TZ_AMBIGUOUS|TZ_DST}, + "ANAT" : {"offset" : 720, "category" : 0}, + "AQTT" : {"offset" : 300, "category" : 0}, + "AWDT" : {"offset" : 540, "category" : TZ_DST}, + "AWST" : {"offset" : 480, "category" : 0}, + "AZOT" : {"offset" : -60, "category" : 0}, + "AZST" : {"offset" : 300, "category" : TZ_DST}, + "BDST" : {"offset" : 60, "category" : TZ_DST}, + "BRST" : {"offset" : -120, "category" : TZ_DST}, + "CAST" : {"offset" : 480, "category" : 0}, + "CDST" : {"offset" : 0, "category" : TZ_AMBIGUOUS|TZ_DST}, + "CEDT" : {"offset" : 120, "category" : TZ_DST}, + "CEST" : {"offset" : 120, "category" : TZ_DST}, + "CHOT" : {"offset" : 480, "category" : 0}, + "ChST" : {"offset" : 600, "category" : 0}, + "CHUT" : {"offset" : 600, "category" : 0}, + "CIST" : {"offset" : -300, "category" : 0}, + "CLDT" : {"offset" : -180, "category" : TZ_DST}, + "CLST" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "DAVT" : {"offset" : 420, "category" : 0}, + "DDUT" : {"offset" : 600, "category" : 0}, + "EADT" : {"offset" : -300, "category" : TZ_DST}, + "EAST" : {"offset" : -300, "category" : 0}, + "ECST" : {"offset" : 120, "category" : TZ_DST}, + "EDST" : {"offset" : 0, "category" : TZ_AMBIGUOUS|TZ_DST}, + "EEDT" : {"offset" : 180, "category" : TZ_DST}, + "EEST" : {"offset" : 180, "category" : TZ_DST}, + "EGST" : {"offset" : 0, "category" : TZ_DST}, + "FJDT" : {"offset" : 780, "category" : TZ_DST}, + "FJST" : {"offset" : 780, "category" : TZ_DST}, + "FKDT" : {"offset" : -180, "category" : TZ_DST}, + "FKST" : {"offset" : 0, "category" : TZ_AMBIGUOUS|TZ_DST}, + "GALT" : {"offset" : -360, "category" : 0}, + "GAMT" : {"offset" : -540, "category" : 0}, + "GILT" : {"offset" : 720, "category" : 0}, + "HADT" : {"offset" : -540, "category" : TZ_DST}, + "HAST" : {"offset" : -600, "category" : 0}, + "HOVT" : {"offset" : 420, "category" : 0}, + "IRDT" : {"offset" : 270, "category" : TZ_DST}, + "IRKT" : {"offset" : 480, "category" : 0}, + "IRST" : {"offset" : 0, "category" : TZ_AMBIGUOUS|TZ_DST}, + "KOST" : {"offset" : 660, "category" : 0}, + "KRAT" : {"offset" : 420, "category" : 0}, + "KUYT" : {"offset" : 240, "category" : 0}, + "LHDT" : {"offset" : 660, "category" : TZ_DST}, + "LHST" : {"offset" : 630, "category" : 0}, + "LINT" : {"offset" : 840, "category" : 0}, + "MAGT" : {"offset" : 600, "category" : 0}, + "MART" : {"offset" : -510, "category" : 0}, + "MAWT" : {"offset" : 300, "category" : 0}, + "MDST" : {"offset" : -360, "category" : TZ_DST}, + "MESZ" : {"offset" : 120, "category" : 0}, + "NFDT" : {"offset" : 720, "category" : TZ_DST}, + "NOVT" : {"offset" : 360, "category" : 0}, + "NZDT" : {"offset" : 780, "category" : TZ_DST}, + "NZST" : {"offset" : 720, "category" : 0}, + "OESZ" : {"offset" : 180, "category" : 0}, + "OMST" : {"offset" : 360, "category" : 0}, + "ORAT" : {"offset" : 300, "category" : 0}, + "PDST" : {"offset" : -420, "category" : TZ_DST}, + "PETT" : {"offset" : 720, "category" : 0}, + "PHOT" : {"offset" : 780, "category" : 0}, + "PMDT" : {"offset" : -120, "category" : TZ_DST}, + "PMST" : {"offset" : -180, "category" : 0}, + "PONT" : {"offset" : 660, "category" : 0}, + "PYST" : {"offset" : 0, "category" : TZ_AMBIGUOUS}, + "QYZT" : {"offset" : 360, "category" : 0}, + "ROTT" : {"offset" : -180, "category" : 0}, + "SAKT" : {"offset" : 600, "category" : 0}, + "SAMT" : {"offset" : 240, "category" : 0}, + "SAST" : {"offset" : 120, "category" : 0}, + "SRET" : {"offset" : 660, "category" : 0}, + "SYOT" : {"offset" : 180, "category" : 0}, + "TAHT" : {"offset" : -600, "category" : 0}, + "TOST" : {"offset" : 840, "category" : TZ_DST}, + "ULAT" : {"offset" : 480, "category" : 0}, + "UYST" : {"offset" : -120, "category" : TZ_DST}, + "VLAT" : {"offset" : 600, "category" : 0}, + "VOST" : {"offset" : 360, "category" : 0}, + "WAKT" : {"offset" : 720, "category" : 0}, + "WAST" : {"offset" : 120, "category" : TZ_DST}, + "WEDT" : {"offset" : 60, "category" : TZ_DST}, + "WEST" : {"offset" : 60, "category" : TZ_DST}, + "WESZ" : {"offset" : 60, "category" : 0}, + "WGST" : {"offset" : -120, "category" : TZ_DST}, + "WITA" : {"offset" : 480, "category" : 0}, + "YAKT" : {"offset" : 540, "category" : 0}, + "YAPT" : {"offset" : 600, "category" : 0}, + "YEKT" : {"offset" : 300, "category" : 0}, + "ACWST" : {"offset" : 525, "category" : 0}, + "ANAST" : {"offset" : 720, "category" : TZ_DST}, + "AZODT" : {"offset" : 0, "category" : TZ_DST}, + "AZOST" : {"offset" : 0, "category" : TZ_AMBIGUOUS|TZ_DST}, + "CHADT" : {"offset" : 825, "category" : TZ_DST}, + "CHAST" : {"offset" : 765, "category" : 0}, + "CHODT" : {"offset" : 540, "category" : TZ_DST}, + "CHOST" : {"offset" : 540, "category" : TZ_DST}, + "CIDST" : {"offset" : -240, "category" : TZ_DST}, + "EASST" : {"offset" : -300, "category" : TZ_DST}, + "EFATE" : {"offset" : 660, "category" : 0}, + "HOVDT" : {"offset" : 480, "category" : TZ_DST}, + "HOVST" : {"offset" : 480, "category" : TZ_DST}, + "IRKST" : {"offset" : 540, "category" : TZ_DST}, + "KRAST" : {"offset" : 480, "category" : TZ_DST}, + "MAGST" : {"offset" : 720, "category" : TZ_DST}, + "NACDT" : {"offset" : -300, "category" : TZ_DST}, + "NACST" : {"offset" : -360, "category" : 0}, + "NAEDT" : {"offset" : -240, "category" : TZ_DST}, + "NAEST" : {"offset" : -300, "category" : 0}, + "NAMDT" : {"offset" : -360, "category" : TZ_DST}, + "NAMST" : {"offset" : -420, "category" : 0}, + "NAPDT" : {"offset" : -420, "category" : TZ_DST}, + "NAPST" : {"offset" : -480, "category" : 0}, + "NOVST" : {"offset" : 420, "category" : TZ_DST}, + "OMSST" : {"offset" : 420, "category" : TZ_DST}, + "PETST" : {"offset" : 720, "category" : TZ_DST}, + "SAMST" : {"offset" : 240, "category" : TZ_DST}, + "ULAST" : {"offset" : 540, "category" : TZ_DST}, + "VLAST" : {"offset" : 660, "category" : TZ_DST}, + "WARST" : {"offset" : -180, "category" : TZ_DST}, + "YAKST" : {"offset" : 600, "category" : TZ_DST}, + "YEKST" : {"offset" : 360, "category" : TZ_DST}, + "CHODST" : {"offset" : 540, "category" : TZ_DST}, + "HOVDST" : {"offset" : 480, "category" : TZ_DST}, +} diff --git a/tarantool/msgpack_ext/types/timezones/validate_timezones.py b/tarantool/msgpack_ext/types/timezones/validate_timezones.py new file mode 100644 index 00000000..0626d8c3 --- /dev/null +++ b/tarantool/msgpack_ext/types/timezones/validate_timezones.py @@ -0,0 +1,12 @@ +import pytz +from timezones import timezoneToIndex, timezoneAbbrevInfo + +if __name__ != '__main__': + raise Error('Import not expected') + +for timezone in timezoneToIndex.keys(): + if timezone in pytz.all_timezones: + continue + + if not timezone in timezoneAbbrevInfo: + raise Exception(f'Unknown Tarantool timezone {timezone}') diff --git a/test/suites/test_datetime.py b/test/suites/test_datetime.py index ecd806a2..c952ec2a 100644 --- a/test/suites/test_datetime.py +++ b/test/suites/test_datetime.py @@ -65,6 +65,24 @@ def test_Datetime_class_API(self): # Both Tarantool and pandas prone to precision loss for timestamp() floats self.assertEqual(dt.timestamp, 1661958474.308543) self.assertEqual(dt.tzoffset, 180) + self.assertEqual(dt.tz, '') + self.assertEqual(dt.value, 1661958474308543321) + + def test_Datetime_class_API_wth_tz(self): + dt = tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, + nsec=308543321, tzoffset=123, tz='Europe/Moscow') + + self.assertEqual(dt.year, 2022) + self.assertEqual(dt.month, 8) + self.assertEqual(dt.day, 31) + self.assertEqual(dt.hour, 18) + self.assertEqual(dt.minute, 7) + self.assertEqual(dt.sec, 54) + self.assertEqual(dt.nsec, 308543321) + # Both Tarantool and pandas prone to precision loss for timestamp() floats + self.assertEqual(dt.timestamp, 1661958474.308543) + self.assertEqual(dt.tzoffset, 180) + self.assertEqual(dt.tz, 'Europe/Moscow') self.assertEqual(dt.value, 1661958474308543321) @@ -93,6 +111,18 @@ def test_Datetime_class_API(self): 'type': ValueError, 'msg': 'timestamp must be int if nsec provided' }, + 'unknown_tz': { + 'args': [], + 'kwargs': {'year': 2022, 'month': 8, 'day': 31, 'tz': 'Moskva'}, + 'type': ValueError, + 'msg': 'Unknown Tarantool timezone "Moskva"' + }, + 'abbrev_tz': { + 'args': [], + 'kwargs': {'year': 2022, 'month': 8, 'day': 31, 'tz': 'AET'}, + 'type': ValueError, + 'msg': 'Failed to create datetime with ambiguous timezone "AET"' + }, } def test_Datetime_class_invalid_init(self): @@ -183,6 +213,49 @@ def test_Datetime_class_invalid_init(self): 'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x00\x00\x00\x00\xc4\xff\x00\x00'), 'tarantool': r"datetime.new({timestamp=1661969274, tzoffset=-60})", }, + 'date_with_utc_tz': { + 'python': tarantool.Datetime(year=1970, month=1, day=1, tz='UTC'), + 'msgpack': (b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x28\x01'), + 'tarantool': r"datetime.new({year=1970, month=1, day=1, tz='UTC'})", + }, + 'date_with_tz': { + 'python': tarantool.Datetime(year=2022, month=8, day=31, tz='Europe/Moscow'), + 'msgpack': (b'\x50\x7a\x0e\x63\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x00\xb3\x03'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31, tz='Europe/Moscow'})", + }, + 'datetime_with_tz': { + 'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, + nsec=308543321, tz='Europe/Moscow'), + 'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\xb3\x03'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " + + r"nsec=308543321, tz='Europe/Moscow'})", + }, + 'datetime_with_tz_winter_time': { + 'python': tarantool.Datetime(year=2008, month=8, day=1, tz='Europe/Moscow'), + 'msgpack': (b'\xc0\x19\x92\x48\x00\x00\x00\x00\x00\x00\x00\x00\xf0\x00\xb3\x03'), + 'tarantool': r"datetime.new({year=2008, month=8, day=1, tz='Europe/Moscow'})", + }, + 'datetime_with_tz_and_offset': { + 'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, + nsec=308543321, tz='Europe/Moscow', tzoffset=123), + 'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\xb3\x03'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " + + r"nsec=308543321, tz='Europe/Moscow', tzoffset=123})", + }, + 'datetime_with_abbrev_tz': { + 'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, + nsec=308543321, tz='MSK'), + 'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\xee\x00'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " + + r"nsec=308543321, tz='MSK'})", + }, + 'datetime_with_abbrev_tz_and_zero_offset': { + 'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, sec=54, + nsec=308543321, tz='AZODT'), + 'msgpack': (b'\x7a\xa3\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\x00\x00\x12\x02'), + 'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " + + r"nsec=308543321, tz='AZODT'})", + }, } def test_msgpack_decode(self): @@ -236,6 +309,17 @@ def test_tarantool_encode(self): self.assertSequenceEqual(self.adm(lua_eval), [True]) + def test_msgpack_decode_unknown_tzindex(self): + case = b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\xff\xff' + self.assertRaisesRegex( + MsgpackError, 'Failed to decode datetime with unknown tzindex "-1"', + lambda: unpacker_ext_hook(4, case)) + + def test_msgpack_decode_ambiguous_tzindex(self): + case = b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\x00\x00\x82\x00' + self.assertRaisesRegex( + MsgpackError, 'Failed to create datetime with ambiguous timezone "AET"', + lambda: unpacker_ext_hook(4, case)) @classmethod def tearDownClass(self):