Skip to content

Commit 868ccfc

Browse files
authored
Fix _convert_to_desired_timezone loses hightime accuracy for DST conversions (#864)
* fix hightime accuracy and add femtosecond test * resolve comment * fix lint * codegen * fix lint * add missing comma * resolve comment * lint
1 parent 519b97d commit 868ccfc

File tree

3 files changed

+92
-4
lines changed

3 files changed

+92
-4
lines changed

generated/nidaqmx/_time.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,23 @@ def _convert_to_desired_timezone(
2222
# use ZoneInfo here to account for daylight savings
2323
if isinstance(tzinfo, ZoneInfo):
2424
localized_time = expected_time_utc.replace(tzinfo=tzinfo)
25-
desired_expected_time = tzinfo.fromutc(localized_time)
26-
return desired_expected_time # type: ignore[return-value] # https://github.com/ni/nidaqmx-python/issues/860
25+
std_datetime_result = tzinfo.fromutc(localized_time)
26+
femtosecond = getattr(expected_time_utc, "femtosecond", 0)
27+
yoctosecond = getattr(expected_time_utc, "yoctosecond", 0)
28+
desired_expected_time = ht_datetime(
29+
std_datetime_result.year,
30+
std_datetime_result.month,
31+
std_datetime_result.day,
32+
std_datetime_result.hour,
33+
std_datetime_result.minute,
34+
std_datetime_result.second,
35+
std_datetime_result.microsecond,
36+
femtosecond,
37+
yoctosecond,
38+
tzinfo=std_datetime_result.tzinfo,
39+
fold=std_datetime_result.fold,
40+
)
41+
return desired_expected_time
2742

2843
# if the tzinfo passed in is a timedelta function, then we don't need to consider daylight savings # noqa: W505 - doc line too long (102 > 100 characters) (auto-generated noqa)
2944
elif tzinfo.utcoffset(None) is not None:

src/handwritten/_time.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,23 @@ def _convert_to_desired_timezone(
2222
# use ZoneInfo here to account for daylight savings
2323
if isinstance(tzinfo, ZoneInfo):
2424
localized_time = expected_time_utc.replace(tzinfo=tzinfo)
25-
desired_expected_time = tzinfo.fromutc(localized_time)
26-
return desired_expected_time # type: ignore[return-value] # https://github.com/ni/nidaqmx-python/issues/860
25+
std_datetime_result = tzinfo.fromutc(localized_time)
26+
femtosecond = getattr(expected_time_utc, "femtosecond", 0)
27+
yoctosecond = getattr(expected_time_utc, "yoctosecond", 0)
28+
desired_expected_time = ht_datetime(
29+
std_datetime_result.year,
30+
std_datetime_result.month,
31+
std_datetime_result.day,
32+
std_datetime_result.hour,
33+
std_datetime_result.minute,
34+
std_datetime_result.second,
35+
std_datetime_result.microsecond,
36+
femtosecond,
37+
yoctosecond,
38+
tzinfo=std_datetime_result.tzinfo,
39+
fold=std_datetime_result.fold,
40+
)
41+
return desired_expected_time
2742

2843
# if the tzinfo passed in is a timedelta function, then we don't need to consider daylight savings # noqa: W505 - doc line too long (102 > 100 characters) (auto-generated noqa)
2944
elif tzinfo.utcoffset(None) is not None:

tests/unit/test_lib_time.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from hightime import datetime as ht_datetime
88

99
from nidaqmx._lib_time import AbsoluteTime as LibTimestamp
10+
from nidaqmx._time import _convert_to_desired_timezone
1011
from tests.unit._time_utils import (
1112
JAN_01_1850_DATETIME,
1213
JAN_01_1850_HIGHTIME,
@@ -106,6 +107,63 @@ def test___utc_datetime___convert_to_timestamp_with_dst___is_reversible(date):
106107
assert astimezone_date == roundtrip_dt
107108

108109

110+
@pytest.mark.parametrize(
111+
"base_dt, femtosecond, subseconds",
112+
[
113+
(ht_datetime(2023, 3, 12, tzinfo=timezone.utc), 0, 0),
114+
(ht_datetime(2023, 3, 12, tzinfo=timezone.utc), 1, 0x480F),
115+
(ht_datetime(2023, 6, 1, tzinfo=timezone.utc), 0, 0),
116+
(ht_datetime(2023, 6, 1, tzinfo=timezone.utc), 1, 0x480F),
117+
(ht_datetime(2023, 11, 5, tzinfo=timezone.utc), 0, 0),
118+
(ht_datetime(2023, 11, 5, tzinfo=timezone.utc), 1, 0x480F),
119+
],
120+
)
121+
def test___utc_datetime_with_femtoseconds___convert_to_timestamp_with_dst___is_reversible(
122+
base_dt, femtosecond, subseconds
123+
):
124+
target_timezone = ZoneInfo("America/Los_Angeles")
125+
from_dt = base_dt.replace(femtosecond=femtosecond)
126+
expected_la_time = _convert_to_desired_timezone(from_dt, target_timezone)
127+
128+
ts = LibTimestamp.from_datetime(from_dt)
129+
roundtrip_dt = ts.to_datetime(tzinfo=target_timezone)
130+
131+
assert ts.msb == LibTimestamp.from_datetime(expected_la_time).msb
132+
assert ts.lsb == subseconds
133+
assert roundtrip_dt.microsecond == expected_la_time.microsecond
134+
assert roundtrip_dt.femtosecond == expected_la_time.femtosecond
135+
# The yoctosecond field may contain up to 1 NI-BTF tick of rounding error.
136+
assert roundtrip_dt.yoctosecond <= 54210
137+
138+
139+
@pytest.mark.parametrize(
140+
"base_dt, yoctosecond, subseconds",
141+
[
142+
(ht_datetime(2023, 3, 12, tzinfo=timezone.utc), 0, 0),
143+
(ht_datetime(2023, 3, 12, tzinfo=timezone.utc), 54210, 1),
144+
(ht_datetime(2023, 6, 1, tzinfo=timezone.utc), 0, 0),
145+
(ht_datetime(2023, 6, 1, tzinfo=timezone.utc), 54210, 1),
146+
(ht_datetime(2023, 11, 5, tzinfo=timezone.utc), 0, 0),
147+
(ht_datetime(2023, 11, 5, tzinfo=timezone.utc), 54210, 1),
148+
],
149+
)
150+
def test___utc_datetime_with_yoctoseconds___convert_to_timestamp_with_dst___is_reversible(
151+
base_dt, yoctosecond, subseconds
152+
):
153+
target_timezone = ZoneInfo("America/Los_Angeles")
154+
from_dt = base_dt.replace(yoctosecond=yoctosecond)
155+
expected_la_time = _convert_to_desired_timezone(from_dt, target_timezone)
156+
157+
ts = LibTimestamp.from_datetime(from_dt)
158+
roundtrip_dt = ts.to_datetime(tzinfo=target_timezone)
159+
160+
assert ts.msb == LibTimestamp.from_datetime(expected_la_time).msb
161+
assert ts.lsb == subseconds
162+
assert roundtrip_dt.microsecond == expected_la_time.microsecond
163+
assert roundtrip_dt.femtosecond == expected_la_time.femtosecond
164+
assert roundtrip_dt.yoctosecond == expected_la_time.yoctosecond
165+
166+
109167
@pytest.mark.parametrize(
110168
"datetime_cls, tzinfo, expected_offset",
111169
[

0 commit comments

Comments
 (0)