Files
api-v2/deps/tzdata/lib/tzdata/far_future_dynamic_periods.ex
2025-04-16 10:03:13 -03:00

131 lines
5.3 KiB
Elixir

defmodule Tzdata.FarFutureDynamicPeriods do
# This module is for calculating periods far into the future
# for time zones with DST. It is assumed that there are two
# rules per year: one for going off of DST and one for going on DST.
#
# Instead of caching 10000 years worth of periods, we can use this
# module for periods that are far into the feature and only
# cache periods that are close to compile time.
@moduledoc false
alias Tzdata.ReleaseReader
alias Tzdata.Util
# February 1st 30 years from compile time
@greg_sec_30_years_from_now :calendar.datetime_to_gregorian_seconds {{(:calendar.universal_time()|>elem(0)|>elem(0)) + 30, 2, 1}, {0, 0, 0}}
# 30 years from compile time, is the zone in a period
# that runs until :max ? Ie. it is not using DST
# If it is not using DST the last cached period is valid forever
# and we do not want to use this module for that timezone.
def zone_in_30_years_in_eternal_period?(zone_name) do
y=Tzdata.periods_for_time(zone_name, @greg_sec_30_years_from_now, :utc) |> hd
y.until.utc == :max
end
def periods_for_point_in_time({{year, _month, _day}, _}, zone_name) do
fp_data = first_period_that_ends_in_year(zone_name, year)
first_period = fp_data.period
# We repeat the rules 3 times which is enough for the 4 periods we need
rules = fp_data.rules ++ fp_data.rules ++ fp_data.rules
rules_per_year = length(fp_data.rules)
if rules_per_year != 2 do
raise "dynamic periods assume 2 rules per year"
end
{:ok, periods_until_year([first_period], first_period.until.utc, first_period.utc_off, fp_data.zone_line, rules, year, rules_per_year)
}
end
# If datetime is not provided in erlang tuple, assume it is gregorian seconds
# and convert and send along.
def periods_for_point_in_time(gregorian_seconds, zone_name) do
periods_for_point_in_time(gregorian_seconds|>:calendar.gregorian_seconds_to_datetime, zone_name)
end
defp periods_until_year(prev_periods, from, utc_off, zone_line, rules, year, rules_per_year) do
begin_rule = rules |> hd
end_rule = rules |> tl |> hd
std_off = begin_rule.save
until_time_year = case length(prev_periods) >= rules_per_year do
true -> year + 1
false -> year
end
from_standard_time = standard_time_from_utc(from, utc_off)
from_wall_time = wall_time_from_utc(from, utc_off, std_off)
until_utc = Util.datetime_to_utc(Util.time_for_rule(end_rule, until_time_year), utc_off, std_off)
until_standard_time = standard_time_from_utc(until_utc, utc_off)
until_wall_time = wall_time_from_utc(until_utc, utc_off, std_off)
period = %{
std_off: std_off,
utc_off: utc_off,
from: %{utc: from, wall: from_wall_time, standard: from_standard_time},
until: %{standard: until_standard_time, wall: until_wall_time, utc: until_utc},
zone_abbr: Util.period_abbrevation(zone_line.format, std_off, utc_off, begin_rule.letter)
}
{{until_year_wall, _, _}, _} = :calendar.gregorian_seconds_to_datetime(until_wall_time)
if length(prev_periods) == rules_per_year do
prev_periods ++ [period]
else
periods_until_year(prev_periods ++ [period], until_utc, utc_off, zone_line, rules |> tl, until_year_wall, rules_per_year)
end
end
defp first_period_that_ends_in_year(zone_name, year) do
zone_line = last_line_for_zone(zone_name)
{:named_rules, rule_name} = zone_line.rules
rules = rules_applying_for_rule_name_and_year(rule_name, year)
utc_off = zone_line.gmtoff
rule_beginning_of_year = Enum.reverse(rules) |> tl |> hd
rule_end_of_year = Enum.reverse(rules) |> hd
# std off before is the offset before the first period starts
std_off_before = rule_beginning_of_year.save
std_off = rule_end_of_year.save
from = Util.datetime_to_utc(Util.time_for_rule(rule_end_of_year, year-1), utc_off, std_off_before)
letter = rule_end_of_year.letter
from_standard_time = standard_time_from_utc(from, utc_off)
from_wall_time = wall_time_from_utc(from, utc_off, std_off)
until_utc = Util.datetime_to_utc(Util.time_for_rule(rule_beginning_of_year, year), utc_off, std_off)
until_standard_time = standard_time_from_utc(until_utc, utc_off)
until_wall_time = wall_time_from_utc(until_utc, utc_off, std_off)
period = %{
std_off: std_off,
utc_off: utc_off,
from: %{utc: from, wall: from_wall_time, standard: from_standard_time},
until: %{standard: until_standard_time, wall: until_wall_time, utc: until_utc},
zone_abbr: Util.period_abbrevation(zone_line.format, std_off, utc_off, letter)
}
%{period: period, rules: rules, zone_line: zone_line, rule_name: rule_name}
end
defp last_line_for_zone(zone_name) do
{:ok, z}=ReleaseReader.zone(zone_name)
last_line = z.zone_lines |> Enum.reverse |> hd
last_line
end
defp rules_applying_for_rule_name_and_year(rule_name, year) do
{:ok, rules} = ReleaseReader.rules_for_name(rule_name)
rules
|> Util.rules_for_year(year)
|> Enum.sort(&(&1.in < &2.in))
end
def standard_time_from_utc(atom, _) when is_atom(atom), do: atom
def standard_time_from_utc(utc_time, utc_off) do
utc_time + utc_off
end
def wall_time_from_utc(atom, _, _) when is_atom(atom), do: atom
def wall_time_from_utc(utc_time, utc_offset, standard_offset) do
utc_time + utc_offset + standard_offset
end
end