diff --git a/src/akkudoktoreos/utils/datetimeutil.py b/src/akkudoktoreos/utils/datetimeutil.py index a3178fb3..8c743dd1 100644 --- a/src/akkudoktoreos/utils/datetimeutil.py +++ b/src/akkudoktoreos/utils/datetimeutil.py @@ -672,12 +672,15 @@ def to_time( - pendulum.Time or pendulum.DateTime - datetime.time or datetime.datetime - strings like "14:30", "2:30 PM", "1430", "14:30:00.123", "2PM", "14h30" + - date/time strings (e.g. "2026-01-15T23:45") - int (e.g. 14 → 14:00) - float (e.g. 14.5 → 14:30) - tuple like (14,), (14, 30), (14, 30, 15) in_timezone: Optional timezone name or object (e.g., "Europe/Berlin"). - Defaults to the local timezone. + Defaults to the local timezone. Date/time strings without an offset use this + timezone as their wall time; strings with `Z` or an offset represent an instant + that is converted to this timezone. to_naive: If True, return a timezone-naive Time object. @@ -812,16 +815,18 @@ def to_time( # Fallback to pendulum's parser try: - dt = cast(pendulum.DateTime, pendulum.parse(value, strict=False)).in_tz(timezone) + dt = cast( + pendulum.DateTime, pendulum.parse(value, tz=timezone, strict=False) + ).in_tz(timezone) return finalize(dt.time()) except Exception as e: logger.trace(f"Pendulum parser failed for '{value}': {e}") # Try parsing with ISO time prefix try: - dt = cast(pendulum.DateTime, pendulum.parse(f"T{value}", strict=False)).in_tz( - timezone - ) + dt = cast( + pendulum.DateTime, pendulum.parse(f"T{value}", tz=timezone, strict=False) + ).in_tz(timezone) return finalize(dt.time()) except Exception as e: logger.trace(f"ISO time parser failed for 'T{value}': {e}") @@ -829,7 +834,8 @@ def to_time( # Try parsing as part of a full datetime try: dt = cast( - pendulum.DateTime, pendulum.parse(f"2000-01-01 {value}", strict=False) + pendulum.DateTime, + pendulum.parse(f"2000-01-01 {value}", tz=timezone, strict=False), ).in_tz(timezone) return finalize(dt.time()) except Exception as e: diff --git a/tests/test_datetimeutil.py b/tests/test_datetimeutil.py index 0654ce5c..bacb351b 100644 --- a/tests/test_datetimeutil.py +++ b/tests/test_datetimeutil.py @@ -675,6 +675,36 @@ class TestToTime: assert result.minute == 30 assert result.second == 45 + @pytest.mark.parametrize("host_timezone", ["UTC", "America/New_York"]) + @pytest.mark.parametrize( + "value, expected_time, expected_microsecond", + [ + ("2026-01-15T23:45", "23:45:00", 0), + ("2026-07-15 23:45", "23:45:00", 0), + ("2026-01-15 23:45:30", "23:45:30", 0), + ("2026-07-15T23:45:30.123456", "23:45:30", 123456), + ("2026-07-15T23:45Z", "01:45:00", 0), + ("2026-01-15 23:45+00:00", "00:45:00", 0), + ("2026-07-15T23:45:30.123456+03:00", "22:45:30", 123456), + ], + ) + def test_to_time_datetime_strings_use_target_timezone( + self, set_other_timezone, host_timezone, value, expected_time, expected_microsecond + ): + set_other_timezone(host_timezone) + result = to_time(value, in_timezone="Europe/Berlin") + assert isinstance(result, Time) + assert result.format("HH:mm:ss") == expected_time + assert result.microsecond == expected_microsecond + + @pytest.mark.parametrize("local_timezone", ["Europe/Berlin", "America/New_York"]) + def test_to_time_naive_datetime_string_defaults_to_local_timezone( + self, set_other_timezone, local_timezone + ): + set_other_timezone(local_timezone) + assert to_time("2026-01-15T23:45", as_string="HH:mm:ss") == "23:45:00" + assert to_time("23:45", as_string="HH:mm:ss") == "23:45:00" + @patch('akkudoktoreos.utils.datetimeutil.logger.trace') def test_to_time_logging_on_parse_failures(self, mock_trace): """Test that parsing failures are logged appropriately."""