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, prev_period \\ nil, periods \\ [])
def build_periods([], _rule_records, _prev_period, periods), do: periods
def build_periods([zone_line | rest_zone_lines], rule_records, prev_period, periods) do
rules = Map.get(rule_records, zone_line.rules, zone_line.rules)
new_periods = build_periods_for_zone_line(zone_line, rules, prev_period)
build_periods(rest_zone_lines, rule_records, List.last(new_periods), periods ++ new_periods)
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 maybe_build_gap_or_overlap_period(_zone_line, _local_offset, %{to: :max}, _period), do: nil
defp maybe_build_gap_or_overlap_period(zone_line, local_offset, prev_period, period) do
offset_diff = offset_diff_from_prev_period(zone_line, local_offset, prev_period)
if offset_diff > 0 do
if period.from.utc_gregorian_seconds != prev_period.to.utc_gregorian_seconds do
raise "logic error"
end
%{
period_before_gap: prev_period,
period_after_gap: period,
from: prev_period.to,
to: period.from
}
else
if offset_diff < 0 do
if period.from.utc_gregorian_seconds != prev_period.to.utc_gregorian_seconds do
raise "logic error"
end
%{
from: period.from,
to: prev_period.to
}
end
end
end
defp build_periods_for_zone_line(zone_line, offset, 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
period = %{
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_line.format_time_zone_abbr
}
maybe_gap_or_overlap_period = maybe_build_gap_or_overlap_period(zone_line, offset, prev_period, period)
if maybe_gap_or_overlap_period, do: [maybe_gap_or_overlap_period, period], else: [period]
end
defp build_periods_for_zone_line(zone_line, rules, 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
zone_line_rules =
if(zone_line.from == :min && zone_line.to == :max) do
rules
else
filter_rules_for_zone_line(zone_line, rules, prev_period, if(prev_period == nil, do: 0, else: prev_period.local_offset_from_std_time))
end
zone_line_rules = maybe_pad_left_rule(zone_line, zone_line_rules, prev_period)
zone_line_rules = trim_zone_rules(zone_line, zone_line_rules, prev_period)
do_build_periods_for_zone_line(zone_line, zone_line_rules, prev_period, [])
end
defp filter_rules_for_zone_line(zone_line, rules, prev_period, prev_local_offset_from_std_time, filtered_rules \\ [])
defp filter_rules_for_zone_line(_zone_line, [], _, _, filtered_rules), do: filtered_rules
defp filter_rules_for_zone_line(zone_line, [rule | rest_rules], 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
if prev_period == nil do
true
else
NaiveDateTime.compare(prev_period.to[rule_to_modifier], rule_to) == :lt
end
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(zone_line, rest_rules, prev_period, rule.local_offset_from_std_time, filtered_rules ++ [rule])
else
filter_rules_for_zone_line(zone_line, rest_rules, prev_period, prev_local_offset_from_std_time, filtered_rules)
end
end
defp trim_zone_rules(_zone_line, [], _), do: []
defp trim_zone_rules(zone_line, rules, prev_period) do
first_rule = List.first(rules)
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} | tl(rules)]
else
rules
end
last_rule = List.last(rules)
if rule_ends_after_zone_line_range?(zone_line, last_rule) do
rules_without_last = Enum.reverse(rules) |> tl() |> Enum.reverse()
rules_without_last ++ [%{last_rule | to: zone_line.to}]
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(%{from: :min}, [first_rule | _] = rules, _) 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(_zone_line, rules, nil), do: rules
defp maybe_pad_left_rule(zone_line, [first_rule | _] = rules, prev_period) do
{rule_from, rule_from_modifier} = first_rule.from
if NaiveDateTime.compare(prev_period.to[rule_from_modifier], rule_from) == :lt do
rule = %{
record_type: :rule,
from: zone_line.from,
name: first_rule.name,
local_offset_from_std_time: 0,
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(zone_line, [rule | rest_rules], 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)
}
period =
if period_to == :max do
period
|> Map.put(:raw_rule, rule.raw)
|> Map.put(:zone_line, zone_line)
else
period
end
maybe_gap_or_overlap_period = maybe_build_gap_or_overlap_period(zone_line, rule.local_offset_from_std_time, prev_period, period)
periods = if maybe_gap_or_overlap_period, do: periods ++ [maybe_gap_or_overlap_period], else: periods
periods = periods ++ [period]
do_build_periods_for_zone_line(zone_line, rest_rules, period, periods)
end
defp zone_abbr(zone_line, rule) do
is_standard_time = rule.local_offset_from_std_time == 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", rule.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
map_of_dates = %{
wall: convert_date(date, std_offset_from_utc_time, local_offset_from_std_time, time_modifier, :wall),
utc: convert_date(date, std_offset_from_utc_time, local_offset_from_std_time, time_modifier, :utc),
standard: convert_date(date, std_offset_from_utc_time, local_offset_from_std_time, time_modifier, :standard)
}
map_of_dates
|> Map.put(:utc_gregorian_seconds, NaiveDateTime.diff(map_of_dates.utc, ~N[0000-01-01 00:00:00]))
|> Map.put(:wall_gregorian_seconds, NaiveDateTime.diff(map_of_dates.wall, ~N[0000-01-01 00:00:00]))
end
defp is_period_gap?(%{period_before_gap: _}), do: true
defp is_period_gap?(_), do: false
def shrink_and_reverse_periods(periods, shrank_periods \\ [])
def shrink_and_reverse_periods([], shrank_periods), do: shrank_periods
def shrink_and_reverse_periods([period | tail], shrank_periods) do
{local_offset_from_std_time, period} = pop_in(period, [:local_offset_from_std_time])
period =
if local_offset_from_std_time != nil do
Map.put(period, :std_offset, local_offset_from_std_time)
else
period
end
{std_offset_from_utc_time, period} = pop_in(period, [:std_offset_from_utc_time])
period =
if std_offset_from_utc_time != nil do
Map.put(period, :utc_offset, std_offset_from_utc_time)
else
period
end
period =
if is_period_gap?(period) do
{local_offset_from_std_time, period} = pop_in(period, [:period_before_gap, :local_offset_from_std_time])
period = put_in(period, [:period_before_gap, :std_offset], local_offset_from_std_time)
{std_offset_from_utc_time, period} = pop_in(period, [:period_before_gap, :std_offset_from_utc_time])
period = put_in(period, [:period_before_gap, :utc_offset], std_offset_from_utc_time)
{local_offset_from_std_time, period} = pop_in(period, [:period_after_gap, :local_offset_from_std_time])
period = put_in(period, [:period_after_gap, :std_offset], local_offset_from_std_time)
{std_offset_from_utc_time, period} = pop_in(period, [:period_after_gap, :std_offset_from_utc_time])
put_in(period, [:period_after_gap, :utc_offset], std_offset_from_utc_time)
else
period
end
period =
if period.from != :min do
{_, period} = pop_in(period, [:from, :standard])
{_, period} = pop_in(period, [:from, :utc])
period
else
period
end
period =
if period.from != :min && !is_period_gap?(period) do
{_, period} = pop_in(period, [:from, :wall])
period
else
period
end
period =
if period.to != :max do
{_, period} = pop_in(period, [:to, :standard])
{_, period} = pop_in(period, [:to, :utc])
period
else
period
end
period =
if period.to != :max && !is_period_gap?(period) do
{_, period} = pop_in(period, [:to, :wall])
period
else
period
end
{_, period} = pop_in(period, [:period_before_gap, :from])
{_, period} = pop_in(period, [:period_before_gap, :to])
{_, period} = pop_in(period, [:period_after_gap, :from])
{_, period} = pop_in(period, [:period_after_gap, :to])
shrink_and_reverse_periods(tail, [period | shrank_periods])
end
def group_periods_by_year(periods) do
Enum.reduce(periods, %{}, fn
%{from: :min, to: :max}, periods_by_year ->
Map.put(periods_by_year, :other, periods)
period, periods_by_year ->
from_year =
if period.from != :min do
min(
NaiveDateTime.add(~N[0000-01-01 00:00:00], period.from.utc_gregorian_seconds).year,
NaiveDateTime.add(~N[0000-01-01 00:00:00], period.from.wall_gregorian_seconds).year
)
end
to_year =
if period.to != :max do
max(
NaiveDateTime.add(~N[0000-01-01 00:00:00], period.to.utc_gregorian_seconds).year,
NaiveDateTime.add(~N[0000-01-01 00:00:00], period.to.wall_gregorian_seconds).year
)
end
periods_by_year =
Enum.reduce(Range.new(from_year || to_year - 1, to_year || from_year + 1), periods_by_year, fn
year, periods_by_year ->
list = Map.get(periods_by_year, year, [])
Map.put(periods_by_year, year, list ++ [period])
end)
if period.from == :min || period.to == :max do
list = Map.get(periods_by_year, :other, [])
Map.put(periods_by_year, :other, list ++ [period])
else
periods_by_year
end
end)
end
def reject_time_zone_periods_before_year(periods_by_year, nil) do
periods_by_year
end
def reject_time_zone_periods_before_year(periods_by_year, reject_before_year) do
Enum.reject(periods_by_year, fn {year, _} -> year < reject_before_year end)
|> Enum.into(%{})
end
defp convert_date(naive_date_time, _standard_offset_from_utc_time, _local_offset_from_standard_time, :wall, :wall), do: naive_date_time
defp convert_date(naive_date_time, _standard_offset_from_utc_time, _local_offset_from_standard_time, :utc, :utc), do: naive_date_time
defp convert_date(naive_date_time, _standard_offset_from_utc_time, _local_offset_from_standard_time, :standard, :standard), do: naive_date_time
defp convert_date(naive_date_time, standard_offset_from_utc_time, local_offset_from_standard_time, :wall, :utc) do
NaiveDateTime.add(naive_date_time, -1 * (standard_offset_from_utc_time + local_offset_from_standard_time), :second)
end
defp convert_date(naive_date_time, _standard_offset_from_utc_time, local_offset_from_standard_time, :wall, :standard) do
NaiveDateTime.add(naive_date_time, -1 * local_offset_from_standard_time, :second)
end
defp convert_date(naive_date_time, standard_offset_from_utc_time, local_offset_from_standard_time, :utc, :wall) do
NaiveDateTime.add(naive_date_time, standard_offset_from_utc_time + local_offset_from_standard_time, :second)
end
defp convert_date(naive_date_time, standard_offset_from_utc_time, _local_offset_from_standard_time, :utc, :standard) do
NaiveDateTime.add(naive_date_time, standard_offset_from_utc_time, :second)
end
defp convert_date(naive_date_time, standard_offset_from_utc_time, _local_offset_from_standard_time, :standard, :utc) do
NaiveDateTime.add(naive_date_time, -1 * standard_offset_from_utc_time, :second)
end
defp convert_date(naive_date_time, _standard_offset_from_utc_time, local_offset_from_standard_time, :standard, :wall) do
NaiveDateTime.add(naive_date_time, local_offset_from_standard_time, :second)
end
end