317 lines
12 KiB
Elixir
317 lines
12 KiB
Elixir
defmodule Tzdata do
|
|
@moduledoc """
|
|
The Tzdata module provides data from the IANA tz database. Also known
|
|
as the Olson/Eggert database, zoneinfo, tzdata and other names.
|
|
|
|
A list of time zone names (e.g. `America/Los_Angeles`) are provided.
|
|
As well as functions for finding out the UTC offset, abbreviation,
|
|
standard offset (DST) for a specific point in time in a certain
|
|
timezone.
|
|
"""
|
|
|
|
@type gregorian_seconds :: non_neg_integer()
|
|
@type time_zone_period_limit :: gregorian_seconds() | :min | :max
|
|
@type time_zone_period :: %{
|
|
from: %{
|
|
standard: time_zone_period_limit,
|
|
utc: time_zone_period_limit,
|
|
wall: time_zone_period_limit
|
|
},
|
|
std_off: integer,
|
|
until: %{
|
|
standard: time_zone_period_limit,
|
|
utc: time_zone_period_limit,
|
|
wall: time_zone_period_limit
|
|
},
|
|
utc_off: integer,
|
|
zone_abbr: String.t()
|
|
}
|
|
|
|
@doc """
|
|
zone_list provides a list of all the zone names that can be used with
|
|
DateTime. This includes aliases.
|
|
"""
|
|
@spec zone_list() :: [Calendar.time_zone]
|
|
def zone_list, do: Tzdata.ReleaseReader.zone_and_link_list()
|
|
|
|
@doc """
|
|
Like zone_list, but excludes aliases for zones.
|
|
"""
|
|
@spec canonical_zone_list() :: [Calendar.time_zone]
|
|
def canonical_zone_list, do: Tzdata.ReleaseReader.zone_list()
|
|
|
|
@doc """
|
|
A list of aliases for zone names. For instance Europe/Jersey
|
|
is an alias for Europe/London. Aliases are also known as linked zones.
|
|
"""
|
|
@spec zone_alias_list() :: [Calendar.time_zone]
|
|
def zone_alias_list, do: Tzdata.ReleaseReader.link_list()
|
|
|
|
@doc """
|
|
Takes the name of a zone. Returns true if zone exists. Otherwise false.
|
|
|
|
iex> Tzdata.zone_exists? "Pacific/Auckland"
|
|
true
|
|
iex> Tzdata.zone_exists? "America/Sao_Paulo"
|
|
true
|
|
iex> Tzdata.zone_exists? "Europe/Jersey"
|
|
true
|
|
"""
|
|
@spec zone_exists?(String.t) :: boolean()
|
|
def zone_exists?(name), do: Enum.member?(zone_list(), name)
|
|
|
|
@doc """
|
|
Takes the name of a zone. Returns true if zone exists and is canonical.
|
|
Otherwise false.
|
|
|
|
iex> Tzdata.canonical_zone? "Europe/London"
|
|
true
|
|
iex> Tzdata.canonical_zone? "Europe/Jersey"
|
|
false
|
|
"""
|
|
@spec canonical_zone?(Calendar.time_zone) :: boolean()
|
|
def canonical_zone?(name), do: Enum.member?(canonical_zone_list(), name)
|
|
|
|
@doc """
|
|
Takes the name of a zone. Returns true if zone exists and is an alias.
|
|
Otherwise false.
|
|
|
|
iex> Tzdata.zone_alias? "Europe/Jersey"
|
|
true
|
|
iex> Tzdata.zone_alias? "Europe/London"
|
|
false
|
|
"""
|
|
@spec zone_alias?(Calendar.time_zone) :: boolean()
|
|
def zone_alias?(name), do: Enum.member?(zone_alias_list(), name)
|
|
|
|
@doc """
|
|
Returns a map of links. Also known as aliases.
|
|
|
|
iex> Tzdata.links()["Europe/Jersey"]
|
|
"Europe/London"
|
|
"""
|
|
@spec links() :: %{Calendar.time_zone => Calendar.time_zone}
|
|
def links, do: Tzdata.ReleaseReader.links()
|
|
|
|
@doc """
|
|
Returns a map with keys being group names and the values lists of
|
|
time zone names. The group names mirror the file names used by the tzinfo
|
|
database.
|
|
"""
|
|
@spec zone_lists_grouped() :: %{atom() => [Calendar.time_zone]}
|
|
def zone_lists_grouped, do: Tzdata.ReleaseReader.by_group()
|
|
|
|
@doc """
|
|
Returns tzdata release version as a string.
|
|
|
|
Example:
|
|
|
|
Tzdata.tzdata_version
|
|
"2014i"
|
|
"""
|
|
@spec tzdata_version() :: String.t
|
|
def tzdata_version, do: Tzdata.ReleaseReader.release_version()
|
|
|
|
@doc """
|
|
Returns a list of periods for the `zone_name` provided as an argument.
|
|
|
|
A period in this case is a period of time where the UTC offset and standard
|
|
offset are in a certain way. When they change, for instance in spring when
|
|
DST takes effect, a new period starts. For instance a period can begin in
|
|
spring when winter time ends and summer time begins. The period lasts until
|
|
DST ends.
|
|
|
|
If either the UTC or standard offset change for any reason, a new period
|
|
begins. For instance instead of DST ending or beginning, a rule change
|
|
that changes the UTC offset will also mean a new period.
|
|
|
|
The result is tagged with :ok if the zone_name is correct.
|
|
|
|
The from and until times can be :mix, :max or gregorian seconds.
|
|
|
|
## Example
|
|
|
|
iex> Tzdata.periods("Europe/Madrid") |> elem(1) |> Enum.take(1)
|
|
[%{from: %{standard: :min, utc: :min, wall: :min}, std_off: 0,
|
|
until: %{standard: 59989763760, utc: 59989764644, wall: 59989763760},
|
|
utc_off: -884, zone_abbr: "LMT"}]
|
|
iex> Tzdata.periods("Not existing")
|
|
{:error, :not_found}
|
|
"""
|
|
@spec periods(Calendar.time_zone) :: {:ok, [time_zone_period]} | {:error, atom()}
|
|
def periods(zone_name) do
|
|
{tag, p} = Tzdata.ReleaseReader.periods_for_zone_or_link(zone_name)
|
|
case tag do
|
|
:ok ->
|
|
mapped_p = for {_, f_utc, f_wall, f_std, u_utc, u_wall, u_std, utc_off, std_off, zone_abbr} <- p do
|
|
%{
|
|
std_off: std_off,
|
|
utc_off: utc_off,
|
|
from: %{utc: f_utc, wall: f_wall, standard: f_std},
|
|
until: %{utc: u_utc, standard: u_std, wall: u_wall},
|
|
zone_abbr: zone_abbr
|
|
}
|
|
end
|
|
{:ok, mapped_p}
|
|
_ -> {:error, p}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Get the periods that cover a certain point in time. Usually it will be a list
|
|
with just one period. But in some cases it will be zero or two periods. For
|
|
instance when going from summer to winter time (DST to standard time) there
|
|
will be an overlap if `time_type` is `:wall`.
|
|
|
|
`zone_name` should be a valid time zone name. The function `zone_list/0`
|
|
provides a valid list of valid zone names.
|
|
|
|
`time_point` is the point in time in gregorian seconds (see erlang
|
|
calendar module documentation for more info on gregorian seconds).
|
|
|
|
Valid values for `time_type` is `:utc`, `:wall` or `:standard`.
|
|
|
|
## Examples
|
|
|
|
# 63555753600 seconds is equivalent to {{2015, 1, 1}, {0, 0, 0}}
|
|
iex> Tzdata.periods_for_time("Asia/Tokyo", 63587289600, :wall)
|
|
[%{from: %{standard: 61589289600, utc: 61589257200, wall: 61589289600}, std_off: 0,
|
|
until: %{standard: :max, utc: :max, wall: :max}, utc_off: 32400, zone_abbr: "JST"}]
|
|
|
|
# 63612960000 seconds is equivalent to 2015-10-25 02:40:00 and is an ambiguous
|
|
# wall time for the zone. So two possible periods will be returned.
|
|
iex> Tzdata.periods_for_time("Europe/Copenhagen", 63612960000, :wall)
|
|
[%{from: %{standard: 63594813600, utc: 63594810000, wall: 63594817200}, std_off: 3600,
|
|
until: %{standard: 63612957600, utc: 63612954000, wall: 63612961200}, utc_off: 3600, zone_abbr: "CEST"},
|
|
%{from: %{standard: 63612957600, utc: 63612954000, wall: 63612957600}, std_off: 0,
|
|
until: %{standard: 63626263200, utc: 63626259600, wall: 63626263200}, utc_off: 3600, zone_abbr: "CET"}]
|
|
|
|
# 63594816000 seconds is equivalent to 2015-03-29 02:40:00 and is a
|
|
# non-existing wall time for the zone. It is spring and the clock skips that hour.
|
|
iex> Tzdata.periods_for_time("Europe/Copenhagen", 63594816000, :wall)
|
|
[]
|
|
"""
|
|
@spec periods_for_time(Calendar.time_zone, gregorian_seconds, :standard | :wall | :utc) :: [time_zone_period] | {:error, term}
|
|
def periods_for_time(zone_name, time_point, time_type) do
|
|
case possible_periods_for_zone_and_time(zone_name, time_point, time_type) do
|
|
{:ok, periods} ->
|
|
match_fn = fn %{from: from, until: until} ->
|
|
smaller_than_or_equals(Map.get(from, time_type), time_point) &&
|
|
bigger_than(Map.get(until, time_type), time_point)
|
|
end
|
|
do_consecutive_matching(periods, match_fn, [], false)
|
|
{:error, _} = error ->
|
|
error
|
|
end
|
|
end
|
|
|
|
# Like Enum.filter, but returns the first consecutive result.
|
|
# If we have found consecutive matches we do not need to look at the
|
|
# remaining list.
|
|
defp do_consecutive_matching([], _fun, [], _did_last_match), do: []
|
|
defp do_consecutive_matching([], _fun, matched, _did_last_match), do: matched
|
|
defp do_consecutive_matching(_list, _fun, matched, false) when matched != [] do
|
|
# If there are matches and previous did not match then the matches are no
|
|
# long consecutive. So we return the result.
|
|
matched |> Enum.reverse
|
|
end
|
|
defp do_consecutive_matching([h|t], fun, matched, _did_last_match) do
|
|
if fun.(h) == true do
|
|
do_consecutive_matching(t, fun, [h|matched], true)
|
|
else
|
|
do_consecutive_matching(t, fun, matched, false)
|
|
end
|
|
end
|
|
|
|
# Use dynamic periods for points in time that are about 40 years into the future
|
|
@years_in_the_future_where_precompiled_periods_are_used 40
|
|
@point_from_which_to_use_dynamic_periods :calendar.datetime_to_gregorian_seconds {{(:calendar.universal_time()|>elem(0)|>elem(0)) + @years_in_the_future_where_precompiled_periods_are_used, 1, 1}, {0, 0, 0}}
|
|
defp possible_periods_for_zone_and_time(zone_name, time_point, time_type) when time_point >= @point_from_which_to_use_dynamic_periods do
|
|
if Tzdata.FarFutureDynamicPeriods.zone_in_30_years_in_eternal_period?(zone_name) do
|
|
periods(zone_name)
|
|
else
|
|
link_status = Tzdata.ReleaseReader.links() |> Map.get(zone_name)
|
|
if link_status == nil do
|
|
Tzdata.FarFutureDynamicPeriods.periods_for_point_in_time(time_point, zone_name)
|
|
else
|
|
possible_periods_for_zone_and_time(link_status, time_point, time_type)
|
|
end
|
|
end
|
|
end
|
|
defp possible_periods_for_zone_and_time(zone_name, time_point, time_type) do
|
|
{:ok, periods} = Tzdata.ReleaseReader.periods_for_zone_time_and_type(zone_name, time_point, time_type)
|
|
mapped_periods = periods
|
|
|> Enum.sort_by(fn {_, from_utc, _, _, _, _, _, _, _, _} -> -(from_utc |> Tzdata.ReleaseReader.delimiter_to_number) end)
|
|
|> Enum.map(
|
|
fn {_, f_utc, f_wall, f_std, u_utc, u_wall, u_std, utc_off, std_off, zone_abbr} ->
|
|
%{
|
|
std_off: std_off,
|
|
utc_off: utc_off,
|
|
from: %{utc: f_utc, wall: f_wall, standard: f_std},
|
|
until: %{utc: u_utc, standard: u_std, wall: u_wall},
|
|
zone_abbr: zone_abbr
|
|
}
|
|
end
|
|
)
|
|
{:ok, mapped_periods}
|
|
end
|
|
|
|
@doc """
|
|
Get a list of maps with known leap seconds and
|
|
the difference between UTC and the TAI in seconds.
|
|
|
|
See also `leap_seconds/1`
|
|
|
|
## Example
|
|
|
|
iex> Tzdata.leap_seconds_with_tai_diff() |> Enum.take(2)
|
|
[%{date_time: {{1972, 6, 30}, {23, 59, 60}}, tai_diff: 11},
|
|
%{date_time: {{1972, 12, 31}, {23, 59, 60}}, tai_diff: 12}]
|
|
"""
|
|
@spec leap_seconds_with_tai_diff() :: [%{date_time: :calendar.datetime(), tai_diff: integer}]
|
|
def leap_seconds_with_tai_diff do
|
|
leap_seconds_data = Tzdata.ReleaseReader.leap_sec_data()
|
|
leap_seconds_data.leap_seconds
|
|
end
|
|
|
|
@doc """
|
|
Get a list of known leap seconds. The leap seconds are datetime
|
|
tuples representing the extra leap second to be inserted.
|
|
The date-times are in UTC.
|
|
|
|
See also `leap_seconds_with_tai_diff/1`
|
|
|
|
## Example
|
|
|
|
iex> Tzdata.leap_seconds() |> Enum.take(2)
|
|
[{{1972, 6, 30}, {23, 59, 60}},
|
|
{{1972, 12, 31}, {23, 59, 60}}]
|
|
"""
|
|
@spec leap_seconds() :: [:calendar.datetime()]
|
|
def leap_seconds do
|
|
for %{date_time: date_time} <- leap_seconds_with_tai_diff() do
|
|
date_time
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
The time when the leap second information returned from the other leap second
|
|
related function expires. The date-time is in UTC.
|
|
|
|
## Example
|
|
|
|
Tzdata.leap_second_data_valid_until
|
|
{{2015, 12, 28}, {0, 0, 0}}
|
|
"""
|
|
@spec leap_second_data_valid_until() :: :calendar.datetime()
|
|
def leap_second_data_valid_until do
|
|
leap_seconds_data = Tzdata.ReleaseReader.leap_sec_data()
|
|
leap_seconds_data.valid_until
|
|
end
|
|
|
|
defp smaller_than_or_equals(:min, _), do: true
|
|
defp smaller_than_or_equals(first, second), do: first <= second
|
|
defp bigger_than(:max, _), do: true
|
|
defp bigger_than(first, second), do: first > second
|
|
end
|