Packages
tz
0.8.0
0.28.2
0.28.1
0.28.0
0.27.3
0.27.2
0.27.1
0.27.0
0.26.6
0.26.5
0.26.4
0.26.3
0.26.2
0.26.1
0.26.0
0.25.1
0.25.0
0.24.0
0.23.0
0.22.0
0.21.1
0.21.0
0.20.1
0.20.0
0.19.0
0.17.0
0.16.2
0.16.1
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.2
0.8.1
0.8.0
0.7.3
0.7.2
0.7.1
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
Time zone support for Elixir
Current section
Files
Jump to
Current section
Files
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, :minmax, 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, :minmax, [])
Map.put(periods_by_year, :minmax, 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