Current section

Files

Jump to
tz lib periods_builder.ex
Raw

lib/periods_builder.ex

defmodule Tz.PeriodsBuilder do
@moduledoc false
def build_periods(zone_lines, rule_records, mode \\ :compilation, prev_period \\ nil, periods \\ [])
def build_periods([], _rule_records, _mode, _prev_period, periods), do: Enum.reverse(periods)
def build_periods([zone_line | rest_zone_lines], rule_records, mode, prev_period, periods) do
rules = Map.get(rule_records, zone_line.rules, zone_line.rules)
periods =
build_periods_for_zone_line(zone_line, rules, mode, prev_period)
|> concat_dedup_periods(periods)
build_periods(rest_zone_lines, rule_records, mode, hd(periods), periods)
end
defp concat_dedup_periods(periods, []), do: periods
defp concat_dedup_periods(periods1, [first_period2 | tail_period2] = periods2) do
last_period1 = List.last(periods1)
compare_keys = [:std_offset_from_utc_time, :local_offset_from_std_time, :zone_abbr]
cond do
Map.take(last_period1, compare_keys) == Map.take(first_period2, compare_keys) ->
(periods1 |> Enum.reverse() |> tl() |> Enum.reverse())
++ [%{first_period2 | to: last_period1.to} | tail_period2]
true ->
periods1 ++ periods2
end
end
defp offset_diff_from_prev_period(_zone_line, _local_offset, nil), do: 0
defp offset_diff_from_prev_period(zone_line, local_offset, prev_period) do
total_offset = zone_line.std_offset_from_utc_time + local_offset
prev_total_offset = prev_period.std_offset_from_utc_time + prev_period.local_offset_from_std_time
total_offset - prev_total_offset
end
defp build_periods_for_zone_line(zone_line, offset, _mode, prev_period) when is_integer(offset) do
if zone_line.from != :min && prev_period != nil do
{zone_from, zone_from_modifier} = zone_line.from
if prev_period.to[zone_from_modifier] != zone_from do
raise "logic error"
end
end
offset_diff = offset_diff_from_prev_period(zone_line, offset, prev_period)
period_from =
if zone_line.from == :min do
:min
else
add_to_and_convert_date_tuple({prev_period.to.wall, :wall}, offset_diff, zone_line.std_offset_from_utc_time, offset)
end
[%{
from: period_from,
to: convert_date_tuple(zone_line.to, zone_line.std_offset_from_utc_time, offset),
std_offset_from_utc_time: zone_line.std_offset_from_utc_time,
local_offset_from_std_time: offset,
zone_abbr: zone_abbr(zone_line, offset)
}]
end
defp build_periods_for_zone_line(zone_line, rules, mode, prev_period) when is_list(rules) do
if zone_line.from != :min && prev_period != nil do
{zone_from, zone_from_modifier} = zone_line.from
if prev_period.to[zone_from_modifier] != zone_from do
raise "logic error"
end
end
if mode == :dynamic_far_future do
rules
else
rules
|> filter_rules_for_zone_line(zone_line, prev_period, if(prev_period == nil, do: 0, else: prev_period.local_offset_from_std_time))
|> maybe_pad_left_rule(zone_line, prev_period)
|> trim_zone_rules(zone_line, prev_period)
end
|> do_build_periods_for_zone_line(zone_line, prev_period, [])
end
defp filter_rules_for_zone_line(rules, zone_line, prev_period, prev_local_offset_from_std_time, filtered_rules \\ [])
defp filter_rules_for_zone_line(rules, %{from: :min, to: :max}, _, _, _), do: rules
defp filter_rules_for_zone_line([], _zone_line, _, _, filtered_rules), do: Enum.reverse(filtered_rules)
defp filter_rules_for_zone_line([rule | rest_rules], zone_line, prev_period, prev_local_offset_from_std_time, filtered_rules) do
is_rule_included =
cond do
zone_line.to == :max && rule.to == :max ->
true
zone_line.to == :max ->
{rule_to, rule_to_modifier} = rule.to
prev_period == nil || NaiveDateTime.compare(prev_period.to[rule_to_modifier], rule_to) == :lt
zone_line.from == :min || rule.to == :max ->
{zone_to, zone_to_modifier} = zone_line.to
rule_from = convert_date_tuple(rule.from, prev_period.std_offset_from_utc_time, prev_local_offset_from_std_time)
NaiveDateTime.compare(zone_to, rule_from[zone_to_modifier]) == :gt
true ->
{zone_to, zone_to_modifier} = zone_line.to
{rule_to, rule_to_modifier} = rule.to
rule_from = convert_date_tuple(rule.from, prev_period.std_offset_from_utc_time, prev_local_offset_from_std_time)
NaiveDateTime.compare(prev_period.to[rule_to_modifier], rule_to) == :lt
&& NaiveDateTime.compare(zone_to, rule_from[zone_to_modifier]) == :gt
end
if is_rule_included do
filter_rules_for_zone_line(rest_rules, zone_line, prev_period, rule.local_offset_from_std_time, [rule | filtered_rules])
else
filter_rules_for_zone_line(rest_rules, zone_line, prev_period, prev_local_offset_from_std_time, filtered_rules)
end
end
defp trim_zone_rules([], _zone_line, _), do: []
defp trim_zone_rules([first_rule | tail_rules] = rules, zone_line, prev_period) do
rules =
if rule_starts_before_zone_line_range?(zone_line, first_rule, if(prev_period == nil, do: 0, else: prev_period.local_offset_from_std_time)) do
[%{first_rule | from: zone_line.from} | tail_rules]
else
rules
end
last_rule = List.last(rules)
if rule_ends_after_zone_line_range?(zone_line, last_rule) do
[%{last_rule | to: zone_line.to} | (Enum.reverse(rules) |> tl())]
|> Enum.reverse()
else
rules
end
end
defp rule_starts_before_zone_line_range?(%{from: :min}, _rule, _), do: false
defp rule_starts_before_zone_line_range?(zone_line, rule, prev_local_offset_from_std_time) do
rule_from = convert_date_tuple(rule.from, zone_line.std_offset_from_utc_time, prev_local_offset_from_std_time)
%{from: {zone_from, zone_from_modifier}} = zone_line
NaiveDateTime.compare(rule_from[zone_from_modifier], zone_from) == :lt
end
defp rule_ends_after_zone_line_range?(%{to: :max}, _rule), do: false
defp rule_ends_after_zone_line_range?(_zone_line, %{to: :max}), do: true
defp rule_ends_after_zone_line_range?(zone_line, rule) do
rule_to = convert_date_tuple(rule.to, zone_line.std_offset_from_utc_time, rule.local_offset_from_std_time)
%{to: {zone_to, zone_to_modifier}} = zone_line
NaiveDateTime.compare(rule_to[zone_to_modifier], zone_to) == :gt
end
defp maybe_pad_left_rule([], _zone_line, _), do: []
defp maybe_pad_left_rule([first_rule | _] = rules, %{from: :min}, _) do
rule = %{
record_type: :rule,
from: :min,
name: "",
local_offset_from_std_time: 0,
letter: Enum.find(rules, & &1.local_offset_from_std_time == 0).letter,
to: first_rule.from
}
[rule | rules]
end
defp maybe_pad_left_rule(rules, _zone_line, nil), do: rules
defp maybe_pad_left_rule([first_rule | _] = rules, zone_line, prev_period) do
{rule_from, rule_from_modifier} = first_rule.from
if NaiveDateTime.compare(prev_period.to[rule_from_modifier], rule_from) == :lt do
letter =
# find first rule with local offset to 0
case Enum.find(rules, & &1.local_offset_from_std_time == 0) do
%{letter: letter} -> letter
_ -> ""
end
rule = %{
record_type: :rule,
from: zone_line.from,
name: first_rule.name,
local_offset_from_std_time: 0,
letter: letter,
to: first_rule.from
}
[rule | rules]
else
rules
end
end
defp do_build_periods_for_zone_line([], _zone_line, _prev_period, periods), do: periods
defp do_build_periods_for_zone_line([rule | rest_rules], zone_line, prev_period, periods) do
offset_diff = offset_diff_from_prev_period(zone_line, rule.local_offset_from_std_time, prev_period)
period_from =
case prev_period do
nil ->
convert_date_tuple(zone_line.from, zone_line.std_offset_from_utc_time, 0)
%{to: :max} ->
convert_date_tuple(rule.from, zone_line.std_offset_from_utc_time, prev_period.local_offset_from_std_time)
_ ->
add_to_and_convert_date_tuple({prev_period.to.wall, :wall}, offset_diff, zone_line.std_offset_from_utc_time, rule.local_offset_from_std_time)
end
period_to = convert_date_tuple(rule.to, zone_line.std_offset_from_utc_time, rule.local_offset_from_std_time)
if period_from != :min && period_to != :max && period_from.utc_gregorian_seconds == period_to.utc_gregorian_seconds do
raise "logic error"
end
period = %{
from: period_from,
to: period_to,
std_offset_from_utc_time: zone_line.std_offset_from_utc_time,
local_offset_from_std_time: rule.local_offset_from_std_time,
zone_abbr: zone_abbr(zone_line, rule.local_offset_from_std_time, rule.letter),
rules_and_template:
if(period_to == :max && prev_period && prev_period.to == :max) do
{zone_line.rules, zone_line.format_time_zone_abbr}
end
}
periods = concat_dedup_periods([period], periods)
do_build_periods_for_zone_line(rest_rules, zone_line, period, periods)
end
defp zone_abbr(zone_line, offset, letter \\ "") do
is_standard_time = offset == 0
cond do
String.contains?(zone_line.format_time_zone_abbr, "/") ->
[zone_abbr_std_time, zone_abbr_dst_time] = String.split(zone_line.format_time_zone_abbr, "/")
if(is_standard_time, do: zone_abbr_std_time, else: zone_abbr_dst_time)
String.contains?(zone_line.format_time_zone_abbr, "%s") ->
String.replace(zone_line.format_time_zone_abbr, "%s", letter)
true ->
zone_line.format_time_zone_abbr
end
end
defp add_to_and_convert_date_tuple(:min, _, _, _), do: :min
defp add_to_and_convert_date_tuple(:max, _, _, _), do: :max
defp add_to_and_convert_date_tuple({date, time_modifier}, add_seconds, std_offset_from_utc_time, local_offset_from_std_time) do
date = NaiveDateTime.add(date, add_seconds, :second)
convert_date_tuple({date, time_modifier}, std_offset_from_utc_time, local_offset_from_std_time)
end
defp convert_date_tuple(:min, _, _), do: :min
defp convert_date_tuple(:max, _, _), do: :max
defp convert_date_tuple({date, time_modifier}, std_offset_from_utc_time, local_offset_from_std_time) do
utc = convert_date(date, std_offset_from_utc_time, local_offset_from_std_time, time_modifier, :utc)
%{
utc: utc,
wall: convert_date(date, std_offset_from_utc_time, local_offset_from_std_time, time_modifier, :wall),
standard: convert_date(date, std_offset_from_utc_time, local_offset_from_std_time, time_modifier, :standard)
}
|> Map.put(:utc_gregorian_seconds, naive_datetime_to_gregorian_seconds(utc))
end
def periods_to_tuples_and_reverse(periods, periods_as_tuples \\ [], prev_period \\ nil)
def periods_to_tuples_and_reverse([], periods_as_tuples, _), do: periods_as_tuples
def periods_to_tuples_and_reverse([period | tail], periods_as_tuples, prev_period) do
period = {
if(period.from == :min, do: 0, else: period.from.utc_gregorian_seconds),
{
period.std_offset_from_utc_time,
period.local_offset_from_std_time,
period.zone_abbr
},
prev_period && elem(prev_period, 1),
period[:rules_and_template]
}
periods_to_tuples_and_reverse(tail, [period | periods_as_tuples], period)
end
defp convert_date(ndt, _, _, modifier, modifier), do: ndt
defp convert_date(ndt, standard_offset_from_utc_time, local_offset_from_standard_time, :wall, :utc) do
NaiveDateTime.add(ndt, -1 * (standard_offset_from_utc_time + local_offset_from_standard_time), :second)
end
defp convert_date(ndt, _standard_offset_from_utc_time, local_offset_from_standard_time, :wall, :standard) do
NaiveDateTime.add(ndt, -1 * local_offset_from_standard_time, :second)
end
defp convert_date(ndt, standard_offset_from_utc_time, local_offset_from_standard_time, :utc, :wall) do
NaiveDateTime.add(ndt, standard_offset_from_utc_time + local_offset_from_standard_time, :second)
end
defp convert_date(ndt, standard_offset_from_utc_time, _local_offset_from_standard_time, :utc, :standard) do
NaiveDateTime.add(ndt, standard_offset_from_utc_time, :second)
end
defp convert_date(ndt, standard_offset_from_utc_time, _local_offset_from_standard_time, :standard, :utc) do
NaiveDateTime.add(ndt, -1 * standard_offset_from_utc_time, :second)
end
defp convert_date(ndt, _standard_offset_from_utc_time, local_offset_from_standard_time, :standard, :wall) do
NaiveDateTime.add(ndt, local_offset_from_standard_time, :second)
end
defp naive_datetime_to_gregorian_seconds(datetime) do
NaiveDateTime.to_erl(datetime)
|> :calendar.datetime_to_gregorian_seconds()
end
end