Current section

Files

Jump to
tz lib time_zone_database.ex
Raw

lib/time_zone_database.ex

defmodule Tz.TimeZoneDatabase do
@moduledoc false
@behaviour Calendar.TimeZoneDatabase
alias Tz.IanaFileParser
alias Tz.PeriodsBuilder
alias Tz.PeriodsProvider
@impl true
def time_zone_period_from_utc_iso_days(iso_days, time_zone) do
with {:ok, periods_by_year} <- PeriodsProvider.periods_by_year(time_zone) do
naive_datetime = naive_datetime_from_iso_days(iso_days)
utc_gregorian_seconds = NaiveDateTime.diff(naive_datetime, ~N[0000-01-01 00:00:00])
found_periods =
Map.get(periods_by_year, naive_datetime.year, periods_by_year.minmax)
|> find_periods_for_timestamp(utc_gregorian_seconds, :utc_gregorian_seconds)
case found_periods do
[period] ->
{:ok, period}
_ ->
raise "#{Enum.count(found_periods)} periods found"
end
end
end
@impl true
def time_zone_periods_from_wall_datetime(naive_datetime, time_zone) do
with {:ok, periods_by_year} <- PeriodsProvider.periods_by_year(time_zone) do
wall_gregorian_seconds = NaiveDateTime.diff(naive_datetime, ~N[0000-01-01 00:00:00])
found_periods =
Map.get(periods_by_year, naive_datetime.year, periods_by_year.minmax)
|> find_periods_for_timestamp(wall_gregorian_seconds, :wall_gregorian_seconds)
case found_periods do
[%{zone_abbr: _} = period] ->
{:ok, period}
[%{period_before_gap: _} = period] ->
{:gap, {period.period_before_gap, period.from.wall}, {period.period_after_gap, period.to.wall}}
[second_period, first_period] ->
{:ambiguous, first_period, second_period}
_ ->
raise "#{Enum.count(found_periods)} periods found"
end
end
end
defp generate_dynamic_periods([period1, period2], year) do
rule_records = IanaFileParser.denormalized_rule_data([
IanaFileParser.change_rule_year(period1.rule, year - 1),
IanaFileParser.change_rule_year(period1.rule, year),
IanaFileParser.change_rule_year(period2.rule, year)
])
PeriodsBuilder.build_periods([period1.zone_line], rule_records)
|> PeriodsBuilder.shrink_and_reverse_periods()
end
defp find_periods_for_timestamp(periods, timestamp, time_modifier) do
do_find_periods_for_timestamp(periods, timestamp, time_modifier)
|> maybe_generate_dynamic_periods(periods, timestamp, time_modifier)
end
defp do_find_periods_for_timestamp(periods, timestamp, time_modifier) do
Enum.filter(periods, fn period ->
period_from = if(period.from == :min, do: :min, else: period.from[time_modifier])
period_to = if(period.to == :max, do: :max, else: period.to[time_modifier])
is_timestamp_in_range?(timestamp, period_from, period_to)
end)
end
defp is_timestamp_in_range?(_, :min, :max), do: true
defp is_timestamp_in_range?(timestamp, :min, date_to), do: timestamp < date_to
defp is_timestamp_in_range?(timestamp, date_from, :max), do: timestamp >= date_from
defp is_timestamp_in_range?(timestamp, date_from, date_to), do: timestamp >= date_from && timestamp < date_to
defp maybe_generate_dynamic_periods([%{to: :max} | _], [%{to: :max} = p1, %{to: :max} = p2 | _], timestamp, time_modifier) do
year = NaiveDateTime.add(~N[0000-01-01 00:00:00], timestamp).year
[p1, p2]
|> generate_dynamic_periods(year)
|> do_find_periods_for_timestamp(timestamp, time_modifier)
end
defp maybe_generate_dynamic_periods(found_periods, _periods, _timestamp, _time_modifier) do
found_periods
end
defp naive_datetime_from_iso_days(iso_days) do
Calendar.ISO.naive_datetime_from_iso_days(iso_days)
|> (&apply(NaiveDateTime, :new, Tuple.to_list(&1))).()
|> elem(1)
end
end