defmodule Zoneinfo.TimeZoneDatabase do
@moduledoc """
`Calendar.TimeZoneDatabase` implementation for Zoneinfo
Pass this module to the `DateTime` functions:
iex> DateTime.now!("Europe/Copenhagen", Zoneinfo.TimeZoneDatabase)
#DateTime<2021-07-24 12:56:38.324705+02:00 CEST Europe/Copenhagen>
or set it as the default by calling `Calendar.put_time_zone_database/1`:
iex> Calendar.put_time_zone_database(Zoneinfo.TimeZoneDatabase)
iex> DateTime.now!("Europe/Copenhagen")
#DateTime<2021-07-24 12:56:38.324705+02:00 CEST Europe/Copenhagen>
"""
@behaviour Calendar.TimeZoneDatabase
import Zoneinfo.Utils
@impl Calendar.TimeZoneDatabase
def time_zone_period_from_utc_iso_days(iso_days, time_zone) do
case Zoneinfo.Cache.get(time_zone) do
{:ok, tzif} ->
iso_days_to_gregorian_seconds(iso_days)
|> find_period_for_utc_secs(tzif.periods)
_ ->
{:error, :time_zone_not_found}
end
end
@impl Calendar.TimeZoneDatabase
def time_zone_periods_from_wall_datetime(naive_datetime, time_zone) do
case Zoneinfo.Cache.get(time_zone) do
{:ok, tzif} ->
{seconds, _micros} = NaiveDateTime.to_gregorian_seconds(naive_datetime)
find_period_for_wall_secs(seconds, tzif.periods)
_ ->
{:error, :time_zone_not_found}
end
end
defp find_period_for_utc_secs(secs, periods) do
period = Enum.find(periods, fn {time, _, _, _} -> secs >= time end)
{:ok, period_to_map(period)}
end
# receives wall gregorian seconds (also referred as the 'given timestamp' in the comments below)
# and the list of transitions
defp find_period_for_wall_secs(_, [period]), do: {:ok, period_to_map(period)}
defp find_period_for_wall_secs(wall_secs, [
period = {utc_secs, utc_off, std_off, _},
prev_period = {_ts2, prev_utc_off, prev_std_off, _}
| tail
]) do
period_start_wall_secs = utc_secs + utc_off + std_off
prev_period_end_wall_secs = utc_secs + prev_utc_off + prev_std_off
case {wall_secs >= period_start_wall_secs, wall_secs >= prev_period_end_wall_secs} do
{false, false} ->
# Try next earlier period
find_period_for_wall_secs(wall_secs, [prev_period | tail])
{true, true} ->
# Contained in this period
{:ok, period_to_map(period)}
{false, true} ->
# Time leaped forward and this is in the gap between periods
{:gap,
{period_to_map(prev_period),
gregorian_seconds_to_naive_datetime(prev_period_end_wall_secs)},
{period_to_map(period), gregorian_seconds_to_naive_datetime(period_start_wall_secs)}}
{true, false} ->
# Time fell back and this is in both periods
{:ambiguous, period_to_map(prev_period), period_to_map(period)}
end
end
defp period_to_map({_timestamp, utc_off, std_off, abbr}) do
%{
utc_offset: utc_off,
std_offset: std_off,
zone_abbr: abbr
}
end
end