Skip to content

Commit

Permalink
Make python timezone conversions handle more cases (deephaven#5249)
Browse files Browse the repository at this point in the history
* Make python timezone conversions handle more cases.

Resolves deephaven#4723

* Responding to review.  New unit test.

* Responding to review.  More careful support for time zone types.
  • Loading branch information
chipkent authored Mar 18, 2024
1 parent 07cf44c commit e5b78e2
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 6 deletions.
67 changes: 63 additions & 4 deletions py/server/deephaven/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
""" This module defines functions for handling Deephaven date/time data. """

import datetime
import zoneinfo
import pytz
from typing import Union, Optional, Literal

import jpy
Expand Down Expand Up @@ -163,6 +165,60 @@ def time_zone_alias_rm(alias: str) -> bool:

# region Conversions: Python To Java

def _tzinfo_to_j_time_zone(tzi: datetime.tzinfo) -> TimeZone:
"""
Converts a Python time zone to a Java TimeZone.
Args:
tzi: time zone info
Returns:
Java TimeZone
"""

if not tzi:
return None

# Handle pytz time zones

if isinstance(tzi, pytz.tzinfo.BaseTzInfo):
return _JDateTimeUtils.parseTimeZone(tzi.zone)

# Handle zoneinfo time zones

if isinstance(tzi, zoneinfo.ZoneInfo):
return _JDateTimeUtils.parseTimeZone(tzi.key)

# Handle constant UTC offset time zones (datetime.timezone)

if isinstance(tzi, datetime.timezone):
offset = tzi.utcoffset(None)

if offset is None:
raise ValueError("Unable to determine the time zone UTC offset")

if not offset:
return _JDateTimeUtils.parseTimeZone("UTC")

if offset.microseconds != 0 or offset.seconds%60 != 0:
raise ValueError(f"Unsupported time zone offset contains fractions of a minute: {offset}")

ts = offset.total_seconds()

if ts >= 0:
sign = "+"
else:
sign = "-"
ts = -ts

hours = int(ts / 3600)
minutes = int((ts % 3600) / 60)
return _JDateTimeUtils.parseTimeZone(f"UTC{sign}{hours:02d}:{minutes:02d}")

details = "\n\t".join([f"type={type(tzi).mro()}"] +
[f"obj.{attr}={getattr(tzi, attr)}" for attr in dir(tzi) if not attr.startswith("_")])
raise TypeError(f"Unsupported conversion: {str(type(tzi))} -> TimeZone\n\tDetails:\n\t{details}")


def to_j_time_zone(tz: Union[None, TimeZone, str, datetime.tzinfo, datetime.datetime, pandas.Timestamp]) -> \
Optional[TimeZone]:
Expand Down Expand Up @@ -190,12 +246,15 @@ def to_j_time_zone(tz: Union[None, TimeZone, str, datetime.tzinfo, datetime.date
elif isinstance(tz, str):
return _JDateTimeUtils.parseTimeZone(tz)
elif isinstance(tz, datetime.tzinfo):
return _JDateTimeUtils.parseTimeZone(str(tz))
return _tzinfo_to_j_time_zone(tz)
elif isinstance(tz, datetime.datetime):
if not tz.tzname():
return _JDateTimeUtils.parseTimeZone(tz.astimezone().tzname())
tzi = tz.tzinfo
rst = _tzinfo_to_j_time_zone(tzi)

if not rst:
raise ValueError("datetime is not time zone aware")

return _JDateTimeUtils.parseTimeZone(tz.tzname())
return rst
else:
raise TypeError("Unsupported conversion: " + str(type(tz)) + " -> TimeZone")
except TypeError as e:
Expand Down
35 changes: 33 additions & 2 deletions py/server/tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import unittest
from time import sleep
import datetime
import zoneinfo
import pandas as pd
import numpy as np

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

pytz = datetime.datetime.now()
tz = to_j_time_zone(pytz)
self.assertEqual(str(tz), "UTC")
with self.assertRaises(DHError):
tz = to_j_time_zone(pytz)
self.fail("Expected DHError")

pytz = datetime.datetime.now().astimezone()
tz = to_j_time_zone(pytz)
Expand All @@ -93,6 +95,35 @@ def test_to_j_time_zone(self):
tz2 = to_j_time_zone(tz1)
self.assertEqual(tz1, tz2)

ts = pd.Timestamp("2022-07-07", tz="America/New_York")
self.assertEqual(to_j_time_zone(ts), to_j_time_zone("America/New_York"))

dttz = datetime.timezone(offset=datetime.timedelta(hours=5), name="XYZ")
dt = datetime.datetime(2022, 7, 7, 14, 21, 17, 123456, tzinfo=dttz)
self.assertEqual(to_j_time_zone(dttz), to_j_time_zone("UTC+5"))
self.assertEqual(to_j_time_zone(dt), to_j_time_zone("UTC+5"))

dttz = datetime.timezone(offset=-datetime.timedelta(hours=5), name="XYZ")
dt = datetime.datetime(2022, 7, 7, 14, 21, 17, 123456, tzinfo=dttz)
self.assertEqual(to_j_time_zone(dttz), to_j_time_zone("UTC-5"))
self.assertEqual(to_j_time_zone(dt), to_j_time_zone("UTC-5"))

dttz = datetime.timezone(offset=-datetime.timedelta(hours=5, microseconds=10), name="XYZ")
dt = datetime.datetime(2022, 7, 7, 14, 21, 17, 123456, tzinfo=dttz)

with self.assertRaises(DHError):
to_j_time_zone(dttz)
self.fail("Expected DHError")

with self.assertRaises(DHError):
to_j_time_zone(dt)
self.fail("Expected DHError")

dttz = zoneinfo.ZoneInfo("America/New_York")
dt = datetime.datetime(2022, 7, 7, 14, 21, 17, 123456, tzinfo=dttz)
self.assertEqual(to_j_time_zone(dttz), to_j_time_zone("America/New_York"))
self.assertEqual(to_j_time_zone(dt), to_j_time_zone("America/New_York"))

with self.assertRaises(TypeError):
to_j_time_zone(False)
self.fail("Expected TypeError")
Expand Down

0 comments on commit e5b78e2

Please sign in to comment.