- Released documentation: docs.rs/tzf-rs
- Latest documentation(not released yet): ringsaturn.github.io/tzf-rs
- Try it online: tzf-web
Note
Version 2 is protobuf-free. The data source is the TZF embedded binary
format (.tzb) shipped by
tzf-dist, and the public surface
is two finder types: DefaultFinder and EmbeddedFinder. See
Migrating from v1.
cargo add tzf-rsuse tzf_rs::DefaultFinder;
fn main() {
let finder = DefaultFinder::new();
// Please note coords are lng-lat.
println!("{:?}", finder.get_tz_name(116.3883, 39.9289));
println!("{:?}", finder.get_tz_names(116.3883, 39.9289));
}By default the tzf CLI binary is built as well. If you don't want/need it,
you can omit the default features and build like this:
cargo build --no-default-features --features bundledDefaultFinder— the recommended general-purpose finder. Expands the file's geometry into materialized polygons at load time.get_tz_nameanswers most queries from the preindex tiles in ~100 ns and falls back to exact point-in-polygon (YStripes-accelerated ray casting) for boundary cases;get_tz_namesis always polygon-exact.EmbeddedFinder— the low-memory finder. Queries the.tzbbytes in place, without expanding geometry: total footprint is roughly the ~4 MB file plus a small open-time index (chunk skip blocks and per-group latitude stripes, ~100 KB on lite). Boundary queries take a few microseconds instead of hundreds of nanoseconds; results are identical toDefaultFinder.
Both also load caller-supplied bytes: DefaultFinder::from_tzb /
EmbeddedFinder::from_tzb (EmbeddedFinder takes &'static [u8] or an owned
Vec<u8>). Both expose get_tz_name, get_tz_names, timezonenames and
data_version (the upstream release the file was built from, e.g. 2026c),
and — with export-geojson — to_geojson, get_tz_geojson,
to_preindex_geojson and get_tz_preindex_geojson.
| Feature | Default | Effect |
|---|---|---|
bundled |
yes | Embeds the lite .tzb (~4 MB) from the crates.io tzf-dist package. |
clap |
yes | Builds the tzf CLI binary. |
export-geojson |
no | GeoJSON export methods (pulls in serde / serde_json). |
full |
no | Full-precision .tzb (~14 MB) plus DefaultFinder::new_full(). The data is git-only; see Setup 100% Accurate Lookup. Mutually exclusive with bundled. |
Minimum supported Rust version: 1.88 (edition 2024 plus let-chains).
There is no wasm-specific feature. wasm32-unknown-unknown builds with
--no-default-features --features bundled (drop clap). Load-time polygon
assembly is distributed over std::thread::available_parallelism(), which
returns an error on wasm; the loader then takes the single-threaded path, so no
threads are spawned there.
It's expensive to init tzf-rs's DefaultFinder/EmbeddedFinder, so please
consider reusing instances or creating one as a global variable:
use std::sync::LazyLock;
use tzf_rs::DefaultFinder;
static FINDER: LazyLock<DefaultFinder> = LazyLock::new(DefaultFinder::new);
fn main() {
// Please note coords are lng-lat.
println!("{:?}", FINDER.get_tz_name(116.3883, 39.9289));
println!("{:?}", FINDER.get_tz_names(116.3883, 39.9289));
}For reuse,
racemap/rust-tz-service provides
a good example.
A Redis protocol demo could be used here:
ringsaturn/redizone.
By default, tzf-rs uses simplified shape data. The error around borders is small and bounded: every simplified boundary stays within ~111 m of the full-precision border. See Accuracy for measured numbers. If you need 100% accurate lookup, use the full-precision dataset (~14 MB).
full.tzb exceeds the crates.io package size limit, so the tzf-dist package
published to the registry ships only lite.tzb. The full feature therefore
needs the tzf-dist git source. There are two ways to get it.
A. Take tzf-rs itself from git (the v1 recipe, still supported):
tzf-rs = { git = "https://github.com/ringsaturn/tzf-rs", rev = "v{X}.{Y}.{Z}", features = ["full"], default-features = false }B. Keep tzf-rs from crates.io and patch the data crate: add a
[patch.crates-io] section to the workspace root manifest.
[dependencies]
tzf-rs = { version = "2", features = ["full"], default-features = false }
[patch.crates-io]
tzf-dist = { git = "https://github.com/ringsaturn/tzf-dist", tag = "v0.0.2026-c-tzb1" }Either way default-features = false is required: full and the default
bundled feature are mutually exclusive, and enabling both is a
compile_error!. Without one of these two recipes, features = ["full"] fails
to compile, because the registry tzf-dist package contains no full dataset.
use tzf_rs::DefaultFinder;
fn main() {
let finder = DefaultFinder::new_full();
println!("{}", finder.timezonenames().len());
let tz_name = finder.get_tz_name(139.767125, 35.681236);
println!("tz_name: {}", tz_name);
}This setup requires more time and memory to build the DefaultFinder.
Note
This feature is designed for data visualization purposes and I can't guarantee the performance when using it in high-performance scenarios. Please do proper performance tests and necessary optimizations before using it in high performace production, for example caching the exported GeoJSON data or push to CDN.
It's a common use case make some visualization of timezone boundaries. For this purpose, tzf-rs provides methods to export specific timezone polygons as GeoJSON format.
To enable this feature, you need to build tzf-rs with export-geojson feature:
tzf-rs = { version = "{version}", features = ["export-geojson"]}Then you can use the following methods:
// examples/query_tokyo.rs
use tzf_rs::DefaultFinder;
fn main() {
let default_finder = DefaultFinder::new();
let lng = 139.6917;
let lat = 35.6895;
let tz_name = default_finder.get_tz_name(lng, lat).to_owned();
println!(
"The timezone at longitude {}, latitude {} is: {}",
lng, lat, tz_name
);
// Get the polygon boundary for the timezone.
if let Some(boundary_file) = default_finder.get_tz_geojson(&tz_name) {
// It's a GeoJSON FeatureCollection whose features contain
// "MultiPolygon" geometry for the timezone.
println!("Found GeoJSON feature for timezone: {}", tz_name);
let mut polygons: usize = 0;
for feature in boundary_file.features {
polygons += feature.geometry.coordinates.len();
}
println!(
"Total number of polygons in feature collection: {}",
polygons
);
}
}cargo run --example query_tokyo --features export-geojsonEmbeddedFinder exports the same GeoJSON, decoding only the requested
timezone's rings from the file on demand.
For now, tzf-rs' binding in Wasm, named tzf-wasm, has exported this feature and it has been deployed to the tzf-web for online usage.
v1 loaded protobuf artifacts (CompressedTopoTimezones, PreindexTimezones);
those artifacts are no longer published, and v2 removes every protobuf-typed
API. Mappings:
| v1 | v2 |
|---|---|
DefaultFinder::new() |
unchanged |
DefaultFinder::new_full() |
unchanged (full feature, git-only data) |
DefaultFinder::{get_tz_name,get_tz_names,timezonenames,data_version} |
unchanged |
Finder (polygon-only) |
DefaultFinder (get_tz_names stays polygon-exact) |
FuzzyFinder (tile-only) |
removed — the preindex is the fast path inside every finder |
Finder::from_compressed_topo(pb) / from_pb(pb) |
DefaultFinder::from_tzb(&[u8]) |
FuzzyFinder::from_pb(pb) |
removed — no separate tile-only finder |
FinderOptions / *_with_options |
removed — YStripes is always on |
tzf_rs::pbgen module |
removed — no protobuf types in the public API |
tzf_rs::revert_timezones(&pb) |
removed — took a protobuf type |
finder.finder.get_tz_geojson(...) |
finder.get_tz_geojson(...) |
FuzzyFinder::to_geojson() |
DefaultFinder::to_preindex_geojson() -> Option<BoundaryFile> |
FuzzyFinder::get_tz_geojson(name) -> Option<FeatureItem> |
DefaultFinder::get_tz_preindex_geojson(name) -> Option<BoundaryFile> |
feature bundled (pb lite data) |
feature bundled (lite .tzb) |
feature full (pb full data, git-only) |
feature full (full .tzb, still git-only) |
features clap, export-geojson |
unchanged |
| — | new: EmbeddedFinder, tzf_rs::Error |
Behavior changes:
get_tz_namesresults are now sorted lexicographically.get_tz_nameonDefaultFinderanswers from the preindex tile when one covers the point (v1DefaultFindersemantics; v1Finderusers who need polygon-exact multi-results useget_tz_names).- The byte constructors return
Result<_, tzf_rs::Error>: files are CRC-checked and structurally validated at open, instead of silently yielding an empty finder. - New:
EmbeddedFinder, an in-place low-memory mechanism (~4 MB total). - MSRV is now 1.88.
The Douglas-Peucker simplification uses an epsilon of 0.001 degrees, which
caps boundary displacement at roughly 111 m by construction. Measured against
the full-precision 2026c dataset with tzf's internal/cmd/borderchange
(spherical model, certified via Lipschitz interval subdivision):
| Metric | Result |
|---|---|
| Certified maximum boundary displacement | 111.7 m (+1.0 m tolerance) |
| Boundary length displaced more than 100 m | 0.41% |
| Boundary length displaced more than 500 m | 0% |
| Total mis-assigned area | 16,962 km² (~0.003% of Earth) |
| Mis-assigned area within 100 m of the true border | 92.8% |
See BORDER_CHANGE.md
in the tzf repository for the complete evaluation results.
Only queries that land within ~111 m of a timezone border can differ from the
full-precision result, and most of that band is far narrower. If your use case
is sensitive inside that band, enable the full feature and use
DefaultFinder::new_full().
The tzf-rs package is intended for high-performance geospatial query services,
such as weather forecasting APIs. Most queries can be returned within a very
short time, averaging around 150-300 nanoseconds with DefaultFinder.
Here is what has been done to improve performance:
- Using the simplified dataset by default.
- Using pre-indexing (the
.tzbFUZZY section) to handle most queries in about 100 nanoseconds. - Using a finely-tuned Ray Casting algorithm package
ringsaturn/geometry-rsto verify whether a polygon contains a point.
That's all. There are no black magic tricks inside the tzf-rs.
Benchmark numbers (Apple M3 Max, bundled lite dataset 2026c, make bench):
| Target | Scenario | Median estimate |
|---|---|---|
| DefaultFinder | get_tz_name, 154,248-city sweep |
25.1 ms total (~163 ns/query) |
| EmbeddedFinder | get_tz_name, 154,248-city sweep |
147.0 ms total (~950 ns/query) |
| DefaultFinder | get_tz_name, edge city (preindex miss) |
434 ns |
| EmbeddedFinder | get_tz_name, edge city (preindex miss) |
4.03 µs |
| DefaultFinder | get_tz_names, edge city |
531 ns |
| EmbeddedFinder | get_tz_names, edge city |
5.71 µs |
| DefaultFinder | open (new()) |
13.0 ms |
| EmbeddedFinder | open (new()) |
2.05 ms |
The whole-dataset sweep is the number to compare against: the per-query
*_random_city benches draw a single random city and reuse it for the whole
measurement, so their absolute value varies with the draw.
tzf-rs consumes the .tzb profile only. The .tzm memory image the Go
runtime uses exists for zero-copy ring aliasing, which geometry-rs's owned
polygon storage cannot exploit — measured here it saved ~3 ms of open time
while costing more memory, so it is not supported.
Peak RSS (macOS, make memory): ~44 MiB for DefaultFinder (v1 needed
~82 MiB), ~6 MiB for EmbeddedFinder.
You can view more details from latest benchmark from GitHub Actions logs.
I have written an article about the history of tzf, its Rust port, and its
Rust port's Python binding; you can view it
here.
- Original Go repo:
ringsaturn/tzf - Binary timezone data (
.tzb/.tzm):ringsaturn/tzf-dist - Geometry: use
ringsaturn/geometry-rswhich istidwall/geometry's Rust port. - Continuous Benchmark compared with other packages:
ringsaturn/tz-benchmark
See Project tzf for more information.
- Ruby, see
HarlemSquirrel/tzf-rb - Python, see
ringsaturn/tzfpy - Wasm, see
ringsaturn/tzf-wasm - PostgreSQL extension, see
ringsaturn/pg-tzf
The binary helps in debugging tzf-rs and using it in (scripting) languages without bindings. Either specify the coordinates as parameters to get a single time zone, or to look up multiple coordinates efficiently specify the ordering and pipe them to the binary one pair of coordinates per line.
tzf --lng 116.3883 --lat 39.9289
echo -e "116.3883 39.9289\n116.3883, 39.9289" | tzf --stdin-order lng-latIf you are using Nixpkgs, you can install the tzf command line tool, please
see more in
Nixpkgs.
This project is licensed under the MIT license. The data is
licensed under the
ODbL license,
same as
evansiroky/timezone-boundary-builder
