Packages
tz
0.8.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/compiler.ex
defmodule Tz.Compiler do
@moduledoc false
require Tz.IanaFileParser
require Tz.PeriodsBuilder
alias Tz.PeriodsBuilder
alias Tz.IanaFileParser
@reject_time_zone_periods_before_year Application.get_env(:tz, :reject_time_zone_periods_before_year)
@default_area_name "Misc"
def compile() do
tz_data_dir_name =
File.ls!(:code.priv_dir(:tz))
|> Enum.filter(&Regex.match?(~r/^tzdata20[0-9]{2}[a-z]$/, &1))
|> Enum.max()
periods_and_links =
for filename <- ~w(africa antarctica asia australasia backward etcetera europe northamerica southamerica)s do
{zone_records, rule_records, link_records} =
IanaFileParser.parse(Path.join([:code.priv_dir(:tz), tz_data_dir_name, 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.shrink_and_reverse_periods()
|> PeriodsBuilder.group_periods_by_year()
|> PeriodsBuilder.reject_time_zone_periods_before_year(@reject_time_zone_periods_before_year)
{:periods, zone_name, periods}
end
links =
for link <- link_records do
{:link, link.link_zone_name, link.canonical_zone_name}
end
periods ++ links
end
|> List.flatten()
periods_and_links_by_area =
Enum.group_by(periods_and_links, fn
{:link, link_zone_name, _} -> get_time_zone_area(link_zone_name) || @default_area_name
{:periods, zone_name, _} -> get_time_zone_area(zone_name) || @default_area_name
end)
# TODO: `:erlang.get(:elixir_compiler_pid)` is a private API.
# Replace by `Code.can_await_modules_compilation?` from Elixir v1.11
fn_async =
case :erlang.get(:elixir_compiler_pid) do
:undefined -> &Task.async/1
_ -> &Kernel.ParallelCompiler.async/1
end
periods_and_links_by_area
|> Enum.map(&fn_async.(fn -> create_area_module(&1) end))
|> Enum.each(&Task.await(&1, :infinity))
contents = [
quote do
@moduledoc(false)
def version() do
unquote(String.trim(tz_data_dir_name, "tzdata"))
end
end,
for {area, _} <- periods_and_links_by_area, area != @default_area_name do
module = :"Elixir.Tz.Area.#{area}"
quote do
def periods_by_year(unquote(area <> "/") <> rest) do
apply(unquote(module), :periods_by_year, [unquote(area <> "/") <> rest])
end
end
end,
quote do
def periods_by_year(time_zone) do
apply(unquote(:"Elixir.Tz.Area.#{@default_area_name}"), :periods_by_year, [time_zone])
end
end
]
module = :"Elixir.Tz.PeriodsProvider"
Module.create(module, contents, Macro.Env.location(__ENV__))
:code.purge(module)
end
defp get_time_zone_area(time_zone) do
case String.split(time_zone, "/") do
[_] -> nil
split_time_zone -> hd(split_time_zone)
end
end
defp create_area_module({area, periods_and_links}) do
contents = [
quote do
@moduledoc(false)
end,
for period_or_link <- periods_and_links do
case period_or_link do
{:link, link_zone_name, canonical_zone_name} ->
area = get_time_zone_area(canonical_zone_name)
canonical_module = :"Elixir.Tz.Area.#{area}"
quote do
def periods_by_year(unquote(link_zone_name)) do
apply(unquote(canonical_module), :periods_by_year, [unquote(canonical_zone_name)])
end
end
{:periods, zone_name, periods_by_year} ->
quote do
def periods_by_year(unquote(zone_name)) do
{:ok, unquote(Macro.escape(periods_by_year))}
end
end
end
end,
quote do
def periods_by_year(_) do
{:error, :time_zone_not_found}
end
end
]
module = :"Elixir.Tz.Area.#{area}"
Module.create(module, contents, Macro.Env.location(__ENV__))
:code.purge(module)
end
end