defmodule Zoneinfo do
@moduledoc """
Elixir time zone support for your OS-supplied time zone database
Tell Elixir to use this as the default time zone database by running:
```elixir
Calendar.put_time_zone_database(Zoneinfo.TimeZoneDatabase)
```
Time zone data is loaded from the path returned by `tzpath/0`. The default
is to use `/usr/share/zoneinfo`, but that may be changed by setting the
`$TZDIR` environment or adding the following to your project's `config.exs`:
```elixir
config :zoneinfo, tzpath: "/custom/location"
```
Call `time_zones/0` to get the list of supported time zones.
"""
@doc """
Return all known time zones
This function scans the path returned by `tzpath/0` for all time zones and
performs a basic check on each file. It may not be fast. It will not return
the aliases that zoneinfo uses for backwards compatibility even though they
may still work.
"""
@spec time_zones() :: [String.t()]
def time_zones() do
path = Path.expand(tzpath())
Path.join(path, "**")
|> Path.wildcard()
# Filter out symlinks to old time zones names and anything that doesn't
# look like it contains TZif data
|> Enum.filter(fn f -> File.lstat!(f, time: :posix).type == :regular and contains_tzif?(f) end)
# Fix up the remaining paths to look like time zones
|> Enum.map(&String.replace_leading(&1, path <> "/", ""))
end
@doc """
Return the path to the time zone files
"""
@spec tzpath() :: binary()
def tzpath() do
# TZDIR is preferred. TZPATH was originally used and is supported for
# backwards compatibility.
with nil <- Application.get_env(:zoneinfo, :tzpath),
nil <- System.get_env("TZDIR"),
nil <- System.get_env("TZPATH") do
"/usr/share/zoneinfo"
end
end
@doc """
Return whether a time zone is valid
"""
@spec valid_time_zone?(String.t()) :: boolean
def valid_time_zone?(time_zone) do
case Zoneinfo.Cache.get(time_zone) do
{:ok, _} ->
true
_ ->
false
end
end
@doc """
Return Zoneinfo metadata on a time zone
The returned metadata is limited to what's available in the source TZif data
file for the time zone. It's mostly useful for verifying that time zone
information is available for dates used in your application. Note that proper
time zone calculations depend on many things and it's possible that they'll
work outside of the returned ranged. However, it's also possible that a time
zone database was built and then a law changed which invalidates a record.
"""
@spec get_metadata(String.t()) :: {:ok, Zoneinfo.Meta.t()} | {:error, atom()}
defdelegate get_metadata(time_zone), to: Zoneinfo.Cache, as: :meta
defp contains_tzif?(path) do
case File.open(path, [:read], &contains_tzif_helper/1) do
{:ok, result} -> result
_error -> false
end
end
defp contains_tzif_helper(io) do
with buff when is_binary(buff) and byte_size(buff) == 8 <- IO.binread(io, 8),
{:ok, _version} <- Zoneinfo.TZif.version(buff) do
true
else
_anything -> false
end
end
end