Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/time/src/mcp_server_time/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class TimeResult(BaseModel):
datetime: str
day_of_week: str
is_dst: bool
note: str | None = None


class TimeConversionResult(BaseModel):
Expand Down Expand Up @@ -92,6 +93,22 @@ def convert_time(
tzinfo=source_timezone,
)

# A local wall-clock time can occur twice on the day a DST fall-back
# transition happens (e.g. 1:30 AM in America/New_York on the first
# Sunday of November occurs once before the clocks go back and once
# after). Attaching tzinfo directly, as above, always resolves to
# `fold=0` -- the earlier of the two real offsets -- with no
# indication to the caller that a second, equally valid answer
# exists. Detect that case by comparing both folds' offsets, and
# surface it instead of silently picking one.
source_note = None
other_fold_offset = source_time.replace(fold=1).utcoffset()
if other_fold_offset != source_time.utcoffset():
source_note = (
f"{parsed_time.strftime('%H:%M')} occurs twice in {source_tz} on this date due to a "
"DST transition; this result uses the earlier of the two occurrences."
)

target_time = source_time.astimezone(target_timezone)
source_offset = source_time.utcoffset() or timedelta()
target_offset = target_time.utcoffset() or timedelta()
Expand All @@ -109,6 +126,7 @@ def convert_time(
datetime=source_time.isoformat(timespec="seconds"),
day_of_week=source_time.strftime("%A"),
is_dst=bool(source_time.dst()),
note=source_note,
),
target=TimeResult(
timezone=target_tz,
Expand Down
46 changes: 46 additions & 0 deletions src/time/test/time_server_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,52 @@ def test_convert_time(test_time, source_tz, time_str, target_tz, expected):
assert result.time_difference == expected["time_difference"]


def test_convert_time_flags_ambiguous_fall_back_time():
# America/New_York's DST fall-back in 2024 happens at 2:00 AM on Nov 3,
# when clocks go back to 1:00 AM -- so 1:30 AM occurs twice that day.
# Freeze at noon UTC (already past the local transition) so `now` in NY
# time also lands on Nov 3, the actual transition date.
with freeze_time("2024-11-03 12:00:00+00:00"):
time_server = TimeServer()
result = time_server.convert_time("America/New_York", "01:30", "Europe/London")

assert result.source.note is not None
assert "occurs twice" in result.source.note
assert "America/New_York" in result.source.note
# The earlier (fold=0) occurrence is used: -04:00 (EDT), not -05:00 (EST).
assert result.source.datetime == "2024-11-03T01:30:00-04:00"


def test_convert_time_no_note_for_an_unambiguous_time_on_the_same_day():
with freeze_time("2024-11-03 12:00:00+00:00"):
time_server = TimeServer()
result = time_server.convert_time("America/New_York", "10:00", "Europe/London")

assert result.source.note is None


def test_convert_time_no_note_for_a_timezone_without_the_transition():
# Same real moment, but a source timezone with no DST fall-back at all --
# the note must not fire just because *some* zone somewhere is ambiguous.
with freeze_time("2024-11-03 12:00:00+00:00"):
time_server = TimeServer()
result = time_server.convert_time("UTC", "01:30", "Europe/London")

assert result.source.note is None


def test_convert_time_flags_ambiguous_time_in_a_different_zone_and_month():
# Europe/Warsaw's DST fall-back in 2024 is Oct 27, a different date and
# a different offset pair (+02:00 -> +01:00) than the US case above --
# confirms the check isn't hardcoded to one zone's transition.
with freeze_time("2024-10-27 12:00:00+00:00"):
time_server = TimeServer()
result = time_server.convert_time("Europe/Warsaw", "02:30", "UTC")

assert result.source.note is not None
assert result.source.datetime == "2024-10-27T02:30:00+02:00"


def test_get_local_tz_with_override():
"""Test that timezone override works correctly."""
result = get_local_tz("America/New_York")
Expand Down