Skip to content

Commit e5b78e2

Browse files
authored
Make python timezone conversions handle more cases (#5249)
* Make python timezone conversions handle more cases. Resolves #4723 * Responding to review. New unit test. * Responding to review. More careful support for time zone types.
1 parent 07cf44c commit e5b78e2

File tree

2 files changed

+96
-6
lines changed

2 files changed

+96
-6
lines changed

py/server/deephaven/time.py

+63-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
""" This module defines functions for handling Deephaven date/time data. """
66

77
import datetime
8+
import zoneinfo
9+
import pytz
810
from typing import Union, Optional, Literal
911

1012
import jpy
@@ -163,6 +165,60 @@ def time_zone_alias_rm(alias: str) -> bool:
163165

164166
# region Conversions: Python To Java
165167

168+
def _tzinfo_to_j_time_zone(tzi: datetime.tzinfo) -> TimeZone:
169+
"""
170+
Converts a Python time zone to a Java TimeZone.
171+
172+
Args:
173+
tzi: time zone info
174+
175+
Returns:
176+
Java TimeZone
177+
"""
178+
179+
if not tzi:
180+
return None
181+
182+
# Handle pytz time zones
183+
184+
if isinstance(tzi, pytz.tzinfo.BaseTzInfo):
185+
return _JDateTimeUtils.parseTimeZone(tzi.zone)
186+
187+
# Handle zoneinfo time zones
188+
189+
if isinstance(tzi, zoneinfo.ZoneInfo):
190+
return _JDateTimeUtils.parseTimeZone(tzi.key)
191+
192+
# Handle constant UTC offset time zones (datetime.timezone)
193+
194+
if isinstance(tzi, datetime.timezone):
195+
offset = tzi.utcoffset(None)
196+
197+
if offset is None:
198+
raise ValueError("Unable to determine the time zone UTC offset")
199+
200+
if not offset:
201+
return _JDateTimeUtils.parseTimeZone("UTC")
202+
203+
if offset.microseconds != 0 or offset.seconds%60 != 0:
204+
raise ValueError(f"Unsupported time zone offset contains fractions of a minute: {offset}")
205+
206+
ts = offset.total_seconds()
207+
208+
if ts >= 0:
209+
sign = "+"
210+
else:
211+
sign = "-"
212+
ts = -ts
213+
214+
hours = int(ts / 3600)
215+
minutes = int((ts % 3600) / 60)
216+
return _JDateTimeUtils.parseTimeZone(f"UTC{sign}{hours:02d}:{minutes:02d}")
217+
218+
details = "\n\t".join([f"type={type(tzi).mro()}"] +
219+
[f"obj.{attr}={getattr(tzi, attr)}" for attr in dir(tzi) if not attr.startswith("_")])
220+
raise TypeError(f"Unsupported conversion: {str(type(tzi))} -> TimeZone\n\tDetails:\n\t{details}")
221+
166222

167223
def to_j_time_zone(tz: Union[None, TimeZone, str, datetime.tzinfo, datetime.datetime, pandas.Timestamp]) -> \
168224
Optional[TimeZone]:
@@ -190,12 +246,15 @@ def to_j_time_zone(tz: Union[None, TimeZone, str, datetime.tzinfo, datetime.date
190246
elif isinstance(tz, str):
191247
return _JDateTimeUtils.parseTimeZone(tz)
192248
elif isinstance(tz, datetime.tzinfo):
193-
return _JDateTimeUtils.parseTimeZone(str(tz))
249+
return _tzinfo_to_j_time_zone(tz)
194250
elif isinstance(tz, datetime.datetime):
195-
if not tz.tzname():
196-
return _JDateTimeUtils.parseTimeZone(tz.astimezone().tzname())
251+
tzi = tz.tzinfo
252+
rst = _tzinfo_to_j_time_zone(tzi)
253+
254+
if not rst:
255+
raise ValueError("datetime is not time zone aware")
197256

198-
return _JDateTimeUtils.parseTimeZone(tz.tzname())
257+
return rst
199258
else:
200259
raise TypeError("Unsupported conversion: " + str(type(tz)) + " -> TimeZone")
201260
except TypeError as e:

py/server/tests/test_time.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import unittest
66
from time import sleep
77
import datetime
8+
import zoneinfo
89
import pandas as pd
910
import numpy as np
1011

@@ -72,8 +73,9 @@ def test_to_j_time_zone(self):
7273
self.assertEqual(str(tz), "UTC")
7374

7475
pytz = datetime.datetime.now()
75-
tz = to_j_time_zone(pytz)
76-
self.assertEqual(str(tz), "UTC")
76+
with self.assertRaises(DHError):
77+
tz = to_j_time_zone(pytz)
78+
self.fail("Expected DHError")
7779

7880
pytz = datetime.datetime.now().astimezone()
7981
tz = to_j_time_zone(pytz)
@@ -93,6 +95,35 @@ def test_to_j_time_zone(self):
9395
tz2 = to_j_time_zone(tz1)
9496
self.assertEqual(tz1, tz2)
9597

98+
ts = pd.Timestamp("2022-07-07", tz="America/New_York")
99+
self.assertEqual(to_j_time_zone(ts), to_j_time_zone("America/New_York"))
100+
101+
dttz = datetime.timezone(offset=datetime.timedelta(hours=5), name="XYZ")
102+
dt = datetime.datetime(2022, 7, 7, 14, 21, 17, 123456, tzinfo=dttz)
103+
self.assertEqual(to_j_time_zone(dttz), to_j_time_zone("UTC+5"))
104+
self.assertEqual(to_j_time_zone(dt), to_j_time_zone("UTC+5"))
105+
106+
dttz = datetime.timezone(offset=-datetime.timedelta(hours=5), name="XYZ")
107+
dt = datetime.datetime(2022, 7, 7, 14, 21, 17, 123456, tzinfo=dttz)
108+
self.assertEqual(to_j_time_zone(dttz), to_j_time_zone("UTC-5"))
109+
self.assertEqual(to_j_time_zone(dt), to_j_time_zone("UTC-5"))
110+
111+
dttz = datetime.timezone(offset=-datetime.timedelta(hours=5, microseconds=10), name="XYZ")
112+
dt = datetime.datetime(2022, 7, 7, 14, 21, 17, 123456, tzinfo=dttz)
113+
114+
with self.assertRaises(DHError):
115+
to_j_time_zone(dttz)
116+
self.fail("Expected DHError")
117+
118+
with self.assertRaises(DHError):
119+
to_j_time_zone(dt)
120+
self.fail("Expected DHError")
121+
122+
dttz = zoneinfo.ZoneInfo("America/New_York")
123+
dt = datetime.datetime(2022, 7, 7, 14, 21, 17, 123456, tzinfo=dttz)
124+
self.assertEqual(to_j_time_zone(dttz), to_j_time_zone("America/New_York"))
125+
self.assertEqual(to_j_time_zone(dt), to_j_time_zone("America/New_York"))
126+
96127
with self.assertRaises(TypeError):
97128
to_j_time_zone(False)
98129
self.fail("Expected TypeError")

0 commit comments

Comments
 (0)