diff --git a/generated/nidaqmx/_time.py b/generated/nidaqmx/_time.py index e2cd4989a..dea71e554 100644 --- a/generated/nidaqmx/_time.py +++ b/generated/nidaqmx/_time.py @@ -22,8 +22,23 @@ def _convert_to_desired_timezone( # use ZoneInfo here to account for daylight savings if isinstance(tzinfo, ZoneInfo): localized_time = expected_time_utc.replace(tzinfo=tzinfo) - desired_expected_time = tzinfo.fromutc(localized_time) - return desired_expected_time # type: ignore[return-value] # https://github.com/ni/nidaqmx-python/issues/860 + std_datetime_result = tzinfo.fromutc(localized_time) + femtosecond = getattr(expected_time_utc, "femtosecond", 0) + yoctosecond = getattr(expected_time_utc, "yoctosecond", 0) + desired_expected_time = ht_datetime( + std_datetime_result.year, + std_datetime_result.month, + std_datetime_result.day, + std_datetime_result.hour, + std_datetime_result.minute, + std_datetime_result.second, + std_datetime_result.microsecond, + femtosecond, + yoctosecond, + tzinfo=std_datetime_result.tzinfo, + fold=std_datetime_result.fold, + ) + return desired_expected_time # 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) elif tzinfo.utcoffset(None) is not None: diff --git a/src/handwritten/_time.py b/src/handwritten/_time.py index e2cd4989a..dea71e554 100644 --- a/src/handwritten/_time.py +++ b/src/handwritten/_time.py @@ -22,8 +22,23 @@ def _convert_to_desired_timezone( # use ZoneInfo here to account for daylight savings if isinstance(tzinfo, ZoneInfo): localized_time = expected_time_utc.replace(tzinfo=tzinfo) - desired_expected_time = tzinfo.fromutc(localized_time) - return desired_expected_time # type: ignore[return-value] # https://github.com/ni/nidaqmx-python/issues/860 + std_datetime_result = tzinfo.fromutc(localized_time) + femtosecond = getattr(expected_time_utc, "femtosecond", 0) + yoctosecond = getattr(expected_time_utc, "yoctosecond", 0) + desired_expected_time = ht_datetime( + std_datetime_result.year, + std_datetime_result.month, + std_datetime_result.day, + std_datetime_result.hour, + std_datetime_result.minute, + std_datetime_result.second, + std_datetime_result.microsecond, + femtosecond, + yoctosecond, + tzinfo=std_datetime_result.tzinfo, + fold=std_datetime_result.fold, + ) + return desired_expected_time # 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) elif tzinfo.utcoffset(None) is not None: diff --git a/tests/unit/test_lib_time.py b/tests/unit/test_lib_time.py index ff77e81da..6e0877fd5 100644 --- a/tests/unit/test_lib_time.py +++ b/tests/unit/test_lib_time.py @@ -7,6 +7,7 @@ from hightime import datetime as ht_datetime from nidaqmx._lib_time import AbsoluteTime as LibTimestamp +from nidaqmx._time import _convert_to_desired_timezone from tests.unit._time_utils import ( JAN_01_1850_DATETIME, JAN_01_1850_HIGHTIME, @@ -106,6 +107,63 @@ def test___utc_datetime___convert_to_timestamp_with_dst___is_reversible(date): assert astimezone_date == roundtrip_dt +@pytest.mark.parametrize( + "base_dt, femtosecond, subseconds", + [ + (ht_datetime(2023, 3, 12, tzinfo=timezone.utc), 0, 0), + (ht_datetime(2023, 3, 12, tzinfo=timezone.utc), 1, 0x480F), + (ht_datetime(2023, 6, 1, tzinfo=timezone.utc), 0, 0), + (ht_datetime(2023, 6, 1, tzinfo=timezone.utc), 1, 0x480F), + (ht_datetime(2023, 11, 5, tzinfo=timezone.utc), 0, 0), + (ht_datetime(2023, 11, 5, tzinfo=timezone.utc), 1, 0x480F), + ], +) +def test___utc_datetime_with_femtoseconds___convert_to_timestamp_with_dst___is_reversible( + base_dt, femtosecond, subseconds +): + target_timezone = ZoneInfo("America/Los_Angeles") + from_dt = base_dt.replace(femtosecond=femtosecond) + expected_la_time = _convert_to_desired_timezone(from_dt, target_timezone) + + ts = LibTimestamp.from_datetime(from_dt) + roundtrip_dt = ts.to_datetime(tzinfo=target_timezone) + + assert ts.msb == LibTimestamp.from_datetime(expected_la_time).msb + assert ts.lsb == subseconds + assert roundtrip_dt.microsecond == expected_la_time.microsecond + assert roundtrip_dt.femtosecond == expected_la_time.femtosecond + # The yoctosecond field may contain up to 1 NI-BTF tick of rounding error. + assert roundtrip_dt.yoctosecond <= 54210 + + +@pytest.mark.parametrize( + "base_dt, yoctosecond, subseconds", + [ + (ht_datetime(2023, 3, 12, tzinfo=timezone.utc), 0, 0), + (ht_datetime(2023, 3, 12, tzinfo=timezone.utc), 54210, 1), + (ht_datetime(2023, 6, 1, tzinfo=timezone.utc), 0, 0), + (ht_datetime(2023, 6, 1, tzinfo=timezone.utc), 54210, 1), + (ht_datetime(2023, 11, 5, tzinfo=timezone.utc), 0, 0), + (ht_datetime(2023, 11, 5, tzinfo=timezone.utc), 54210, 1), + ], +) +def test___utc_datetime_with_yoctoseconds___convert_to_timestamp_with_dst___is_reversible( + base_dt, yoctosecond, subseconds +): + target_timezone = ZoneInfo("America/Los_Angeles") + from_dt = base_dt.replace(yoctosecond=yoctosecond) + expected_la_time = _convert_to_desired_timezone(from_dt, target_timezone) + + ts = LibTimestamp.from_datetime(from_dt) + roundtrip_dt = ts.to_datetime(tzinfo=target_timezone) + + assert ts.msb == LibTimestamp.from_datetime(expected_la_time).msb + assert ts.lsb == subseconds + assert roundtrip_dt.microsecond == expected_la_time.microsecond + assert roundtrip_dt.femtosecond == expected_la_time.femtosecond + assert roundtrip_dt.yoctosecond == expected_la_time.yoctosecond + + @pytest.mark.parametrize( "datetime_cls, tzinfo, expected_offset", [