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