bitwalker / bitwalker/timex

`Timezone.convert/2` gives `:error` on DST gap instead of `AmbiguousDateTime`

Open
#789 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Elixir
Stars
1.8k
Forks
407
PR merge metrics
No merged PRs in 30d

Description

Looking at the [source code](https://github.com/bitwalker/timex/blob/5ad1b8206977ebffb3bf72f88c18d490c36151c8/lib/timezone/timezone.ex#L576), it looks like `Timezone.convert/2` is supposed to return `AmbiguousDateTime` on `:ambiguous` AND `:gap`. However, in some cases it instead returns a not very helpful `:error` tuple.

Example with gap datetime:
```elixir
iex(1)> use Timex
Timex.Timezone
iex(2)> naive_datetime_on_gap = ~N[2025-03-09 02:30:00]
~N[2025-03-09 02:30:00]
iex(3)> Timex.to_datetime(naive_datetime_on_gap, "America/Toronto")
{:error, {:could_not_resolve_timezone, "America/Toronto", 63908706600, :wall}}
iex(4)> Timezone.convert(naive_datetime_on_gap, "America/Toronto")
{:error, {:could_not_resolve_timezone, "America/Toronto", 63908706600, :wall}}
```

The error seems to come from calling `Timezone.get("America/Toronto", ~N[2025-03-09 02:30:00])` and ultimately `Tzdata.periods_for_time("America/Toronto", 63908706600, :wall)` which comes back with `[]`. From the `period_for_time` docs, this seems to be the expected return for "non-existing wall time for the zone".

I seem to be able to get around this issue if I skip `Timezone.get`:
```elixir
iex(6)> a = Timezone.convert(naive_datetime_on_gap, %TimezoneInfo{full_name: "America/Toronto"})
# ~ #DateTime<2025-03-09 03:00:00-04:00 EDT America/Toronto>)>
iex(7)> a.__struct__
Timex.AmbiguousDateTime
iex(8)> a.before
#DateTime<2025-03-09 01:59:59.999999-05:00 EST America/Toronto>
iex(9)> a.after
#DateTime<2025-03-09 03:00:00-04:00 EDT America/Toronto>
```

I'm wondering if the following should be changed from https://github.com/bitwalker/timex/blob/5ad1b8206977ebffb3bf72f88c18d490c36151c8/lib/timezone/timezone.ex#L584-L588
to something like:
```elixir
def convert(%NaiveDateTime{} = date, tz) do
convert(date, %TimezoneInfo{full_name: tz})
end
```

That seems to give the expected results:
```elixir
iex(22)> Timezone.convert(~N[2025-03-09 02:30:00], %TimezoneInfo{full_name: "America/Toronto"})
# ~ #DateTime<2025-03-09 03:00:00-04:00 EDT America/Toronto>)>
iex(23)> Timezone.convert(~N[2025-11-02 01:30:00], %TimezoneInfo{full_name: "America/Toronto"})
# ~ #DateTime<2025-11-02 01:30:00-05:00 EST America/Toronto>)>
iex(24)> Timezone.convert(~N[2025-11-12 01:30:00], %TimezoneInfo{full_name: "America/Toronto"})
#DateTime<2025-11-12 01:30:00-05:00 EST America/Toronto>
iex(25)> Timezone.convert(~N[2025-11-12 01:30:00], %TimezoneInfo{full_name: "NOT FOUND"})
{:error, :time_zone_not_found}
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.