Packages
tz
0.20.1
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, 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