131 lines
5.3 KiB
Elixir
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
|