Time zone support for Elixir.
The Elixir standard library does not ship with a time zone database. As a result, the functions in the DateTime
module can, by default, only operate on datetimes in the UTC time zone. Alternatively (and
deliberately), the standard library relies on
third-party libraries, such as tz, to bring in time zone support and deal with datetimes in other time zones than UTC.
The tz library relies on the time zone database maintained by
IANA. As of version 0.28.4, tz uses version tzdata2026d of the IANA time zone database.
Add tz for Elixir as a dependency in your mix.exs file:
def deps do
[
{:tz, "~> 0.28"}
]
end
To use the tz database, either configure it via configuration:
config :elixir, :time_zone_database, Tz.TimeZoneDatabase
or by calling Calendar.put_time_zone_database/1:
Calendar.put_time_zone_database(Tz.TimeZoneDatabase)
or by passing the module name Tz.TimeZoneDatabase directly to the functions that need a time zone database:
DateTime.now("America/Sao_Paulo", Tz.TimeZoneDatabase)
Refer to the DateTime API for more details about handling datetimes with time zones.
The tz library is tested against nearly 10 million past dates, which includes most of all possible edge cases.
The repo tzdb_test compares the output of the different available libraries (tz, time_zone_info, tzdata and zoneinfo), and gives some idea of the difference in performance.
Time zone periods are deducted from the IANA time zone data. A period is a period of time where a certain offset is observed. For example, in Belgium from 31 March 2019 until 27 October 2019, clock went forward by 1 hour; as Belgium has a base offset from UTC of 1 hour, this means that during this period, Belgium observed a total offset of 2 hours from UTC time (base UTC offset of 1 hour + DST offset of 1 hour).
The time zone periods are computed and made available in Elixir maps during compilation time, to be consumed by the DateTime module.
tz can watch for IANA time zone database updates and automatically recompile the time zone periods.
To enable automatic updates, add Tz.UpdatePeriodically as a child in your supervisor:
{Tz.UpdatePeriodically, []}
You may pass the option :interval_in_days in order to configure the frequency of the updates:
{Tz.UpdatePeriodically, [interval_in_days: 5]}
If you do not wish to update automatically, but still wish to be alerted for new upcoming IANA updates, add
Tz.WatchPeriodically as a child in your supervisor:
{Tz.WatchPeriodically, []}
Tz.WatchPeriodically simply logs to your server when a new time zone database is available.
You may pass the options:
:interval_in_days: frequency of the checks:on_update: a user callback executed when an update is availableFor updating IANA data manually, there are 2 options:
just update the tz library in the mix.exs file, which hopefully includes the latest IANA time zone database (if not, wait for the library maintainer to include the latest version or send a pull request on GitHub).
download the files and recompile:
:data_dir option. For example:
config :tz, :data_dir, Path.join(Path.dirname(__DIR__), "priv")
mix tz.download
mix deps.compile tz --force
Or from an iex session to recompile at runtime:
iex -S mix
iex> Tz.Compiler.compile()
Note that recompilation at runtime is not persistent, run mix deps.compile tz --force in addition.iex> Tz.iana_version()
To force a specific IANA version:
:data_dir option. For example:
config :tz, :data_dir, Path.join(Path.dirname(__DIR__), "priv")
mix tz.download 2021a
:iana_version option:
config :tz, :iana_version, "2021a"
mix deps.compile tz --force
iex> Tz.iana_version()
Some users prefer to use Tz.WatchPeriodically (over Tz.UpdatePeriodically) to watch and update manually. Example cases:
https://data.iana.org in this case) before processing.tz to ensure IANA versions match.To avoid the updater to run while executing tests, you may conditionally add the child worker in your supervisor. For example:
children = [
MyApp.Repo,
MyApp.Endpoint,
#...
]
|> append_if(Application.get_env(:my_app, :env) != :test, {Tz.UpdatePeriodically, []})
defp append_if(list, condition, item) do
if condition, do: list ++ [item], else: list
end
In config.exs, add config :my_app, env: Mix.env().
Lastly, add the http client mint and ssl certificate store castore into your mix.exs file:
defp deps do
[
{:castore, "~> 1.0"},
{:mint, "~> 1.6"},
{:tz, "~> 0.28"}
]
end
You may also add custom options for the http client mint:
config :tz, Tz.HTTP.Mint.HTTPClient,
proxy: {:http, proxy_host, proxy_port, []}
You may implement the Tz.HTTP.HTTPClient behaviour in order to use another HTTP client.
Example using Finch:
defmodule MyApp.Tz.HTTPClient do
@behaviour Tz.HTTP.HTTPClient
alias Tz.HTTP.HTTPResponse
alias MyApp.MyFinch
@impl Tz.HTTP.HTTPClient
def request(hostname, path) do
{:ok, response} =
Finch.build(:get, "https://" <> Path.join(hostname, path))
|> Finch.request(MyFinch)
%HTTPResponse{
status_code: response.status,
body: response.body
}
end
end
A Tz.HTTP.HTTPResponse struct must be returned with fields :status_code and :body.
The custom module must then be passed into the config:
config :tz, :http_client, MyApp.Tz.HTTPClient
tz accepts two environment options to tweak performance.
For time zones that have ongoing DST changes, period lookups for dates far in the future result in periods being dynamically computed based on the IANA data. For example, what is the period for 20 March 2040 for New York (let's assume that the last rules for New York still mention an ongoing DST change as you read this)? We can't compile periods indefinitely in the future; by default, such periods are computed until 5 years from compilation time. Dynamic period computations is a slow operation.
You can decrease period lookup time for time zones affected by DST changes, by specifying until what year those periods have to be computed:
config :tz, build_dst_periods_until_year: 20 + NaiveDateTime.utc_now().year
Note that increasing the year will also slightly increase compilation time, as it generates more periods to compile.
The default setting computes periods for a period of 5 years from the time the code is compiled. Note that if you have added the automatic updater, the periods will be recomputed with every update, which occurs multiple times throughout the year.
You can slightly decrease memory usage and compilation time, by rejecting time zone periods before a given year:
config :tz, reject_periods_before_year: 2010
Note that this option is aimed towards embedded devices as the difference should be insignificant for ordinary servers.
By default, no periods are rejected.
By default, the files are stored in the priv directory of the tz library. You may customize the directory that will hold all of the IANA timezone data. For example, if you want to store the files in your project's priv dir instead:
config :tz, :data_dir, Path.join(Path.dirname(__DIR__), "priv")
Tz.iana_version() == "2026d"
Tz's API is intentionally kept as minimal as possible to implement Calendar.TimeZoneDatabase's behaviour. Utility functions around time zones are provided by TzExtra.
TzExtra.countries_time_zones/1: returns a list of time zone data by countryTzExtra.time_zone_identifiers/1: returns a list of time zone identifiersTzExtra.civil_time_zone_identifiers/1: returns a list of time zone identifiers that are tied to a countryTzExtra.countries/0: returns a list of ISO country codes with their English nameTzExtra.get_canonical_time_zone_identifier/1: returns the canonical time zone identifier for the given time zone identifierTzExtra.Changeset.validate_time_zone_identifier/3: an Ecto Changeset validator, validating that the user input is a valid time zoneTzExtra.Changeset.validate_civil_time_zone_identifier/3: an Ecto Changeset validator, validating that the user input is a valid civil time zoneTzExtra.Changeset.validate_iso_country_code/3: an Ecto Changeset validator, validating that the user input is a valid ISO country codeRecommended for embedded devices.
The current state of Tz wouldn't have been possible to achieve without the work of the following contributors related to time zones:
java.time package, against which Tz is testing its output.Elixir
100.0%
Time zone support for Elixir.
The Elixir standard library does not ship with a time zone database. As a result, the functions in the DateTime
module can, by default, only operate on datetimes in the UTC time zone. Alternatively (and
deliberately), the standard library relies on
third-party libraries, such as tz, to bring in time zone support and deal with datetimes in other time zones than UTC.
The tz library relies on the time zone database maintained by
IANA. As of version 0.28.4, tz uses version tzdata2026d of the IANA time zone database.
Add tz for Elixir as a dependency in your mix.exs file:
def deps do
[
{:tz, "~> 0.28"}
]
end
To use the tz database, either configure it via configuration:
config :elixir, :time_zone_database, Tz.TimeZoneDatabase
or by calling Calendar.put_time_zone_database/1:
Calendar.put_time_zone_database(Tz.TimeZoneDatabase)
or by passing the module name Tz.TimeZoneDatabase directly to the functions that need a time zone database:
DateTime.now("America/Sao_Paulo", Tz.TimeZoneDatabase)
Refer to the DateTime API for more details about handling datetimes with time zones.
The tz library is tested against nearly 10 million past dates, which includes most of all possible edge cases.
The repo tzdb_test compares the output of the different available libraries (tz, time_zone_info, tzdata and zoneinfo), and gives some idea of the difference in performance.
Time zone periods are deducted from the IANA time zone data. A period is a period of time where a certain offset is observed. For example, in Belgium from 31 March 2019 until 27 October 2019, clock went forward by 1 hour; as Belgium has a base offset from UTC of 1 hour, this means that during this period, Belgium observed a total offset of 2 hours from UTC time (base UTC offset of 1 hour + DST offset of 1 hour).
The time zone periods are computed and made available in Elixir maps during compilation time, to be consumed by the DateTime module.
tz can watch for IANA time zone database updates and automatically recompile the time zone periods.
To enable automatic updates, add Tz.UpdatePeriodically as a child in your supervisor:
{Tz.UpdatePeriodically, []}
You may pass the option :interval_in_days in order to configure the frequency of the updates:
{Tz.UpdatePeriodically, [interval_in_days: 5]}
If you do not wish to update automatically, but still wish to be alerted for new upcoming IANA updates, add
Tz.WatchPeriodically as a child in your supervisor:
{Tz.WatchPeriodically, []}
Tz.WatchPeriodically simply logs to your server when a new time zone database is available.
You may pass the options:
:interval_in_days: frequency of the checks:on_update: a user callback executed when an update is availableFor updating IANA data manually, there are 2 options:
just update the tz library in the mix.exs file, which hopefully includes the latest IANA time zone database (if not, wait for the library maintainer to include the latest version or send a pull request on GitHub).
download the files and recompile:
:data_dir option. For example:
config :tz, :data_dir, Path.join(Path.dirname(__DIR__), "priv")
mix tz.download
mix deps.compile tz --force
Or from an iex session to recompile at runtime:
iex -S mix
iex> Tz.Compiler.compile()
Note that recompilation at runtime is not persistent, run mix deps.compile tz --force in addition.iex> Tz.iana_version()
To force a specific IANA version:
:data_dir option. For example:
config :tz, :data_dir, Path.join(Path.dirname(__DIR__), "priv")
mix tz.download 2021a
:iana_version option:
config :tz, :iana_version, "2021a"
mix deps.compile tz --force
iex> Tz.iana_version()
Some users prefer to use Tz.WatchPeriodically (over Tz.UpdatePeriodically) to watch and update manually. Example cases:
https://data.iana.org in this case) before processing.tz to ensure IANA versions match.To avoid the updater to run while executing tests, you may conditionally add the child worker in your supervisor. For example:
children = [
MyApp.Repo,
MyApp.Endpoint,
#...
]
|> append_if(Application.get_env(:my_app, :env) != :test, {Tz.UpdatePeriodically, []})
defp append_if(list, condition, item) do
if condition, do: list ++ [item], else: list
end
In config.exs, add config :my_app, env: Mix.env().
Lastly, add the http client mint and ssl certificate store castore into your mix.exs file:
defp deps do
[
{:castore, "~> 1.0"},
{:mint, "~> 1.6"},
{:tz, "~> 0.28"}
]
end
You may also add custom options for the http client mint:
config :tz, Tz.HTTP.Mint.HTTPClient,
proxy: {:http, proxy_host, proxy_port, []}
You may implement the Tz.HTTP.HTTPClient behaviour in order to use another HTTP client.
Example using Finch:
defmodule MyApp.Tz.HTTPClient do
@behaviour Tz.HTTP.HTTPClient
alias Tz.HTTP.HTTPResponse
alias MyApp.MyFinch
@impl Tz.HTTP.HTTPClient
def request(hostname, path) do
{:ok, response} =
Finch.build(:get, "https://" <> Path.join(hostname, path))
|> Finch.request(MyFinch)
%HTTPResponse{
status_code: response.status,
body: response.body
}
end
end
A Tz.HTTP.HTTPResponse struct must be returned with fields :status_code and :body.
The custom module must then be passed into the config:
config :tz, :http_client, MyApp.Tz.HTTPClient
tz accepts two environment options to tweak performance.
For time zones that have ongoing DST changes, period lookups for dates far in the future result in periods being dynamically computed based on the IANA data. For example, what is the period for 20 March 2040 for New York (let's assume that the last rules for New York still mention an ongoing DST change as you read this)? We can't compile periods indefinitely in the future; by default, such periods are computed until 5 years from compilation time. Dynamic period computations is a slow operation.
You can decrease period lookup time for time zones affected by DST changes, by specifying until what year those periods have to be computed:
config :tz, build_dst_periods_until_year: 20 + NaiveDateTime.utc_now().year
Note that increasing the year will also slightly increase compilation time, as it generates more periods to compile.
The default setting computes periods for a period of 5 years from the time the code is compiled. Note that if you have added the automatic updater, the periods will be recomputed with every update, which occurs multiple times throughout the year.
You can slightly decrease memory usage and compilation time, by rejecting time zone periods before a given year:
config :tz, reject_periods_before_year: 2010
Note that this option is aimed towards embedded devices as the difference should be insignificant for ordinary servers.
By default, no periods are rejected.
By default, the files are stored in the priv directory of the tz library. You may customize the directory that will hold all of the IANA timezone data. For example, if you want to store the files in your project's priv dir instead:
config :tz, :data_dir, Path.join(Path.dirname(__DIR__), "priv")
Tz.iana_version() == "2026d"
Tz's API is intentionally kept as minimal as possible to implement Calendar.TimeZoneDatabase's behaviour. Utility functions around time zones are provided by TzExtra.
TzExtra.countries_time_zones/1: returns a list of time zone data by countryTzExtra.time_zone_identifiers/1: returns a list of time zone identifiersTzExtra.civil_time_zone_identifiers/1: returns a list of time zone identifiers that are tied to a countryTzExtra.countries/0: returns a list of ISO country codes with their English nameTzExtra.get_canonical_time_zone_identifier/1: returns the canonical time zone identifier for the given time zone identifierTzExtra.Changeset.validate_time_zone_identifier/3: an Ecto Changeset validator, validating that the user input is a valid time zoneTzExtra.Changeset.validate_civil_time_zone_identifier/3: an Ecto Changeset validator, validating that the user input is a valid civil time zoneTzExtra.Changeset.validate_iso_country_code/3: an Ecto Changeset validator, validating that the user input is a valid ISO country codeRecommended for embedded devices.
The current state of Tz wouldn't have been possible to achieve without the work of the following contributors related to time zones:
java.time package, against which Tz is testing its output.Elixir
100.0%