TzWorld
Resolve timezones from a location using data from the timezone-boundary-builder project.
Upgrading from 1.x {: .warning}
The on-disk data format changed in 2.x:
priv/timezones-geodata.tzw1replacespriv/timezones-geodata.etf.zip. After upgrading you must runmix tz_world.updateonce to reinstall the data in the new format. Until you do, every lookup returns{:error, :enoent}. The old.etf.zipand.detsfiles inpriv/are no longer read and can be deleted to reclaim disk space (≈ 900 MB).
Installation
Add tz_world to your list of dependencies in mix.exs:
def deps do
[
{:tz_world, "~> 2.5"}
]
end
After adding TzWorld as a dependency, run mix deps.get to install it. Then run mix tz_world.update to install the timezone data.
NOTE No data is installed with the package and until the data is installed
with mix tz_world.update all calls to TzWorld.timezone_at/1 will return
{:error, :enoent}.
Configuration
There is no mandatory configuration required however four options may be configured in config.exs:
config :tz_world,
# Configure a custom TzWorld backend. It will be used
# as the default backend in calls to `TzWorld.timezone_at/1`
default_backend: MyTzWorldBackend,
# The default is the `priv` directory of `:tz_world`
data_dir: "geodata/directory",
# The default is either the trust store included in the
# libraries `CAStore` or `certifi` or the platform
# trust store.
cacertfile: "path/to/ca_trust_store",
# The default is the HTTPS_PROXY or https_proxy
# environment variable, if either is set.
https_proxy: "https://proxy.example.com:8080"
Download timeouts can also be set with environment variables; see TzWorld.Downloader.
Backend selection
TzWorld provides alternative strategies for managing access to the backend data. Each backend is implemented as a GenServer that needs to be either manually started with BackendModule.start_link/1 or preferably added to your application's supervision tree.
The recommended backend is TzWorld.Backend.SpatialIndex. It is also the default — applications that do not pin :default_backend will pick it up automatically.
For example:
defmodule MyApp.Application do
@moduledoc false
use Application
def start(_type, _args) do
children = [
...
TzWorld.Backend.SpatialIndex
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
The following backends are available:
-
TzWorld.Backend.SpatialIndex(recommended, default) resolves a point by querying an R-tree spatial index built once at startup and held in:persistent_term, then testing only the shape edges near the point, so a lookup takes microseconds. Lookups read directly from:persistent_termand bypass the GenServer mailbox entirely. This is the fastest backend on every benchmarked workload — and dramatically faster (≈18×) than any other backend on no-match queries (e.g. ocean points), where the previous bounding-box-scan algorithms had to walk every shape. -
TzWorld.Backend.EtsWithIndexCache(deprecated, removed in the next release) keeps the timezone shapes in a compressed:etstable, with an in-memory cache of every bounding box for candidate filtering. It is populated from the:detscache at startup, so it carries that file's cost on top of its own — useTzWorld.Backend.SpatialIndexinstead. -
TzWorld.Backend.DetsWithIndexCache(deprecated, removed in the next release) keeps the shapes on disk in a:detsfile, with the same in-memory bounding-box cache. UseTzWorld.Backend.SpatialIndexinstead, which reads the.tzw1data directly and needs no separate cache — one that is an order of magnitude larger than the data itself.
Data for the deprecated backends is only built when mix tz_world.update is
run with --backends dets (or ets); the default, spatial_index, does not
build the :dets cache.
Other backends can be implemented as long as they follow the TzWorld.Backend
behaviour. Custom backends should be configured in config.exs or runtime.exs
under the :default_backend key so that they will be considered as the default
for calls to TzWorld.timezone_at/1. For example:
config :tz_world,
default_backend: MyTzWorldBackend
Installing the Timezones Geo JSON data
Installing tz_world from source or from hex does not include the timezones
Geo JSON data. The data is required and to install or update it run:
mix tz_world.update
This task will download, transform, zip and store the timezones Geo data. Depending on internet and computer speed this may take a few minutes.
By default mix tz_world.update will download geojson data that does not include time zone information for the oceans. The following optional flags configure its behaviour:
-
--include-oceans(-o) will download the geojson data, including data for the oceans. This gives almost complete global coverage of time zone data. The default is--no-include-oceanswhich does not include data that covers the oceans. The geojson data including the oceans is about 10% larger than the data that does not include the oceans. -
--force(-f) does two things:- Forces an update even if the currently installed data is already at the latest release. Useful when switching between including and excluding ocean coverage.
- Creates the directory configured under
:data_dirif it does not yet exist. Without--forcea missing:data_dirraisesFile.Errorso that a misconfigured:data_diris loud rather than silently materialised. Pass--forceon the first install when you have set a custom:data_diroutside the build artifacts.
-
--trace(-t) emits debug-level progress logs (current memory usage, download / extract / parse phases).
Updating the Timezone data
From time-to-time the timezones Geo JSON data is updated in the upstream project. The mix task mix tz_world.update will update the data if it is available.
A running application can also be instructed to reload the data by executing TzWorld.reload_timezone_data.
Usage
The primary API is TzWorld.timezone_at. It takes either a Geo.Point struct or a longitude and latitude in degrees. Note the parameter order: longitude, latitude. It also takes an optional second parameter, backend, which must be one of the configured and running backend modules. By default timezone_at/2 will detect a running backend and will raise an exception if no running backend is found. A point that is not one of these forms, or is out of range, returns {:error, :invalid_point}.
iex> TzWorld.timezone_at(%Geo.Point{coordinates: {3.2, 45.32}})
{:ok, "Europe/Paris"}
iex> TzWorld.timezone_at({3.2, 45.32})
{:ok, "Europe/Paris"}
iex> TzWorld.timezone_at(%Geo.PointZ{coordinates: {-74.006, 40.7128, 0.0}})
{:ok, "America/New_York"}
# Assumes that the downloaded data does not include
# data for the oceans (the default)
iex> TzWorld.timezone_at(%Geo.Point{coordinates: {1.3, 65.62}})
{:error, :time_zone_not_found}
iex> TzWorld.timezone_at({200.0, 45.32})
{:error, :invalid_point}
Performance
Version 2.5 lookups take about 10 µs, some 1,500× faster than 2.4, and the loaded data takes 152 MB rather than 530 MB, with identical results. Version 2.0 lookups were 1.4×–18× faster than 1.x (R-tree spatial index), and mix tz_world.update peak memory is ≈ 13× lower (end-to-end streaming pipeline). See the Performance guide for measurements and methodology.