Packages
tz
0.27.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/tz/compiler.ex
defmodule Tz.Compiler do
@moduledoc false
require Logger
require Tz.IanaFileParser
require Tz.PeriodsBuilder
alias Tz.IanaDataDir
alias Tz.IanaFileParser
alias Tz.PeriodsBuilder
def compile() do
{tzdata_dir_path, tzdata_version} = tzdata_dir_and_version()
validate_tzdata_dir_has_required_files(tzdata_dir_path)
{periods_and_links, ongoing_rules} =
for filename <-
~w(africa antarctica asia australasia backward etcetera europe northamerica southamerica)s do
{zone_records, rule_records, link_records, ongoing_rules} =
IanaFileParser.parse(Path.join(tzdata_dir_path, filename))
Enum.each(rule_records, fn {rule_name, rules} ->
ongoing_rules_count = Enum.count(rules, &(&1.to == :max))
if ongoing_rules_count > 2 do
raise "unexpected case: #{ongoing_rules_count} ongoing rules for rule \"#{rule_name}\""
end
end)
periods =
for {zone_name, zone_lines} <- zone_records do
periods =
PeriodsBuilder.build_periods(zone_lines, rule_records)
|> PeriodsBuilder.periods_to_tuples_and_reverse()
|> reject_periods_before_year()
if length(periods) == 0 do
raise "no periods for time zone #{zone_name}"
end
{:periods, zone_name, periods}
end
links =
for link <- link_records do
{:link, link.link_zone_name, link.canonical_zone_name}
end
{periods ++ links, ongoing_rules}
end
|> Enum.reduce(
{[], %{}},
fn {periods_and_links, ongoing_rules}, {all_periods_and_links, all_ongoing_rules} ->
{periods_and_links ++ all_periods_and_links,
Map.merge(ongoing_rules, all_ongoing_rules)}
end
)
compile_periods(periods_and_links, tzdata_version)
compile_map_ongoing_changing_rules(ongoing_rules)
end
defp tzdata_dir_and_version() do
IanaDataDir.maybe_copy_iana_files_to_custom_dir()
cond do
tzdata_dir_path = IanaDataDir.relevant_tzdata_dir_path() ->
"tzdata" <> tzdata_version = Path.basename(tzdata_dir_path)
{tzdata_dir_path, tzdata_version}
IanaDataDir.forced_iana_version() == nil ->
raise "tzdata files not found"
tzdata_dir_path = IanaDataDir.latest_tzdata_dir_path() ->
"tzdata" <> installed_iana_version = Path.basename(tzdata_dir_path)
forced_iana_version = IanaDataDir.forced_iana_version()
raise(
"Missing tzdata#{forced_iana_version} files.\n" <>
"1. Temporarily remove the :iana_version config\n" <>
"2. Download version #{forced_iana_version} " <>
"by running: mix tz.download #{forced_iana_version}\n" <>
"3. Restore the :iana_version config\n" <>
"4. Recompile the time zone periods " <>
"by running: mix deps.compile tz --force\n" <>
"5. Make sure the periods are compiled with tzdata#{forced_iana_version} " <>
"by running: Tz.iana_version()"
)
{tzdata_dir_path, installed_iana_version}
true ->
raise "tzdata files not found"
end
end
defp reject_periods_before_year(periods) do
case Application.get_env(:tz, :reject_periods_before_year) do
nil ->
periods
reject_before_year ->
filtered_periods =
Enum.reject(periods, fn {secs, _, _, _} ->
%{year: year} = gregorian_seconds_to_naive_datetime(secs)
year < reject_before_year
end)
if length(filtered_periods) > 0 do
filtered_periods
else
periods
end
end
end
defp gregorian_seconds_to_naive_datetime(seconds) do
:calendar.gregorian_seconds_to_datetime(seconds)
|> NaiveDateTime.from_erl!()
end
def compile_periods(periods_and_links, tzdata_version) do
quoted = [
quote do
@moduledoc false
def iana_version() do
unquote(tzdata_version)
end
def compiled_at() do
unquote(Macro.escape(DateTime.utc_now()))
end
def next_period(%DateTime{} = datetime) do
{gregorian_secs, _} = DateTime.to_gregorian_seconds(datetime)
{:ok, periods} = periods(datetime.time_zone)
reversed_periods = Enum.reverse(periods)
period = Enum.find(reversed_periods, fn {from, _, _, _} -> gregorian_secs < from end)
if period do
period
else
{utc_secs, {utc_to_std_offset, _, _}, _, rules_and_template} = hd(periods)
periods =
Tz.TimeZoneDatabase.generate_dynamic_periods(
utc_secs,
utc_to_std_offset,
rules_and_template
)
reversed_periods = Enum.reverse(periods)
Enum.find(reversed_periods, fn {from, _, _, _} -> gregorian_secs < from end)
end
end
end,
for period_or_link <- periods_and_links do
case period_or_link do
{:link, link_zone_name, canonical_zone_name} ->
quote do
def periods(unquote(link_zone_name)) do
periods(unquote(Macro.escape(canonical_zone_name)))
end
end
{:periods, zone_name, periods} ->
quote do
def periods(unquote(zone_name)) do
{:ok, unquote(Macro.escape(periods))}
end
end
end
end,
quote do
def periods(_) do
{:error, :time_zone_not_found}
end
end
]
module = :"Elixir.Tz.PeriodsProvider"
Module.create(module, quoted, Macro.Env.location(__ENV__))
:code.purge(module)
end
defp compile_map_ongoing_changing_rules(ongoing_rules) do
quoted = [
quote do
@moduledoc false
end,
for {rule_name, rules} <- ongoing_rules do
quote do
def rules(unquote(rule_name)) do
unquote(Macro.escape(rules))
end
end
end
]
module = :"Elixir.Tz.OngoingChangingRulesProvider"
Module.create(module, quoted, Macro.Env.location(__ENV__))
:code.purge(module)
end
defp validate_tzdata_dir_has_required_files(tzdata_dir_path) do
required_files = [
"africa",
"antarctica",
"asia",
"australasia",
"backward",
"etcetera",
"europe",
"northamerica",
"southamerica",
"iso3166.tab",
"zone1970.tab"
]
missing_files = Enum.filter(required_files, &(!File.exists?(Path.join(tzdata_dir_path, &1))))
if length(missing_files) > 0 do
raise "files #{inspect(missing_files)} are missing in #{tzdata_dir_path}"
end
end
end