├── fuzz ├── .gitignore ├── bin │ └── libfuzzer.rs ├── run.sh └── Cargo.toml ├── tzdb ├── LICENSE.md ├── README.md ├── CHANGELOG.md ├── src │ ├── changelog.rs │ ├── lib.rs │ └── now.rs └── Cargo.toml ├── clippy.toml ├── make-tzdb ├── LICENSE.md ├── README.md ├── Cargo.toml ├── src │ ├── parse.rs │ └── main.rs └── generate_lookup_table.py ├── .gitattributes ├── .gitmodules ├── Cargo.toml ├── deny.toml ├── tzdb.tar.lz.sha ├── _typos.toml ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── examples └── current-time │ ├── Cargo.toml │ └── src │ └── main.rs ├── rustfmt.toml ├── testing ├── Cargo.toml └── tests │ ├── by_name.rs │ └── proptest.rs ├── tzdb_data ├── Cargo.toml ├── LICENSE.md ├── README.md └── src │ └── lib.rs ├── SECURITY.md ├── benchmarks ├── Cargo.toml └── benches │ └── by-name.rs ├── Makefile ├── README.md ├── CHANGELOG.md └── LICENSE.md /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | /corpus 2 | -------------------------------------------------------------------------------- /tzdb/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /tzdb/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.81.0" 2 | -------------------------------------------------------------------------------- /make-tzdb/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /tzdb/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /tzdb/src/changelog.rs: -------------------------------------------------------------------------------- 1 | #![doc(cfg(any()))] 2 | #![doc = include_str!("../CHANGELOG.md")] 3 | -------------------------------------------------------------------------------- /make-tzdb/README.md: -------------------------------------------------------------------------------- 1 | Tool to generate `tzdb/src/generated.rs`. Execute `make` in the project root. 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/generated"] 2 | path = tzdb_data/src/generated 3 | url = ./ 4 | branch = generated 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "examples/current-time", 4 | "tzdb", 5 | "tzdb_data", 6 | ] 7 | resolver = "2" 8 | -------------------------------------------------------------------------------- /fuzz/bin/libfuzzer.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | libfuzzer_sys::fuzz_target!(|name: &[u8]| { 4 | let _ = tzdb_data::find_tz(name); 5 | }); 6 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | version = 2 3 | allow = ["Apache-2.0", "MIT", "MIT-0", "Unicode-3.0"] 4 | private = { ignore = true } 5 | confidence-threshold = 1.0 6 | -------------------------------------------------------------------------------- /tzdb.tar.lz.sha: -------------------------------------------------------------------------------- 1 | 6fa233d6a884acd24b52c44b2eecf683f9403f44b39dcbe85b1ec92fc11f4daf6ee5143f05332faf9258b8bd8f7ca7d4882e19b1e3d892b4761be43fd4f39d51 tmp/tzdb-2025c.tar.lz 2 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | locale = "en-us" 3 | 4 | [files] 5 | extend-exclude = [ 6 | # hidden files 7 | ".*", 8 | # generated files 9 | "tzdb_data/src/generated/*", 10 | ] 11 | 12 | [default.extend-words] 13 | # a suffix, as in "2nd" 14 | nd = "nd" 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /fuzz/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -exuo pipefail 3 | cd "${0%/*}" 4 | export RUSTC_BOOTSTRAP=1 5 | nice \ 6 | cargo fuzz run tzdb-fuzz-libfuzzer --features="libfuzzer-sys" \ 7 | --debug-assertions --release --jobs 16 -- \ 8 | -max_total_time=180 -max_len=40 -timeout=5ms 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | 3 | target/ 4 | run.cgi 5 | Cargo.lock 6 | 7 | *.pyc 8 | *.swp* 9 | *.nfs* 10 | *~ 11 | *.~* 12 | *.tmp 13 | *.old 14 | *.bak 15 | *.orig 16 | *.pid 17 | *.so 18 | *.so.* 19 | *.cpp 20 | *.py[co] 21 | *.egg-info 22 | *.whl 23 | 24 | .* 25 | !.git* 26 | !.readthedocs.yaml 27 | -------------------------------------------------------------------------------- /examples/current-time/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "current-time" 3 | version = "0.0.0" 4 | edition = "2021" 5 | authors = ["René Kijewski "] 6 | repository = "https://github.com/Kijewski/tzdb" 7 | license = "MIT OR Apache-2.0" 8 | publish = false 9 | 10 | [dependencies] 11 | tzdb = { path = "../../tzdb", version = "0.7.2" } 12 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | combine_control_expr = false 2 | edition = "2018" 3 | style_edition = "2024" 4 | format_macro_matchers = true 5 | match_block_trailing_comma = true 6 | imports_granularity = "Module" 7 | newline_style = "Unix" 8 | normalize_doc_attributes = true 9 | overflow_delimited_expr = true 10 | reorder_impl_items = true 11 | group_imports = "StdExternalCrate" 12 | unstable_features = true 13 | use_field_init_shorthand = true 14 | use_try_shorthand = true 15 | -------------------------------------------------------------------------------- /testing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tzdb-testing" 3 | version = "0.0.0" 4 | edition = "2021" 5 | authors = ["René Kijewski "] 6 | repository = "https://github.com/Kijewski/tzdb" 7 | description = "… benchmarking …" 8 | license = "Apache-2.0" 9 | rust-version = "1.81.0" 10 | publish = false 11 | 12 | [dependencies] 13 | tzdb = { path = "../tzdb" } 14 | 15 | proptest = "1.5.0" 16 | structmeta = "0.3.0" 17 | test-strategy = "0.4.0" 18 | tz-rs = { version = "0.7.0", default-features = false, features = ["std"] } 19 | 20 | [workspace] 21 | members = ["."] 22 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tzdb-fuzz" 3 | version = "0.0.0" 4 | authors = ["René Kijewski "] 5 | repository = "https://github.com/Kijewski/tzdb" 6 | description = "… fuzz …" 7 | license = "Apache-2.0" 8 | publish = false 9 | edition = "2021" 10 | 11 | [package.metadata] 12 | cargo-fuzz = true 13 | 14 | [dependencies] 15 | tzdb_data = { path = "../tzdb_data" } 16 | 17 | libfuzzer-sys = "0.4" 18 | 19 | [[bin]] 20 | name = "tzdb-fuzz-libfuzzer" 21 | path = "bin/libfuzzer.rs" 22 | test = false 23 | doc = false 24 | 25 | [workspace] 26 | members = ["."] 27 | -------------------------------------------------------------------------------- /make-tzdb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "make-tzdb" 3 | version = "0.0.0" 4 | edition = "2021" 5 | authors = ["René Kijewski "] 6 | repository = "https://github.com/Kijewski/tzdb" 7 | description = "Tool to generate `tzdb/src/generated.rs`" 8 | license = "MIT OR Apache-2.0" 9 | publish = false 10 | 11 | [dependencies] 12 | anyhow = "1.0.98" 13 | convert_case = "0.8.0" 14 | indexmap = "2.9.0" 15 | itertools = "0.14.0" 16 | ron = "0.10.1" 17 | serde = { version = "1.0.219", features = ["derive"] } 18 | subprocess = "0.2.9" 19 | tz-rs = "0.7.0" 20 | walkdir = "2.5.0" 21 | 22 | [workspace] 23 | members = ["."] 24 | -------------------------------------------------------------------------------- /tzdb_data/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tzdb_data" 3 | version = "0.2.3" 4 | edition = "2021" 5 | authors = ["René Kijewski "] 6 | repository = "https://github.com/Kijewski/tzdb" 7 | description = "Static, #![no_std] time zone information for tz-rs" 8 | license = "MIT-0 OR MIT OR Apache-2.0" 9 | keywords = ["timezone", "no_std"] 10 | categories = ["date-and-time", "no-std"] 11 | readme = "README.md" 12 | rust-version = "1.81.0" 13 | 14 | [dependencies] 15 | tz-rs = { version = "0.7.0", default-features = false } 16 | 17 | [package.metadata.docs.rs] 18 | all-features = true 19 | rustdoc-args = ["--generate-link-to-definition", "--cfg=docsrs"] 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | :-----: | :----------------: | 7 | | 0.7.x | :white_check_mark: | 8 | | 0.6.x | :white_check_mark: | 9 | | 0.5.x | :white_check_mark: | 10 | | 0.4.x | :white_check_mark: | 11 | | 0.3.x | :white_check_mark: | 12 | | 0.2.x | :white_check_mark: | 13 | | 0.1.x | :white_check_mark: | 14 | | 0.0.x | :x: | 15 | 16 | ## Reporting a Vulnerability 17 | 18 | If you find a vulnerability, and you think that it would be better not to open a public issue just yet, then please write an e-mail to crates.io@k6i.\[Germany\]. 19 | 20 | I check my e-mail every workday Mon to Fri. You can expect an answer in very few days. More severe problems will get reported to . 21 | -------------------------------------------------------------------------------- /benchmarks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tzdb-bench" 3 | version = "0.0.0" 4 | edition = "2021" 5 | authors = ["René Kijewski "] 6 | repository = "https://github.com/Kijewski/tzdb" 7 | description = "… benchmarking …" 8 | license = "Apache-2.0" 9 | rust-version = "1.81.0" 10 | publish = false 11 | 12 | [dependencies] 13 | chrono-tz = { version = "0.10.0", features = ["case-insensitive"] } 14 | tzdb_data = { path = "../tzdb_data" } 15 | 16 | [dev-dependencies] 17 | criterion = { version = "0.5.1", default-features = false, features = ["html_reports"] } 18 | minstant = "0.1.7" 19 | rand = { version = "0.8.5", default-features = false, features = ["std"] } 20 | rand_xoshiro = "0.6.0" 21 | test-strategy = "0.4.0" 22 | structmeta = "0.3.0" 23 | 24 | [[bench]] 25 | name = "by-name" 26 | harness = false 27 | 28 | [workspace] 29 | members = ["."] 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DELETE_ON_ERROR: 2 | 3 | TZDB_VERSION := tzdb-2025c 4 | 5 | tzdb_data/src/generated/mod.rs: tmp/${TZDB_VERSION}/usr/share/zoneinfo/ tzdb.tar.lz.sha 6 | cd make-tzdb && cargo r -- ../$(@D) ../$< ../tzdb.tar.lz.sha 7 | cargo +nightly fmt -- \ 8 | $(@D)/by_name.rs \ 9 | $(@D)/mod.rs \ 10 | $(@D)/raw_tzdata.rs \ 11 | $(@D)/test_all_names.rs \ 12 | $(@D)/time_zone.rs \ 13 | $(@D)/tzdata.rs \ 14 | $(@D)/tz_names.rs 15 | 16 | tmp/${TZDB_VERSION}/usr/share/zoneinfo/: tmp/${TZDB_VERSION}/ 17 | cd tmp/${TZDB_VERSION}/ && make PACKRATDATA=backzone PACKRATLIST=zone.tab TOPDIR="." install 18 | 19 | tmp/${TZDB_VERSION}/: tmp/${TZDB_VERSION}.tar.lz 20 | cd tmp/ && tar xf $( 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /tzdb_data/README.md: -------------------------------------------------------------------------------- 1 | # tzdb_data — Time Zone Database 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/Kijewski/tzdb/ci.yml?branch=v0.7.x&style=flat-square&logo=github&logoColor=white "GitHub Workflow Status")](https://github.com/Kijewski/tzdb/actions/workflows/ci.yml) 4 | [![Crates.io](https://img.shields.io/crates/v/tzdb_data?logo=rust&style=flat-square "Crates.io")](https://crates.io/crates/tzdb_data) 5 | ![Minimum supported Rust version](https://img.shields.io/badge/rustc-1.81+-important?logo=rust&style=flat-square "Minimum Supported Rust Version: 1.81") 6 | [![License: MIT-0](https://img.shields.io/badge/license-MIT--0-informational?logo=apache&style=flat-square)](https://github.com/Kijewski/tzdb/blob/v0.6.1/tzdb_data/LICENSE.md "License: MIT-0") 7 | 8 | Static, `#![no_std]` time zone information for tz-rs 9 | 10 | ## Usage examples 11 | 12 | ```rust 13 | // access by identifier 14 | let time_zone = tzdb_data::time_zone::europe::KYIV; 15 | // access by name 16 | let time_zone = tzdb_data::find_tz(b"Europe/Berlin").unwrap(); 17 | // names are case insensitive 18 | let time_zone = tzdb_data::find_tz(b"ArCtIc/LoNgYeArByEn").unwrap(); 19 | ``` 20 | -------------------------------------------------------------------------------- /tzdb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tzdb" 3 | version = "0.7.3" 4 | edition = "2021" 5 | authors = ["René Kijewski "] 6 | repository = "https://github.com/Kijewski/tzdb" 7 | description = "Static time zone information for tz-rs" 8 | license = "Apache-2.0" 9 | keywords = ["date", "time", "timezone", "zone", "calendar"] 10 | categories = ["date-and-time"] 11 | readme = "README.md" 12 | rust-version = "1.81.0" 13 | 14 | [dependencies] 15 | tzdb_data = { version = "0.2.0", default-features = false, path = "../tzdb_data" } 16 | 17 | iana-time-zone = { version = "0.1.60", default-features = false, features = ["fallback"], optional = true } 18 | tz-rs = { version = "0.7.0", default-features = false } 19 | 20 | [features] 21 | default = ["local", "now", "std"] 22 | # Enables querying the current time in a given time zone: 23 | now = ["std"] 24 | # Enable functions to query the current system time: 25 | local = ["std", "dep:iana-time-zone"] 26 | # Enable the use of features in the `std` crate: 27 | std = ["alloc", "tz-rs/std"] 28 | # Enable the use of features in the `alloc` crate: 29 | alloc = [] 30 | 31 | [package.metadata.docs.rs] 32 | all-features = true 33 | rustdoc-args = ["--generate-link-to-definition", "--cfg=docsrs"] 34 | -------------------------------------------------------------------------------- /testing/tests/by_name.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | use tzdb::{raw_tz_by_name, time_zone, tz_by_name}; 4 | 5 | #[test] 6 | fn test_by_name() { 7 | let _: tz::TimeZoneRef<'static> = tz_by_name("Europe/Berlin").unwrap(); 8 | let _: tz::TimeZoneRef<'static> = tz_by_name("America/Dominica").unwrap(); 9 | } 10 | 11 | #[test] 12 | fn test_by_absent_name() { 13 | assert_eq!(tz_by_name("Berlin/Steglitz-Zehlendorf"), None); 14 | } 15 | 16 | #[test] 17 | fn test_name_empty() { 18 | assert_eq!(tz_by_name(""), None); 19 | } 20 | 21 | #[test] 22 | fn test_name_too_long() { 23 | assert_eq!( 24 | tz_by_name( 25 | "Pacific/Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu" 26 | ), 27 | None, 28 | ); 29 | } 30 | 31 | #[test] 32 | fn test_static() { 33 | assert_eq!( 34 | time_zone::pacific::NAURU, 35 | tz_by_name("Pacific/Nauru").unwrap() 36 | ); 37 | } 38 | 39 | #[test] 40 | fn test_raw_static() { 41 | assert_eq!( 42 | time_zone::pacific::RAW_NAURU, 43 | raw_tz_by_name("Pacific/Nauru").unwrap() 44 | ); 45 | } 46 | 47 | #[test] 48 | fn test_issue_49() { 49 | assert_eq!( 50 | time_zone::asia::HO_CHI_MINH, 51 | tz_by_name("Asia/Ho_Chi_Minh").unwrap() 52 | ); 53 | assert_eq!( 54 | time_zone::asia::HO_CHI_MINH, 55 | tz_by_name("asia/ho_chi_minh").unwrap() 56 | ); 57 | assert_eq!( 58 | time_zone::asia::HO_CHI_MINH, 59 | tz_by_name("ASIA/HO_CHI_MINH").unwrap() 60 | ); 61 | assert_eq!( 62 | time_zone::asia::HO_CHI_MINH, 63 | tz_by_name("aSIA/hO_cHI_mINH").unwrap() 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /testing/tests/proptest.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(test, not(miri)))] 2 | 3 | use proptest::collection::{vec, SizeRange}; 4 | use proptest::prelude::*; 5 | use test_strategy::proptest; 6 | use tz::TimeZoneRef; 7 | use tzdb::{raw_tz_by_name, tz_by_name}; 8 | 9 | fn ascii_string(size: impl Into) -> impl Strategy { 10 | vec(proptest::char::range('\0', '\x7f'), size).prop_map(|v| v.into_iter().collect()) 11 | } 12 | 13 | fn random_string(size: impl Into) -> impl Strategy { 14 | vec(any::(), size).prop_map(|v| v.into_iter().collect()) 15 | } 16 | 17 | fn random_bytes(size: impl Into) -> impl Strategy> { 18 | vec(any::(), size) 19 | } 20 | 21 | #[proptest] 22 | fn test_short_ascii_string(#[strategy(ascii_string(0..8))] s: String) { 23 | let _: Option> = tz_by_name(&s); 24 | let _: Option<&[u8]> = raw_tz_by_name(&s); 25 | } 26 | 27 | #[proptest] 28 | fn test_ascii_string(#[strategy(ascii_string(8..40))] s: String) { 29 | let _: Option> = tz_by_name(&s); 30 | let _: Option<&[u8]> = raw_tz_by_name(&s); 31 | } 32 | 33 | #[proptest] 34 | fn test_short_string(#[strategy(random_string(0..8))] s: String) { 35 | let _: Option> = tz_by_name(&s); 36 | let _: Option<&[u8]> = raw_tz_by_name(&s); 37 | } 38 | 39 | #[proptest] 40 | fn test_string(#[strategy(random_string(8..40))] s: String) { 41 | let _: Option> = tz_by_name(&s); 42 | let _: Option<&[u8]> = raw_tz_by_name(&s); 43 | } 44 | 45 | #[proptest] 46 | fn test_short_bytes(#[strategy(random_bytes(0..8))] s: Vec) { 47 | let _: Option> = tz_by_name(&s); 48 | let _: Option<&[u8]> = raw_tz_by_name(&s); 49 | } 50 | 51 | #[proptest] 52 | fn test_bytes(#[strategy(random_bytes(8..40))] s: Vec) { 53 | let _: Option> = tz_by_name(&s); 54 | let _: Option<&[u8]> = raw_tz_by_name(&s); 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tzdb — Time Zone Database 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/Kijewski/tzdb/ci.yml?branch=v0.7.x&style=flat-square&logo=github&logoColor=white "GitHub Workflow Status")](https://github.com/Kijewski/tzdb/actions/workflows/ci.yml) 4 | [![Crates.io](https://img.shields.io/crates/v/tzdb?logo=rust&style=flat-square "Crates.io")](https://crates.io/crates/tzdb) 5 | [![docs.rs](https://img.shields.io/docsrs/tzdb?logo=docsdotrs&style=flat-square&logoColor=white "docs.rs")](https://docs.rs/tzdb/) 6 | ![Minimum supported Rust version](https://img.shields.io/badge/rustc-1.81+-important?logo=rust&style=flat-square "Minimum Supported Rust Version: 1.81") 7 | [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-informational?logo=apache&style=flat-square)](https://github.com/Kijewski/tzdb/blob/v0.6.1/LICENSE.md "License: Apache-2.0") 8 | 9 | Static time zone information for [tz-rs](https://crates.io/crates/tz-rs). 10 | 11 | This crate provides all time zones found in the [Time Zone Database](https://www.iana.org/time-zones). 12 | 13 | ## Usage examples 14 | 15 | ```rust 16 | let time_zone = tzdb::local_tz()?; // tz::TimeZoneRef<'_> 17 | let current_time = tzdb::now::local()?; // tz::DateTime 18 | 19 | // access by identifier 20 | let time_zone = tzdb::time_zone::europe::KYIV; 21 | let current_time = tzdb::now::in_tz(tzdb::time_zone::europe::KYIV)?; 22 | 23 | // access by name 24 | let time_zone = tzdb::tz_by_name("Europe/Berlin")?; 25 | let current_time = tzdb::now::in_named("Europe/Berlin")?; 26 | 27 | // names are case insensitive 28 | let time_zone = tzdb::tz_by_name("ArCtIc/LongYeArByEn")?; 29 | let current_time = tzdb::now::in_named("ArCtIc/LoNgYeArByEn")?; 30 | 31 | // provide a default time zone 32 | let current_time = tzdb::now::local_or(tzdb::time_zone::GMT)?; 33 | let current_time = tzdb::now::in_named_or(tzdb::time_zone::GMT, "Some/City")?; 34 | ``` 35 | 36 | ## Feature flags 37 | 38 | * `local` (enabled by default) — enable functions to query the current system time 39 | * `now` (enabled by default) — enable functions to query the current system time 40 | * `std` (enabled by default, `now` and `local`) — enable the use of features in the `std` crate 41 | * `alloc` (enabled by `std`) — enable the use of features in the `alloc` crate 42 | -------------------------------------------------------------------------------- /examples/current-time/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::pedantic)] 2 | 3 | use std::env::args; 4 | use std::process::exit; 5 | 6 | use tzdb::{TZ_NAMES, local_tz, now, time_zone, tz_by_name}; 7 | 8 | pub fn main() -> Result<(), now::NowError> { 9 | let mut args = args().fuse(); 10 | let exe = args.next(); 11 | let exe = exe.as_deref().unwrap_or("current-time"); 12 | let argument = args.next(); 13 | 14 | if matches!(argument.as_deref(), Some("-l" | "--list")) { 15 | let mut line = String::with_capacity(80); 16 | for tz_name in TZ_NAMES { 17 | if line.len() + 2 + tz_name.len() >= 80 { 18 | println!("{line},"); 19 | line.clear(); 20 | } 21 | if !line.is_empty() { 22 | line.push_str(", "); 23 | } 24 | line.push_str(tz_name); 25 | } 26 | if !line.is_empty() { 27 | println!("{line}"); 28 | } 29 | return Ok(()); 30 | } 31 | 32 | let timezone = if let Some(argument) = argument { 33 | match tz_by_name(&argument) { 34 | Some(timezone) => timezone, 35 | None => { 36 | eprintln!("No such time zone found in database: {argument:?}"); 37 | eprintln!("To see a list of all known time zones run: {exe} --list"); 38 | exit(1); 39 | }, 40 | } 41 | } else { 42 | eprintln!("No time zone selected, defaulting to the system time zone."); 43 | eprintln!("To see a list of all known time zones run: {exe} --list"); 44 | eprintln!(); 45 | local_tz().unwrap_or(time_zone::UTC) 46 | }; 47 | 48 | let dt = now::in_tz(timezone)?; 49 | let dow = match DOW.get(dt.week_day() as usize) { 50 | Some(dow) => *dow, 51 | None => unreachable!("Impossible week_day: {}", dt.week_day()), 52 | }; 53 | let month = match MONTH.get(dt.month() as usize) { 54 | Some(month) => *month, 55 | None => unreachable!("Impossible month: {}", dt.month()), 56 | }; 57 | println!( 58 | "In the time zone {}:", 59 | dt.local_time_type().time_zone_designation(), 60 | ); 61 | println!( 62 | "Today is {}, {} the {}{}.", 63 | dow, 64 | month, 65 | dt.month_day(), 66 | suffix(dt.month_day() as _) 67 | ); 68 | println!( 69 | "This is the {}{} day of the year {}.", 70 | 1 + dt.year_day(), 71 | suffix((1 + dt.year_day()) as _), 72 | dt.year(), 73 | ); 74 | println!( 75 | "Now it is {:02}:{:02}:{:02}.", 76 | dt.hour(), 77 | dt.minute(), 78 | dt.second(), 79 | ); 80 | println!("{dt}"); 81 | 82 | Ok(()) 83 | } 84 | 85 | fn suffix(index: usize) -> &'static str { 86 | match (index % 100, index % 10) { 87 | (10..=19, _) => "th", 88 | (_, 1) => "st", 89 | (_, 2) => "nd", 90 | (_, 3) => "rd", 91 | _ => "th", 92 | } 93 | } 94 | 95 | const DOW: [&str; 7] = [ 96 | "Sunday", 97 | "Monday", 98 | "Tuesday", 99 | "Wednesday", 100 | "Thursday", 101 | "Friday", 102 | "Saturday", 103 | ]; 104 | 105 | const MONTH: [&str; 13] = [ 106 | "", 107 | "January", 108 | "February", 109 | "March", 110 | "April", 111 | "May", 112 | "June", 113 | "July", 114 | "August", 115 | "September", 116 | "October", 117 | "November", 118 | "December", 119 | ]; 120 | -------------------------------------------------------------------------------- /benchmarks/benches/by-name.rs: -------------------------------------------------------------------------------- 1 | use std::iter::FusedIterator; 2 | use std::str::FromStr; 3 | use std::time::Duration; 4 | 5 | use criterion::black_box; 6 | use minstant::Instant; 7 | use rand::seq::{IteratorRandom, SliceRandom}; 8 | use rand::SeedableRng; 9 | use rand_xoshiro::Xoroshiro128PlusPlus; 10 | use tzdb_data::{find_raw, TZ_NAMES}; 11 | 12 | fn main() { 13 | #[cfg(not(miri))] 14 | { 15 | criterion::criterion_group!(benches, benchmark_by_name); 16 | benches(); 17 | 18 | criterion::Criterion::default() 19 | .configure_from_args() 20 | .with_plots() 21 | .final_summary(); 22 | } 23 | } 24 | 25 | fn benchmark_by_name(c: &mut criterion::Criterion) { 26 | let names = generate_names(); 27 | let names: Vec<&str> = names.iter().map(String::as_str).collect(); 28 | 29 | macro_rules! bench_functions { 30 | ($($ident:ident($name:pat) $body:block)*) => { $({ 31 | fn $ident(limit: u64, names: &[&str]) -> Duration { 32 | let start = Instant::now(); 33 | for $name in names.iter().cycle().take64(limit) { 34 | let _ = black_box($body); 35 | } 36 | start.elapsed() 37 | } 38 | 39 | c.bench_function( 40 | stringify!($ident), 41 | |b| b.iter_custom(|iters| $ident(iters, black_box(&names))), 42 | ); 43 | })* }; 44 | } 45 | 46 | bench_functions! { 47 | tzdb_find_raw(name) { 48 | crate::find_raw(name.as_bytes()) 49 | } 50 | chrono_tz_from_str(name) { 51 | chrono_tz::Tz::from_str(name) 52 | } 53 | chrono_tz_from_str_insensitive(name) { 54 | chrono_tz::Tz::from_str_insensitive(name) 55 | } 56 | } 57 | } 58 | 59 | fn generate_names() -> Vec { 60 | let rng = &mut Xoroshiro128PlusPlus::seed_from_u64(2024_02_01); 61 | 62 | let mut names: Vec<_> = TZ_NAMES.iter().map(|s| String::from(*s)).collect(); 63 | 64 | // insert 10% unknown names 65 | for _ in 0..names.len().div_ceil(10) { 66 | let mut continent = *b"abcdefghijklmnopqrstuvwxyz"; 67 | let mut city = *b"abcdefghijklmnopqrstuvwxyz"; 68 | 69 | continent.shuffle(rng); 70 | city.shuffle(rng); 71 | 72 | let continent = &mut continent[..(4..=8).choose(rng).unwrap()]; 73 | let city = &mut city[..(4..=12).choose(rng).unwrap()]; 74 | 75 | continent[0] = continent[0].to_ascii_uppercase(); 76 | city[0] = city[0].to_ascii_uppercase(); 77 | 78 | let continent = std::str::from_utf8(continent).unwrap(); 79 | let city = std::str::from_utf8(city).unwrap(); 80 | 81 | names.push(format!("{}/{}", continent, city)); 82 | } 83 | 84 | // collect all names with "random" capitalization 85 | let mut names: Vec<_> = names 86 | .into_iter() 87 | .flat_map(|name| { 88 | let upper = name.to_uppercase(); 89 | let lower = name.to_lowercase(); 90 | let inverted = name 91 | .chars() 92 | .map(|c| match c { 93 | 'A'..='Z' => c.to_ascii_lowercase(), 94 | 'a'..='z' => c.to_ascii_uppercase(), 95 | c => c, 96 | }) 97 | .collect(); 98 | let spongebob1 = name 99 | .chars() 100 | .enumerate() 101 | .map(|(i, c)| { 102 | if i % 2 == 0 { 103 | c.to_ascii_uppercase() 104 | } else { 105 | c.to_ascii_lowercase() 106 | } 107 | }) 108 | .collect(); 109 | let spongebob2 = name 110 | .chars() 111 | .enumerate() 112 | .map(|(i, c)| { 113 | if i % 2 == 1 { 114 | c.to_ascii_uppercase() 115 | } else { 116 | c.to_ascii_lowercase() 117 | } 118 | }) 119 | .collect(); 120 | [name, upper, lower, inverted, spongebob1, spongebob2] 121 | }) 122 | .collect(); 123 | 124 | // randomize order 125 | names.shuffle(rng); 126 | names 127 | } 128 | 129 | #[derive(Debug, Clone, Copy)] 130 | struct Take64 { 131 | iter: I, 132 | limit: u64, 133 | } 134 | 135 | impl Iterator for Take64 { 136 | type Item = I::Item; 137 | 138 | fn next(&mut self) -> Option { 139 | self.limit = self.limit.checked_sub(1)?; 140 | self.iter.next() 141 | } 142 | } 143 | 144 | impl FusedIterator for Take64 {} 145 | 146 | trait Take64Ext: Sized { 147 | fn take64(self, limit: u64) -> Take64 { 148 | Take64 { iter: self, limit } 149 | } 150 | } 151 | 152 | impl Take64Ext for I {} 153 | -------------------------------------------------------------------------------- /tzdb/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // 3 | // Copyright 2022-2024 René Kijewski 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | #![cfg_attr(docsrs, feature(doc_cfg))] 18 | #![allow(unknown_lints)] 19 | #![forbid(unsafe_code)] 20 | #![warn(absolute_paths_not_starting_with_crate)] 21 | #![warn(elided_lifetimes_in_paths)] 22 | #![warn(explicit_outlives_requirements)] 23 | #![warn(meta_variable_misuse)] 24 | #![warn(missing_copy_implementations)] 25 | #![warn(missing_debug_implementations)] 26 | #![warn(missing_docs)] 27 | #![warn(non_ascii_idents)] 28 | #![warn(noop_method_call)] 29 | #![warn(single_use_lifetimes)] 30 | #![warn(trivial_casts)] 31 | #![warn(unreachable_pub)] 32 | #![warn(unused_extern_crates)] 33 | #![warn(unused_lifetimes)] 34 | #![warn(unused_results)] 35 | #![no_std] 36 | 37 | //! # `tzdb` — Time Zone Database 38 | //! 39 | //! [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/Kijewski/tzdb/ci.yml?branch=v0.7.x&style=flat-square&logo=github&logoColor=white "GitHub Workflow Status")](https://github.com/Kijewski/tzdb/actions/workflows/ci.yml) 40 | //! [![Crates.io](https://img.shields.io/crates/v/tzdb?logo=rust&style=flat-square "Crates.io")](https://crates.io/crates/tzdb) 41 | //! [![docs.rs](https://img.shields.io/docsrs/tzdb?logo=docsdotrs&style=flat-square&logoColor=white "docs.rs")](https://docs.rs/tzdb/) 42 | //! ![Minimum supported Rust version](https://img.shields.io/badge/rustc-1.81+-important?logo=rust&style=flat-square "Minimum Supported Rust Version: 1.81") 43 | //! [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-informational?logo=apache&style=flat-square)](https://github.com/Kijewski/tzdb/blob/v0.6.1/LICENSE.md "License: Apache-2.0") 44 | //! 45 | //! Static time zone information for [tz-rs](https://crates.io/crates/tz-rs). 46 | //! 47 | //! This crate provides all time zones found in the [Time Zone Database](https://www.iana.org/time-zones). 48 | //! 49 | //! ## Usage examples 50 | //! 51 | //! ```rust 52 | //! # #[cfg(feature = "local")] { 53 | //! // get the system time zone 54 | //! let time_zone = tzdb::local_tz().unwrap(); // tz::TimeZoneRef<'_> 55 | //! let current_time = tzdb::now::local().unwrap(); // tz::DateTime 56 | //! # } 57 | //! 58 | //! // access by identifier 59 | //! let time_zone = tzdb::time_zone::europe::KYIV; 60 | //! # #[cfg(feature = "now")] { 61 | //! let current_time = tzdb::now::in_tz(tzdb::time_zone::europe::KYIV).unwrap(); 62 | //! # } 63 | //! 64 | //! // access by name 65 | //! let time_zone = tzdb::tz_by_name("Europe/Berlin").unwrap(); 66 | //! # #[cfg(feature = "now")] { 67 | //! let current_time = tzdb::now::in_named("Europe/Berlin").unwrap(); 68 | //! # } 69 | //! 70 | //! // names are case insensitive 71 | //! let time_zone = tzdb::tz_by_name("ArCtIc/LoNgYeArByEn").unwrap(); 72 | //! # #[cfg(feature = "now")] { 73 | //! let current_time = tzdb::now::in_named("ArCtIc/LoNgYeArByEn").unwrap(); 74 | //! # } 75 | //! 76 | //! // provide a default time zone 77 | //! # #[cfg(feature = "now")] { 78 | //! let current_time = tzdb::now::local_or(tzdb::time_zone::GMT).unwrap(); 79 | //! let current_time = tzdb::now::in_named_or(tzdb::time_zone::GMT, "Some/City").unwrap(); 80 | //! # } 81 | //! ``` 82 | //! 83 | //! ## Feature flags 84 | //! 85 | //! * `local` (enabled by default) — enable functions to query the current system time 86 | //! * `now` (enabled by default) — enable functions to query the current system time 87 | //! * `std` (enabled by default, `now` and `local`) — enable the use of features in the [`std`] crate 88 | //! * `alloc` (enabled by `std`) — enable the use of features in the [`alloc`] crate 89 | 90 | #[cfg(docsrs)] 91 | extern crate alloc; 92 | #[cfg(docsrs)] 93 | extern crate std; 94 | 95 | #[cfg(docsrs)] 96 | pub mod changelog; 97 | #[cfg(feature = "now")] 98 | pub mod now; 99 | 100 | #[doc(no_inline)] 101 | pub use tzdb_data::{TZ_NAMES, VERSION, VERSION_HASH, time_zone}; 102 | 103 | /// Find a time zone by name, e.g. `"Europe/Berlin"` (case-insensitive) 104 | /// 105 | /// # Example 106 | /// 107 | /// ``` 108 | /// assert_eq!( 109 | /// tzdb::time_zone::europe::BERLIN, 110 | /// tzdb::tz_by_name("Europe/Berlin").unwrap(), 111 | /// ); 112 | /// ``` 113 | #[inline] 114 | pub fn tz_by_name>(s: S) -> Option> { 115 | Some(*tzdb_data::find_tz(s.as_ref())?) 116 | } 117 | 118 | /// Find the raw, unparsed time zone data by name, e.g. `"Europe/Berlin"` (case-insensitive) 119 | /// 120 | /// # Example 121 | /// 122 | /// ``` 123 | /// assert_eq!( 124 | /// tzdb::time_zone::europe::RAW_BERLIN, 125 | /// tzdb::raw_tz_by_name("Europe/Berlin").unwrap(), 126 | /// ); 127 | /// ``` 128 | #[inline] 129 | pub fn raw_tz_by_name>(s: S) -> Option<&'static [u8]> { 130 | tzdb_data::find_raw(s.as_ref()) 131 | } 132 | 133 | /// Find the time zone of the current system 134 | /// 135 | /// This function uses [`iana_time_zone::get_timezone()`] in the background. 136 | /// You may want to cache the output to avoid repeated filesystem accesses by `get_timezone()`. 137 | /// 138 | /// # Example 139 | /// 140 | /// ```rust 141 | /// // Query the time zone of the local system: 142 | /// let time_zone = tzdb::local_tz().unwrap(); 143 | /// ``` 144 | /// 145 | /// Most likely you will want to fallback to a default time zone, 146 | /// if the system time zone could not be determined or was not found in the database: 147 | /// 148 | /// ```rust 149 | /// // Query the time zone of the local system: 150 | /// let time_zone = tzdb::local_tz().unwrap_or(tzdb::time_zone::GMT); 151 | /// ``` 152 | #[must_use] 153 | #[cfg(feature = "local")] 154 | #[cfg_attr(docsrs, doc(cfg(feature = "local")))] 155 | pub fn local_tz() -> Option> { 156 | tz_by_name(iana_time_zone::get_timezone().ok()?) 157 | } 158 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - v0.7.x 7 | pull_request: 8 | branches: 9 | - v0.7.x 10 | schedule: 11 | - cron: "58 7 * * 3" 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | fmt: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | submodules: recursive 25 | 26 | - name: Setup Rust 27 | uses: dtolnay/rust-toolchain@master 28 | with: 29 | toolchain: nightly 30 | components: rustfmt 31 | 32 | - run: cargo fmt --all -- --check 33 | 34 | lint: 35 | runs-on: ubuntu-latest 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | toolchain: 40 | - stable 41 | - nightly 42 | versions: 43 | - "" 44 | - "-Zminimal-versions" 45 | include: 46 | - toolchain: "1.81" 47 | versions: "-Zminimal-versions" 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v4 51 | with: 52 | submodules: recursive 53 | 54 | - name: Setup Rust 55 | uses: dtolnay/rust-toolchain@master 56 | with: 57 | toolchain: ${{ matrix.toolchain }} 58 | components: clippy 59 | 60 | - name: Update lockfile 61 | env: 62 | RUSTC_BOOTSTRAP: 1 63 | run: cargo generate-lockfile ${{ matrix.versions }} 64 | 65 | - run: cargo check --workspace --all-targets 66 | - run: cargo clippy --workspace --all-targets -- -D warnings 67 | 68 | clippy-pedantic: 69 | runs-on: ubuntu-latest 70 | strategy: 71 | matrix: 72 | package: 73 | - tzdb 74 | - tzdb_data 75 | steps: 76 | - name: Checkout 77 | uses: actions/checkout@v4 78 | with: 79 | submodules: recursive 80 | 81 | - name: Setup Rust 82 | uses: dtolnay/rust-toolchain@master 83 | with: 84 | toolchain: stable 85 | components: clippy 86 | 87 | - run: cargo clippy --package ${{ matrix.package }} --all-targets -- -D clippy::pedantic 88 | 89 | test: 90 | runs-on: ubuntu-latest 91 | steps: 92 | - name: Checkout 93 | uses: actions/checkout@v4 94 | with: 95 | submodules: recursive 96 | 97 | - name: Setup Rust 98 | uses: dtolnay/rust-toolchain@master 99 | with: 100 | toolchain: stable 101 | components: clippy 102 | 103 | - run: cargo check --workspace --all-targets 104 | - run: cargo clippy --workspace --all-targets -- -D warnings 105 | - run: cargo test --workspace --all-targets 106 | - run: cargo run --package current-time 107 | - run: cd testing && cargo test 108 | 109 | miri: 110 | name: "Miri" 111 | runs-on: ubuntu-latest 112 | steps: 113 | - name: Checkout 114 | uses: actions/checkout@v4 115 | with: 116 | submodules: recursive 117 | 118 | - name: Setup Rust 119 | uses: dtolnay/rust-toolchain@master 120 | with: 121 | toolchain: nightly 122 | components: miri 123 | 124 | - name: Test (tzdb) 125 | run: cargo miri test --workspace --all-targets 126 | env: 127 | MIRIFLAGS: "-Zmiri-disable-isolation" 128 | 129 | - name: Test (testing) 130 | run: cd testing && cargo miri test --workspace --all-targets 131 | env: 132 | MIRIFLAGS: "-Zmiri-disable-isolation" 133 | 134 | cross-miri: 135 | runs-on: ubuntu-latest 136 | strategy: 137 | fail-fast: false 138 | matrix: 139 | target: 140 | - aarch64-unknown-linux-gnu 141 | - i586-unknown-linux-gnu 142 | - powerpc-unknown-linux-gnu 143 | - powerpc64-unknown-linux-gnu 144 | - powerpc64le-unknown-linux-gnu 145 | steps: 146 | - name: Checkout 147 | uses: actions/checkout@v4 148 | with: 149 | submodules: recursive 150 | 151 | - name: Setup Rust 152 | uses: dtolnay/rust-toolchain@master 153 | with: 154 | toolchain: nightly 155 | components: miri 156 | target: ${{ matrix.target }} 157 | 158 | - name: Test 159 | run: cargo miri test --workspace --target ${{ matrix.target }} 160 | env: 161 | MIRIFLAGS: "-Zmiri-disable-isolation" 162 | 163 | doc: 164 | runs-on: ubuntu-latest 165 | steps: 166 | - name: Checkout 167 | uses: actions/checkout@v4 168 | with: 169 | submodules: recursive 170 | 171 | - name: Setup Rust 172 | uses: dtolnay/rust-toolchain@master 173 | with: 174 | toolchain: nightly 175 | 176 | - run: cargo doc --workspace --all-features --no-deps 177 | env: 178 | RUSTDOCFLAGS: -Z unstable-options --generate-link-to-definition --cfg=docsrs -D warnings 179 | 180 | audit: 181 | runs-on: ubuntu-latest 182 | steps: 183 | - name: Checkout 184 | uses: actions/checkout@v4 185 | with: 186 | submodules: recursive 187 | 188 | - name: Audit 189 | uses: EmbarkStudios/cargo-deny-action@v2 190 | 191 | devskim: 192 | name: DevSkim 193 | runs-on: ubuntu-latest 194 | permissions: 195 | actions: read 196 | contents: read 197 | security-events: write 198 | steps: 199 | - name: Checkout code 200 | uses: actions/checkout@v4 201 | with: 202 | submodules: recursive 203 | 204 | - name: Run DevSkim scanner 205 | uses: microsoft/DevSkim-Action@v1 206 | 207 | - name: Upload DevSkim scan results to GitHub Security tab 208 | uses: github/codeql-action/upload-sarif@v3 209 | with: 210 | sarif_file: devskim-results.sarif 211 | 212 | typos: 213 | runs-on: ubuntu-latest 214 | steps: 215 | - uses: actions/checkout@v4 216 | - uses: crate-ci/typos@master 217 | 218 | feature-powerset: 219 | runs-on: ubuntu-latest 220 | steps: 221 | - name: Checkout code 222 | uses: actions/checkout@v4 223 | with: 224 | submodules: recursive 225 | 226 | - name: Setup Rust 227 | uses: dtolnay/rust-toolchain@master 228 | with: 229 | toolchain: nightly 230 | 231 | - name: Install cargo-hack 232 | uses: taiki-e/install-action@cargo-hack 233 | 234 | - name: Check feature-powerset 235 | run: cargo hack --workspace --feature-powerset check 236 | -------------------------------------------------------------------------------- /tzdb_data/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT-0 2 | // 3 | // Copyright 2022-2024 René Kijewski 4 | 5 | #![cfg_attr(docsrs, feature(doc_cfg))] 6 | #![allow(unknown_lints)] 7 | #![forbid(unsafe_code)] 8 | #![warn(absolute_paths_not_starting_with_crate)] 9 | #![warn(elided_lifetimes_in_paths)] 10 | #![warn(explicit_outlives_requirements)] 11 | #![warn(meta_variable_misuse)] 12 | #![warn(missing_copy_implementations)] 13 | #![warn(missing_debug_implementations)] 14 | #![warn(missing_docs)] 15 | #![warn(non_ascii_idents)] 16 | #![warn(noop_method_call)] 17 | #![warn(single_use_lifetimes)] 18 | #![warn(trivial_casts)] 19 | #![warn(unreachable_pub)] 20 | #![warn(unused_extern_crates)] 21 | #![warn(unused_lifetimes)] 22 | #![warn(unused_results)] 23 | #![allow(clippy::single_match_else)] 24 | #![allow(clippy::type_complexity)] 25 | #![no_std] 26 | 27 | //! # `tzdb_data` — Time Zone Database 28 | //! 29 | //! [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/Kijewski/tzdb/ci.yml?branch=v0.7.x&style=flat-square&logo=github&logoColor=white "GitHub Workflow Status")](https://github.com/Kijewski/tzdb/actions/workflows/ci.yml) 30 | //! [![Crates.io](https://img.shields.io/crates/v/tzdb_data?logo=rust&style=flat-square "Crates.io")](https://crates.io/crates/tzdb_data) 31 | //! [![docs.rs](https://img.shields.io/docsrs/tzdb_data?logo=docsdotrs&style=flat-square&logoColor=white "docs.rs")](https://docs.rs/tzdb_data/) 32 | //! ![Minimum supported Rust version](https://img.shields.io/badge/rustc-1.81+-important?logo=rust&style=flat-square "Minimum Supported Rust Version: 1.81") 33 | //! [![License: MIT-0](https://img.shields.io/badge/license-MIT--0-informational?logo=apache&style=flat-square)](https://github.com/Kijewski/tzdb/blob/v0.6.1/tzdb_data/LICENSE.md "License: MIT-0") 34 | //! 35 | //! Static, `#![no_std]` time zone information for tz-rs 36 | //! 37 | //! This crate provides all time zones found in the [Time Zone Database](https://www.iana.org/time-zones). 38 | //! 39 | //! ## Usage examples 40 | //! 41 | //! ```rust 42 | //! // access by identifier 43 | //! let time_zone = tzdb_data::time_zone::europe::KYIV; 44 | //! // access by name 45 | //! let time_zone = tzdb_data::find_tz(b"Europe/Berlin").unwrap(); 46 | //! // names are case insensitive 47 | //! let time_zone = tzdb_data::find_tz(b"ArCtIc/LoNgYeArByEn").unwrap(); 48 | //! ``` 49 | //! 50 | 51 | mod generated; 52 | 53 | #[doc(inline)] 54 | pub use crate::generated::{TZ_NAMES, VERSION, VERSION_HASH, time_zone}; 55 | 56 | /// Find a time zone by name, e.g. `b"Europe/Berlin"` (case-insensitive) 57 | /// 58 | /// # Example 59 | /// 60 | /// ``` 61 | /// assert_eq!( 62 | /// &tzdb_data::time_zone::europe::BERLIN, 63 | /// tzdb_data::find_tz(b"Europe/Berlin").unwrap(), 64 | /// ); 65 | /// ``` 66 | #[inline] 67 | #[must_use] 68 | pub const fn find_tz(s: &[u8]) -> Option<&'static tz::TimeZoneRef<'static>> { 69 | match generated::by_name::find_key(s) { 70 | Some(key) => Some(generated::by_name::TIME_ZONES[key as u16 as usize]), 71 | None => None, 72 | } 73 | } 74 | 75 | /// Find the raw, unparsed time zone data by name, e.g. `b"Europe/Berlin"` (case-insensitive) 76 | /// 77 | /// # Example 78 | /// 79 | /// ``` 80 | /// assert_eq!( 81 | /// tzdb_data::time_zone::europe::RAW_BERLIN, 82 | /// tzdb_data::find_raw(b"Europe/Berlin").unwrap(), 83 | /// ); 84 | /// ``` 85 | #[inline] 86 | #[must_use] 87 | pub const fn find_raw(s: &[u8]) -> Option<&'static [u8]> { 88 | match generated::by_name::find_key(s) { 89 | Some(key) => Some(generated::by_name::RAW_TIME_ZONES[key as u16 as usize]), 90 | None => None, 91 | } 92 | } 93 | 94 | #[allow(clippy::ref_option)] // we cannot change `TimeZoneRef::new`'s interface 95 | #[must_use] 96 | const fn new_time_zone_ref( 97 | transitions: &'static [tz::timezone::Transition], 98 | local_time_types: &'static [tz::LocalTimeType], 99 | leap_seconds: &'static [tz::timezone::LeapSecond], 100 | extra_rule: &'static Option, 101 | ) -> tz::timezone::TimeZoneRef<'static> { 102 | match tz::timezone::TimeZoneRef::new(transitions, local_time_types, leap_seconds, extra_rule) { 103 | Ok(value) => value, 104 | Err(_) => panic!(), 105 | } 106 | } 107 | 108 | #[must_use] 109 | const fn new_local_time_type( 110 | ut_offset: i32, 111 | is_dst: bool, 112 | time_zone_designation: Option<&[u8]>, 113 | ) -> tz::LocalTimeType { 114 | match tz::LocalTimeType::new(ut_offset, is_dst, time_zone_designation) { 115 | Ok(value) => value, 116 | Err(_) => panic!(), 117 | } 118 | } 119 | 120 | #[must_use] 121 | const fn new_transition( 122 | unix_leap_time: i64, 123 | local_time_type_index: usize, 124 | ) -> tz::timezone::Transition { 125 | tz::timezone::Transition::new(unix_leap_time, local_time_type_index) 126 | } 127 | 128 | #[must_use] 129 | const fn new_alternate_time( 130 | std: tz::LocalTimeType, 131 | dst: tz::LocalTimeType, 132 | dst_start: tz::timezone::RuleDay, 133 | dst_start_time: i32, 134 | dst_end: tz::timezone::RuleDay, 135 | dst_end_time: i32, 136 | ) -> tz::timezone::AlternateTime { 137 | match tz::timezone::AlternateTime::new( 138 | std, 139 | dst, 140 | dst_start, 141 | dst_start_time, 142 | dst_end, 143 | dst_end_time, 144 | ) { 145 | Ok(value) => value, 146 | Err(_) => panic!(), 147 | } 148 | } 149 | 150 | #[must_use] 151 | const fn new_month_week_day(month: u8, week: u8, week_day: u8) -> tz::timezone::MonthWeekDay { 152 | match tz::timezone::MonthWeekDay::new(month, week, week_day) { 153 | Ok(value) => value, 154 | Err(_) => panic!(), 155 | } 156 | } 157 | 158 | // This implementation allows for invalid equalities like `b'-' == b'\x7f'`, but that's OK. 159 | // 160 | // The only troublesome characters are: 161 | // @ -> ` 162 | // [ -> { 163 | // \ -> | 164 | // ] -> } 165 | // ^ -> ~ 166 | // _ -> DEL 167 | // 168 | // None the these characters have a "false lower case" variant which can occur in the input. 169 | // This function is 40% faster than the variant in rust's core library, which is implemented 170 | // more strictly. 171 | #[inline] 172 | #[must_use] 173 | const fn eq_ignore_ascii_case(a: &[u8], b: &[u8]) -> bool { 174 | if a.len() != b.len() { 175 | return false; 176 | } 177 | 178 | // Cannot use for-loops in const fn. 179 | let mut i = 0; 180 | while i < a.len() { 181 | if (a[i] | 0x20) != (b[i] | 0x20) { 182 | return false; 183 | } 184 | i += 1; 185 | } 186 | 187 | true 188 | } 189 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changes between the versions 2 | 3 | ### 0.7.2 (2024-09-16) 4 | 5 | * Enable full no-std use 6 | 7 | ### 0.7.1 (2024-09-15) 8 | 9 | * Add missing Changelog for doc creation 10 | 11 | ### 0.7.0 (2024-09-15) 12 | 13 | * Update to tz-rs v0.7.0 14 | 15 | ### 0.6.1 (2023-12-30) 16 | 17 | * Split into `tzdb` and `tzdb_data` 18 | * Optimize lookup. It's ~39% faster now. 19 | 20 | ### 0.6.0 (2023-12-29) 21 | 22 | * Unchanged stable release 23 | 24 | ### 0.6.0-pre.1 (2023-12-27) 25 | 26 | * Make `iana_time_zone` inclusion optional 27 | 28 | ### 0.5.9 (2023-12-27) 29 | 30 | * Update to Time Zone Database release [2023d](https://mm.icann.org/pipermail/tz-announce/2023-December/000080.html) 31 | 32 | ### ~~0.5.8 (2023-12-27)~~ 33 | 34 | ### 0.5.7 (2023-05-11) 35 | 36 | * Fewer macros = faster compile times 37 | * Update to Time Zone Database release [2023c](https://mm.icann.org/pipermail/tz-announce/2023-March/000079.html) 38 | 39 | ### 0.5.6 (2023-03-24) 40 | 41 | * Update to Time Zone Database release [2023b](https://mm.icann.org/pipermail/tz-announce/2023-March/000078.html) 42 | 43 | ### 0.5.5 (2023-03-24) 44 | 45 | * Update to Time Zone Database release [2023a](https://mm.icann.org/pipermail/tz-announce/2023-March/000077.html) 46 | * Remove "etc/localtime" as it should not be part of this library 47 | 48 | ### ~~0.5.4 (2023-03-24)~~ 49 | 50 | ### 0.5.3 (2023-01-01) 51 | 52 | * No need to use `unsafe` functions 53 | 54 | ### 0.5.2 (2022-12-22) 55 | 56 | * Prepare v0.5.x branch so that v0.3.x can re-export it 57 | 58 | ### 0.5.1 (2022-11-30) 59 | 60 | * Update to Time Zone Database release [2022g](https://mm.icann.org/pipermail/tz-announce/2022-November/000076.html) 61 | 62 | ### 0.5.0 (2022-11-24) 63 | 64 | * Release v0.5.0 65 | 66 | #### 0.5.0-pre.4 (2022-10-29) 67 | 68 | * Update to Time Zone Database release [2022f](https://mm.icann.org/pipermail/tz-announce/2022-October/000075.html) 69 | * Use `edition = "2021"` 70 | 71 | #### 0.5.0-pre.3 (2022-10-12) 72 | 73 | * Remove `utcnow` integration 74 | * Simplify by removing `no_std` support 75 | * Update to Time Zone Database release [2022e](https://mm.icann.org/pipermail/tz-announce/2022-October/000074.html) 76 | 77 | #### 0.5.0-pre.2 (2022-09-25) 78 | 79 | * Update to Time Zone Database release [2022d](https://mm.icann.org/pipermail/tz-announce/2022-September/000073.html) 80 | * Update to iana-time-zone 0.1.50 81 | 82 | #### 0.5.0-pre.1 (2022-09-14) 83 | 84 | * Simplify a lot by removing feature gates [[#123](https://github.com/Kijewski/tzdb/pull/123)] 85 | 86 | ### 0.4.5 (2022-08-31) 87 | 88 | * Remove [phf](https://crates.io/crates/phf) dependency 89 | 90 | ### 0.4.4 (2022-08-16) 91 | 92 | * Update to [Time Zone Database 2022c](https://mm.icann.org/pipermail/tz-announce/2022-August/000072.html) 93 | 94 | ### 0.4.3 (2022-08-12) 95 | 96 | * Update [iana-time-zone](https://crates.io/crates/iana-time-zone) to fix more issues on CentOS 7 97 | ([#49](https://github.com/strawlab/iana-time-zone/pull/49)), and not to depend on core-foundation 98 | ([#50](https://github.com/strawlab/iana-time-zone/pull/50)) 99 | 100 | ### 0.4.2 (2022-08-11) 101 | 102 | * Update to [Time Zone Database 2022b](https://mm.icann.org/pipermail/tz-announce/2022-August/000071.html) 103 | 104 | ### 0.4.1 (2022-08-10) 105 | 106 | * Update [iana-time-zone](https://crates.io/crates/iana-time-zone) to v0.1.42 107 | in order to fix problems on CentOS 108 | ([#48](https://github.com/strawlab/iana-time-zone/pull/48)) 109 | 110 | ### 0.4.0 (2022-08-05) 111 | 112 | * Increase msrv to 1.60 113 | * Add `now` module, which uses [`utcnow()`](https://crates.io/crates/utcnow), 114 | and works in `#[no_std]` 115 | 116 | ### 0.3.4 (2022-08-02) 117 | 118 | * Fix endianness issues for PowerPCs 119 | 120 | ### 0.3.3 (2022-08-01) 121 | 122 | * Update [tz-rs](https://crates.io/crates/tz-rs) to v0.6.12 to work in a no-std context 123 | ([#33](https://github.com/x-hgg-x/tz-rs/pull/33)) 124 | * Expand documentation 125 | * Add features `std`, `alloc`, and `fallback` (unused until the next breaking change) 126 | 127 | ### 0.3.2 (2022-07-30) 128 | 129 | * Update [iana-time-zone](https://crates.io/crates/iana-time-zone) to implement 130 | [`local_tz()`](https://docs.rs/tzdb/0.3.2/tzdb/fn.local_tz.html) for 131 | Illumos ([#44](https://github.com/strawlab/iana-time-zone/pull/44)) and 132 | Android ([#45](https://github.com/strawlab/iana-time-zone/pull/45)) 133 | 134 | ### 0.3.1 (2022-07-23) 135 | 136 | * Update [iana-time-zone](https://crates.io/crates/iana-time-zone) to implement 137 | [`local_tz()`](https://docs.rs/tzdb/0.2.6/tzdb/fn.local_tz.html) for 138 | iOS ([#41](https://github.com/strawlab/iana-time-zone/pull/41)) 139 | 140 | ### 0.3.0 (2022-07-21) 141 | 142 | * Remove serde-as feature. The feature is very unrelated to goals of the crate, so it should be 143 | moved somewhere else 144 | * Split up `generated.rs` to speed up compilation if not all features are selected 145 | * Reduce msrv to 1.55 146 | 147 | ### 0.2.7 (2022-06-30) 148 | 149 | * Fix error if build and target platform have different pointer widths 150 | 151 | ### 0.2.6 (2022-06-29) 152 | 153 | * Update [iana-time-zone](https://crates.io/crates/iana-time-zone) to implement 154 | [`local_tz()`](https://docs.rs/tzdb/0.2.6/tzdb/fn.local_tz.html) for 155 | Wasm ([#38](https://github.com/strawlab/iana-time-zone/pull/38)), and 156 | {Free,Net,Open,Dragonfly}BSD ([#39](https://github.com/strawlab/iana-time-zone/pull/39)) 157 | 158 | ### 0.2.5 (2022-06-26) 159 | 160 | * Ensure `-Zminimal-versions` works 161 | 162 | ### 0.2.4 (2022-06-08) 163 | 164 | * Fix missing import if the project is used with `default-features = false` 165 | 166 | ### 0.2.3 (2022-04-15) 167 | 168 | * Fix lookup error for names containing underscores 169 | 170 | ### 0.2.2 (2022-03-27) 171 | 172 | * Bump dependency versions 173 | 174 | ### 0.2.1 (2022-03-27) 175 | 176 | * Fix typos 177 | * Introduce `VERSION` and `VERSION_HASH` 178 | 179 | ### 0.2.0 (2022-03-17) 180 | 181 | * Update to 2022a 182 | * Make the unparsed binary time zone data available 183 | * Simplify the library by removing the trait TimeZoneExt: 184 | 185 | * `TimeZoneExt::from_db()` is now `tz_by_name()` 186 | * `TimeZoneExt::local_from_db()` is now `local_tz()` 187 | * `TimeZoneExt::names_in_db()` is now `TZ_NAMES` 188 | 189 | ### 0.1.4 (2022-03-17) 190 | 191 | * Re-export v0.2 with old names and default features 192 | 193 | ### 0.1.3 (2022-03-03) 194 | 195 | * Optimize `DateTime` deserialization to work without dynamic allocation 196 | ([tz-rs#22](https://github.com/x-hgg-x/tz-rs/pull/22)) 197 | 198 | ### 0.1.2 (2022-03-02) 199 | 200 | * Include “backzone” data to include pre-1970 information for some more time zones 201 | 202 | ### 0.1.1 (2022-03-01) 203 | 204 | * Make `UtcDateTime`/`DateTime` serializable with `serde` using `serde_with` 205 | 206 | ### 0.1.0 (2022-02-28) 207 | 208 | * Initial release 209 | -------------------------------------------------------------------------------- /make-tzdb/src/parse.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::Deserialize; 4 | 5 | #[derive(Debug, Deserialize)] 6 | pub(crate) enum TimeZone { 7 | TimeZone { 8 | transitions: Vec, 9 | local_time_types: Vec, 10 | leap_seconds: Vec, 11 | extra_rule: Option, 12 | }, 13 | } 14 | 15 | #[derive(Debug, Deserialize)] 16 | pub(crate) enum Transition { 17 | Transition { 18 | unix_leap_time: i64, 19 | local_time_type_index: usize, 20 | }, 21 | } 22 | 23 | #[derive(Debug, Deserialize)] 24 | pub(crate) enum LeapSecond { 25 | LeapSecond { 26 | unix_leap_time: i64, 27 | correction: i32, 28 | }, 29 | } 30 | 31 | #[derive(Debug, Deserialize)] 32 | pub(crate) enum TransitionRule { 33 | Fixed(LocalTimeType), 34 | Alternate(AlternateTime), 35 | } 36 | 37 | #[derive(Debug, Deserialize)] 38 | pub(crate) enum AlternateTime { 39 | AlternateTime { 40 | std: LocalTimeType, 41 | dst: LocalTimeType, 42 | dst_start: RuleDay, 43 | dst_start_time: i32, 44 | dst_end: RuleDay, 45 | dst_end_time: i32, 46 | }, 47 | } 48 | 49 | #[derive(Debug, Deserialize)] 50 | pub(crate) enum LocalTimeType { 51 | LocalTimeType { 52 | ut_offset: i32, 53 | is_dst: bool, 54 | time_zone_designation: Option, 55 | }, 56 | } 57 | 58 | #[derive(Debug, Deserialize)] 59 | pub(crate) enum RuleDay { 60 | Julian1WithoutLeap(Julian1WithoutLeap), 61 | Julian0WithLeap(Julian0WithLeap), 62 | MonthWeekDay(MonthWeekDay), 63 | } 64 | 65 | #[derive(Debug, Deserialize)] 66 | pub(crate) enum Julian1WithoutLeap { 67 | Julian1WithoutLeap(u16), 68 | } 69 | 70 | #[derive(Debug, Deserialize)] 71 | pub(crate) enum Julian0WithLeap { 72 | Julian0WithLeap(u16), 73 | } 74 | 75 | #[derive(Debug, Deserialize)] 76 | pub(crate) enum MonthWeekDay { 77 | MonthWeekDay { month: u8, week: u8, week_day: u8 }, 78 | } 79 | 80 | impl fmt::Display for Transition { 81 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 82 | let Transition::Transition { 83 | unix_leap_time, 84 | local_time_type_index, 85 | } = &self; 86 | writeln!( 87 | f, 88 | "new_transition({}, {})", 89 | unix_leap_time, local_time_type_index 90 | )?; 91 | Ok(()) 92 | } 93 | } 94 | 95 | impl fmt::Display for LocalTimeType { 96 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 97 | let LocalTimeType::LocalTimeType { 98 | ut_offset, 99 | is_dst, 100 | time_zone_designation, 101 | } = self; 102 | let time_zone_designation = time_zone_designation.as_deref().map(DisplayTzd); 103 | writeln!( 104 | f, 105 | "new_local_time_type({}, {}, {})", 106 | ut_offset, 107 | is_dst, 108 | DisplayOption(time_zone_designation.as_ref()), 109 | )?; 110 | Ok(()) 111 | } 112 | } 113 | 114 | impl fmt::Display for LeapSecond { 115 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 116 | let LeapSecond::LeapSecond { 117 | unix_leap_time, 118 | correction, 119 | } = self; 120 | writeln!(f, "new_leap_second({}, {})", unix_leap_time, correction)?; 121 | Ok(()) 122 | } 123 | } 124 | 125 | impl fmt::Display for TransitionRule { 126 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 127 | match self { 128 | TransitionRule::Fixed(t) => writeln!(f, "TransitionRule::Fixed({})", t)?, 129 | TransitionRule::Alternate(t) => { 130 | writeln!(f, "TransitionRule::Alternate({})", t)?; 131 | }, 132 | } 133 | Ok(()) 134 | } 135 | } 136 | 137 | impl fmt::Display for AlternateTime { 138 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 139 | let AlternateTime::AlternateTime { 140 | std, 141 | dst, 142 | dst_start, 143 | dst_start_time, 144 | dst_end, 145 | dst_end_time, 146 | } = self; 147 | writeln!( 148 | f, 149 | "new_alternate_time({}, {}, {}, {}, {}, {})", 150 | std, dst, dst_start, dst_start_time, dst_end, dst_end_time, 151 | ) 152 | } 153 | } 154 | 155 | impl fmt::Display for MonthWeekDay { 156 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 157 | let MonthWeekDay::MonthWeekDay { 158 | month, 159 | week, 160 | week_day, 161 | } = self; 162 | writeln!(f, "new_month_week_day({}, {}, {})", month, week, week_day) 163 | } 164 | } 165 | 166 | impl fmt::Display for Julian0WithLeap { 167 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 168 | let Julian0WithLeap::Julian0WithLeap(t) = self; 169 | writeln!(f, "new_julian0_with_leap({})", t) 170 | } 171 | } 172 | 173 | impl fmt::Display for Julian1WithoutLeap { 174 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 175 | let Julian1WithoutLeap::Julian1WithoutLeap(t) = self; 176 | writeln!(f, "new_julian1_without_leap({})", t) 177 | } 178 | } 179 | 180 | impl fmt::Display for RuleDay { 181 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 182 | match self { 183 | RuleDay::Julian0WithLeap(t) => writeln!(f, "RuleDay::Julian0WithLeap({})", t)?, 184 | RuleDay::Julian1WithoutLeap(t) => writeln!(f, "RuleDay::Julian1WithoutLeap({})", t)?, 185 | RuleDay::MonthWeekDay(t) => { 186 | writeln!(f, "RuleDay::MonthWeekDay({})", t)?; 187 | }, 188 | } 189 | Ok(()) 190 | } 191 | } 192 | 193 | pub(crate) struct DisplayVec<'a, T>(pub(crate) &'a [T]); 194 | 195 | impl<'a, T: fmt::Display> fmt::Display for DisplayVec<'a, T> { 196 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 197 | writeln!(f, "[")?; 198 | for elem in self.0 { 199 | writeln!(f, " {},", elem)?; 200 | } 201 | writeln!(f, "]") 202 | } 203 | } 204 | 205 | pub(crate) struct DisplayOption<'a, T>(Option<&'a T>); 206 | 207 | impl<'a, T: fmt::Display> fmt::Display for DisplayOption<'a, T> { 208 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 209 | match &self.0 { 210 | Some(v) => writeln!(f, "Some({})", v), 211 | None => writeln!(f, "None"), 212 | } 213 | } 214 | } 215 | 216 | struct DisplayTzd<'a>(&'a str); 217 | 218 | impl fmt::Display for DisplayTzd<'_> { 219 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 220 | write!(f, "b{:?}", &self.0) 221 | } 222 | } 223 | 224 | impl fmt::Display for TimeZone { 225 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 226 | let TimeZone::TimeZone { 227 | transitions, 228 | local_time_types, 229 | leap_seconds, 230 | extra_rule, 231 | } = self; 232 | writeln!( 233 | f, 234 | "new_time_zone_ref(&{}, &{}, &{}, &{})", 235 | DisplayVec(transitions), 236 | DisplayVec(local_time_types), 237 | DisplayVec(leap_seconds), 238 | DisplayOption(extra_rule.as_ref()), 239 | ) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /tzdb/src/now.rs: -------------------------------------------------------------------------------- 1 | //! Get the current time in some time zone 2 | 3 | extern crate std; 4 | 5 | use core::convert::TryFrom; 6 | use core::fmt; 7 | use std::time::{SystemTime, SystemTimeError}; 8 | 9 | use tz::{DateTime, TimeZoneRef, TzError}; 10 | 11 | #[cfg(not(feature = "local"))] 12 | mod iana_time_zone { 13 | #[allow(missing_copy_implementations)] // intentionally omitted 14 | #[derive(Debug)] 15 | #[non_exhaustive] 16 | pub struct GetTimezoneError(Impossible); 17 | 18 | #[derive(Debug, Clone, Copy)] 19 | enum Impossible {} 20 | } 21 | 22 | /// An error as returned by [`local()`] and similar functions 23 | /// 24 | /// # See also: 25 | /// 26 | /// * [`local()`] / [`local_or()`] 27 | /// * [`in_named()`] / [`in_named_or()`] 28 | /// * [`in_tz()`] 29 | #[allow(clippy::module_name_repetitions)] 30 | #[derive(Debug)] 31 | pub enum NowError { 32 | /// Could not get time zone. Only returned by [`local()`]. 33 | TimeZone(iana_time_zone::GetTimezoneError), 34 | /// Unknown system time zone. Only returned by [`local()`], and [`in_named()`]. 35 | UnknownTimezone, 36 | /// Could not project timestamp. 37 | ProjectDateTime(TzError), 38 | /// Could not get current system time. 39 | Utcnow(SystemTimeError), 40 | } 41 | 42 | impl fmt::Display for NowError { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | f.write_str(match self { 45 | Self::TimeZone(_) => "could not get time zone", 46 | Self::UnknownTimezone => "unknown system time zone", 47 | Self::ProjectDateTime(_) => "could not project timestamp", 48 | Self::Utcnow(_) => "could not get current system time", 49 | }) 50 | } 51 | } 52 | 53 | impl core::error::Error for NowError { 54 | fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { 55 | match self { 56 | #[cfg(feature = "local")] 57 | Self::TimeZone(err) => Some(err), 58 | #[cfg(not(feature = "local"))] 59 | Self::TimeZone(_) => None, 60 | Self::UnknownTimezone => None, 61 | Self::ProjectDateTime(err) => Some(err), 62 | Self::Utcnow(err) => Some(err), 63 | } 64 | } 65 | } 66 | 67 | /// Get the current time in the local system time zone 68 | /// 69 | /// # Errors 70 | /// 71 | /// Possible errors include: 72 | /// 73 | /// * The current [Unix time](https://en.wikipedia.org/w/index.php?title=Unix_time&oldid=1101650731) 74 | /// could not be determined. 75 | /// * The current Unix time could not be projected into the time zone. 76 | /// Most likely the system time is off, or you are a time traveler trying run this code a few billion years in the future or past. 77 | /// * The local time zone could not be determined. 78 | /// * The local time zone is not a valid [IANA time zone](https://www.iana.org/time-zones). 79 | /// 80 | /// # Example 81 | /// 82 | /// ```rust 83 | /// # fn main() -> Result<(), tzdb::now::NowError> { 84 | /// // Query the time zone of the local system: 85 | /// let now = tzdb::now::local()?; 86 | /// # Ok(()) } 87 | /// ``` 88 | /// 89 | /// In most cases you will want to default to a specified time zone if the system timezone 90 | /// could not be determined. Then use e.g. 91 | /// 92 | /// ```rust 93 | /// # fn main() -> Result<(), tzdb::now::NowError> { 94 | /// let now = tzdb::now::local_or(tzdb::time_zone::GMT)?; 95 | /// # Ok(()) } 96 | /// ``` 97 | /// 98 | /// # See also: 99 | /// 100 | /// * `local()` / [`local_or()`] 101 | /// * [`in_named()`] / [`in_named_or()`] 102 | /// * [`in_tz()`] 103 | #[cfg(feature = "local")] 104 | #[cfg_attr(docsrs, doc(cfg(feature = "local")))] 105 | pub fn local() -> Result { 106 | in_named(iana_time_zone::get_timezone().map_err(NowError::TimeZone)?) 107 | } 108 | 109 | /// Get the current time in the local system time zone with a fallback time zone 110 | /// 111 | /// # Errors 112 | /// 113 | /// Possible errors include: 114 | /// 115 | /// * The current [Unix time](https://en.wikipedia.org/w/index.php?title=Unix_time&oldid=1101650731) 116 | /// could not be determined. 117 | /// * The current Unix time could not be projected into the time zone. 118 | /// Most likely the system time is off, or you are a time traveler trying run this code a few billion years in the future or past. 119 | /// 120 | /// # Example 121 | /// 122 | /// ```rust 123 | /// # fn main() -> Result<(), tzdb::now::NowError> { 124 | /// // Query the time zone of the local system, or use GMT as default: 125 | /// let now = tzdb::now::local_or(tzdb::time_zone::GMT)?; 126 | /// # Ok(()) } 127 | /// ``` 128 | /// 129 | /// # See also: 130 | /// 131 | /// * [`local()`] / `local_or()` 132 | /// * [`in_named()`] / [`in_named_or()`] 133 | /// * [`in_tz()`] 134 | #[cfg(feature = "local")] 135 | #[cfg_attr(docsrs, doc(cfg(feature = "local")))] 136 | pub fn local_or(default: TimeZoneRef<'_>) -> Result { 137 | let tz = iana_time_zone::get_timezone() 138 | .ok() 139 | .and_then(crate::tz_by_name) 140 | .unwrap_or(default); 141 | in_tz(tz) 142 | } 143 | 144 | /// Get the current time a given time zone 145 | /// 146 | /// # Errors 147 | /// 148 | /// Possible errors include: 149 | /// 150 | /// * The current [Unix time](https://en.wikipedia.org/w/index.php?title=Unix_time&oldid=1101650731) 151 | /// could not be determined. 152 | /// * The current Unix time could not be projected into the time zone. 153 | /// Most likely the system time is off, or you are a time traveler trying run this code a few billion years in the future or past. 154 | /// 155 | /// # Example 156 | /// 157 | /// ```rust 158 | /// # fn main() -> Result<(), tzdb::now::NowError> { 159 | /// // What is the time in Berlin? 160 | /// let now = tzdb::now::in_tz(tzdb::time_zone::europe::BERLIN)?; 161 | /// # Ok(()) } 162 | /// ``` 163 | /// 164 | /// # See also: 165 | /// 166 | /// * [`local()`] / [`local_or()`] 167 | /// * [`in_named()`] / [`in_named_or()`] 168 | /// * `in_tz()` 169 | pub fn in_tz(time_zone_ref: TimeZoneRef<'_>) -> Result { 170 | let now = SystemTime::now() 171 | .duration_since(SystemTime::UNIX_EPOCH) 172 | .map_err(NowError::Utcnow)?; 173 | let secs = 174 | i64::try_from(now.as_secs()).map_err(|_| NowError::ProjectDateTime(TzError::OutOfRange))?; 175 | let nanos = now.subsec_nanos(); 176 | DateTime::from_timespec(secs, nanos, time_zone_ref).map_err(NowError::ProjectDateTime) 177 | } 178 | 179 | /// Get the current time in a given time zone, by name 180 | /// 181 | /// # Errors 182 | /// 183 | /// Possible errors include: 184 | /// 185 | /// * The current [Unix time](https://en.wikipedia.org/w/index.php?title=Unix_time&oldid=1101650731) 186 | /// could not be determined. 187 | /// * The current Unix time could not be projected into the time zone. 188 | /// Most likely the system time is off, or you are a time traveler trying run this code a few billion years in the future or past. 189 | /// * The time zone is not a valid [IANA time zone](https://www.iana.org/time-zones). 190 | /// 191 | /// # Example 192 | /// 193 | /// ```rust 194 | /// # fn main() -> Result<(), tzdb::now::NowError> { 195 | /// // What is the time in Berlin? 196 | /// let now = tzdb::now::in_named("Europe/Berlin")?; 197 | /// # Ok(()) } 198 | /// ``` 199 | /// 200 | /// In most cases you will want to default to a specified time zone if the time zone was not found. 201 | /// Then use e.g. 202 | /// 203 | /// ```rust 204 | /// # fn main() -> Result<(), tzdb::now::NowError> { 205 | /// let now = tzdb::now::in_named_or(tzdb::time_zone::GMT, "Some/City")?; 206 | /// # Ok(()) } 207 | /// ``` 208 | /// 209 | /// # See also: 210 | /// 211 | /// * [`local()`] / [`local_or()`] 212 | /// * `in_named()` / [`in_named_or()`] 213 | /// * [`in_tz()`] 214 | pub fn in_named(tz: impl AsRef<[u8]>) -> Result { 215 | in_tz(crate::tz_by_name(tz).ok_or(NowError::UnknownTimezone)?) 216 | } 217 | 218 | /// Get the current time in a given time zone, by name, or default to some static time zone 219 | /// 220 | /// # Errors 221 | /// 222 | /// Possible errors include: 223 | /// 224 | /// * The current [Unix time](https://en.wikipedia.org/w/index.php?title=Unix_time&oldid=1101650731) 225 | /// could not be determined. 226 | /// * The current Unix time could not be projected into the time zone. 227 | /// Most likely the system time is off, or you are a time traveler trying run this code a few billion years in the future or past. 228 | /// 229 | /// # Example 230 | /// 231 | /// ```rust 232 | /// # fn main() -> Result<(), tzdb::now::NowError> { 233 | /// // What is the time in Some City? 234 | /// let now = tzdb::now::in_named_or(tzdb::time_zone::GMT, "Some/City")?; 235 | /// # Ok(()) } 236 | /// ``` 237 | /// 238 | /// # See also: 239 | /// 240 | /// * [`local()`] / [`local_or()`] 241 | /// * [`in_named()`] / `in_named_or()` 242 | /// * [`in_tz()`] 243 | pub fn in_named_or(default: TimeZoneRef<'_>, tz: impl AsRef<[u8]>) -> Result { 244 | in_tz(crate::tz_by_name(tz).unwrap_or(default)) 245 | } 246 | -------------------------------------------------------------------------------- /make-tzdb/generate_lookup_table.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from logging import basicConfig, INFO 4 | from shutil import which 5 | from subprocess import PIPE, Popen 6 | from sys import stdin, stdout 7 | from typing import Optional 8 | from re import match 9 | 10 | 11 | def convert(stdin, stdout): 12 | gperf = which('gperf') 13 | if not gperf: 14 | raise Exception('No gperf') 15 | gperf = Popen( 16 | ['gperf', '--ignore-case', '--readonly-tables', '--struct-type', '--global-table', '-m', '1000'], 17 | stdin=stdin, stdout=PIPE, 18 | ) 19 | with gperf: 20 | stdin, _ = gperf.communicate(timeout=120) 21 | match gperf.returncode: 22 | case 0: 23 | pass 24 | case code: 25 | raise Exception(f'gperf returned {code!r}') 26 | stdin = str(stdin, 'UTF-8', 'strict') 27 | 28 | total_keywords: Optional[int] = None 29 | min_word_length: Optional[int] = None 30 | max_word_length: Optional[int] = None 31 | min_hash_value: Optional[int] = None 32 | max_hash_value: Optional[int] = None 33 | duplicates: Optional[int] = None 34 | maximum_key_range: Optional[int] = None 35 | asso_values: list[int] = [] 36 | hash_init: Optional[str] = [] 37 | hash_switch_fst_idx: Optional[int] = None 38 | hash_switch_idx: Optional[int|str] = None 39 | hash_switch: dict[int|str, tuple[str, Optional[str]]] = {} 40 | table: list[Optional[tuple[str, str]]] = [] 41 | 42 | state = 'start' 43 | line: str 44 | for line in stdin.splitlines(): 45 | line = line.strip() 46 | if not line: 47 | continue 48 | 49 | match state: 50 | case 'start': 51 | match [s.strip() for s in line.split()]: 52 | case ('#define', 'TOTAL_KEYWORDS', s): 53 | total_keywords = int(s) 54 | continue 55 | case ('#define', 'MIN_WORD_LENGTH', s): 56 | min_word_length = int(s) 57 | continue 58 | case ('#define', 'MAX_WORD_LENGTH', s): 59 | max_word_length = int(s) 60 | continue 61 | case ('#define', 'MIN_HASH_VALUE', s): 62 | min_hash_value = int(s) 63 | continue 64 | case ('#define', 'MAX_HASH_VALUE', s): 65 | max_hash_value = int(s) 66 | continue 67 | 68 | m = match(r'/\* maximum key range = (\d+), duplicates = (\d+) \*/', line) 69 | if m: 70 | maximum_key_range, duplicates = m.groups() 71 | maximum_key_range = int(maximum_key_range) 72 | duplicates = int(duplicates) 73 | state = 'pre_asso_values' 74 | 75 | case 'pre_asso_values': 76 | if 'asso_values[] =' in line: 77 | state = 'asso_values' 78 | 79 | case 'asso_values': 80 | if '}' in line: 81 | state = 'pre_hash_switch' 82 | elif '{' not in line: 83 | s = (s.strip() for s in line.split(',')) 84 | asso_values.extend(int(s) for s in s if s) 85 | 86 | case 'pre_hash_switch': 87 | m = match(r'register unsigned int hval = ([^;]+);', line) 88 | if m: 89 | hash_init, = m.groups() 90 | state = 'hash_switch' 91 | continue 92 | 93 | case 'hash_switch': 94 | if line == 'default:': 95 | hash_switch_idx = 'default' 96 | continue 97 | 98 | m = match(r'(return)?[^\)]+\)str\[([^\]]+)*\](?:([\+\-]\d+))?\];', line) 99 | if m: 100 | ret, idx, offs = m.groups() 101 | if ret: 102 | hash_switch_idx = 'finally' 103 | assert hash_switch_idx is not None 104 | hash_switch[hash_switch_idx] = idx, offs 105 | hash_switch_idx = None 106 | if ret: 107 | state = 'pre_wordlist' 108 | continue 109 | 110 | m = match(r'case (\d+):', line) 111 | if m: 112 | hash_switch_idx, = m.groups() 113 | hash_switch_idx = int(hash_switch_idx) 114 | if hash_switch_fst_idx is None: 115 | hash_switch_fst_idx = hash_switch_idx 116 | continue 117 | 118 | case 'pre_wordlist': 119 | if line.endswith('wordlist[] ='): 120 | state = 'wordlist' 121 | 122 | case 'wordlist': 123 | if line == '};': 124 | state = 'done' 125 | break 126 | 127 | m = match(r'{"([^"]+)", "([^"]+)"}', line) 128 | if m: 129 | name, canon = m.groups() 130 | table.append((name, canon)) 131 | continue 132 | 133 | for _ in range(line.count('{""}')): 134 | table.append(None) 135 | 136 | assert duplicates == 0 137 | 138 | print('// GENERATED FILE', file=stdout) 139 | print('// ALL CHANGES MADE IN THIS FOLDER WILL BE LOST!', file=stdout) 140 | print(file=stdout) 141 | print('use tz::TimeZoneRef;', file=stdout) 142 | print(file=stdout) 143 | print('use crate::eq_ignore_ascii_case;', file=stdout) 144 | print('use super::raw_tzdata;', file=stdout) 145 | print('use super::tzdata;', file=stdout) 146 | print(file=stdout) 147 | 148 | print('#[derive(Clone, Copy)]', file=stdout) 149 | print('#[repr(u16)]', file=stdout) 150 | print('pub(crate) enum Index {', file=stdout) 151 | idx = 0 152 | for entry in table: 153 | match entry: 154 | case (name, canon): 155 | print(f' V{idx} = {idx + 1},', file=stdout) 156 | idx += 1 157 | entry_count = idx 158 | print('}', file=stdout) 159 | print(file=stdout) 160 | 161 | print(f'const WORDLIST: [Option; {len(table)}] = [', file=stdout) 162 | idx = 0 163 | for entry in table: 164 | match entry: 165 | case (name, canon): 166 | print(f' Some(Index::V{idx}),', file=stdout) 167 | idx += 1 168 | case _: 169 | print(' None,', file=stdout) 170 | print('];', file=stdout) 171 | print(file=stdout) 172 | 173 | print(f'const NAMES: [&[u8]; {entry_count + 1}] = [', file=stdout) 174 | print(f' b"",', file=stdout) 175 | for entry in table: 176 | match entry: 177 | case (name, canon): 178 | print(f' b"{name}",', file=stdout) 179 | print('];', file=stdout) 180 | print(file=stdout) 181 | 182 | print(f'pub(crate) const TIME_ZONES: [&TimeZoneRef<\'static>; {entry_count + 1}] = [', file=stdout) 183 | for entry in table: 184 | match entry: 185 | case (name, canon): 186 | print(f' &tzdata::{canon},', file=stdout) 187 | break 188 | for entry in table: 189 | match entry: 190 | case (name, canon): 191 | print(f' &tzdata::{canon},', file=stdout) 192 | print('];', file=stdout) 193 | print(file=stdout) 194 | 195 | print(f'pub(crate) const RAW_TIME_ZONES: [&[u8]; {entry_count + 1}] = [', file=stdout) 196 | for entry in table: 197 | match entry: 198 | case (name, canon): 199 | print(f' raw_tzdata::{canon},', file=stdout) 200 | break 201 | for entry in table: 202 | match entry: 203 | case (name, canon): 204 | print(f' raw_tzdata::{canon},', file=stdout) 205 | print('];', file=stdout) 206 | print(file=stdout) 207 | 208 | asso_values.pop() 209 | print(f'const ASSO_VALUES: [u16; 257] = [', file=stdout) 210 | for asso_value in asso_values: 211 | print(f' {asso_value},', file=stdout) 212 | print(f'{max_hash_value + 1}];', file=stdout) 213 | print(file=stdout) 214 | 215 | print('pub(crate) const fn find_key(s: &[u8]) -> Option {', file=stdout) 216 | print(' let len = s.len();', file=stdout) 217 | print(f' if !matches!(len, {min_word_length}..={max_word_length}) {{', file=stdout) 218 | print(' return None;', file=stdout) 219 | print(' }', file=stdout) 220 | print(file=stdout) 221 | 222 | def hash_add(idx, offs): 223 | value = f's[{idx}] as usize' 224 | if offs: 225 | if offs.startswith('+'): 226 | value = f'({value}).wrapping_add({offs[1:]})' 227 | elif offs.startswith('-'): 228 | value = f'({value}).wrapping_sub({offs[1:]})' 229 | else: 230 | raise Exception(f'offs? {offs!r}') 231 | return f'key = key.wrapping_add(ASSO_VALUES[{value}] as usize);' 232 | 233 | match hash_init: 234 | case 'len': 235 | print(' let mut key: usize = len;', file=stdout) 236 | case _: 237 | raise Exception(f'hash_init == {hash_init!r} != "len"') 238 | match hash_switch.get('finally'): 239 | case (idx, offs): 240 | print(f' {hash_add(idx, offs)}', file=stdout) 241 | for item in reversed(hash_switch.items()): 242 | match item: 243 | case (int(key), (idx, offs)): 244 | print(f' if len >= {key} {{', file=stdout) 245 | print(f' {hash_add(idx, offs)}', file=stdout) 246 | print(' }', file=stdout) 247 | match hash_switch.get('default'): 248 | case (idx, offs): 249 | print(f' if len > {hash_switch_fst_idx} {{', file=stdout) 250 | print(f' {hash_add(idx, offs)}', file=stdout) 251 | print(' }', file=stdout) 252 | print(file=stdout) 253 | 254 | print(f' if key > {max_hash_value} {{', file=stdout) 255 | print(' return None;', file=stdout) 256 | print(' }', file=stdout) 257 | print(' let key = match WORDLIST[key] {', file=stdout) 258 | print(' Some(key) => key,', file=stdout) 259 | print(' None => return None,', file=stdout) 260 | print(' };', file=stdout) 261 | print(' if !eq_ignore_ascii_case(s, NAMES[key as u16 as usize]) {', file=stdout) 262 | print(' return None;', file=stdout) 263 | print(' }', file=stdout) 264 | print(file=stdout) 265 | print(' Some(key)', file=stdout) 266 | print('}', file=stdout) 267 | print(file=stdout) 268 | 269 | 270 | if __name__ == '__main__': 271 | basicConfig(level=INFO) 272 | convert(stdin, stdout) 273 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | -------------------------------------------------------------------------------- /make-tzdb/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // 3 | // Copyright 2022 René Kijewski 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | mod parse; 18 | 19 | use std::cmp::Ordering; 20 | use std::env::{args, var_os}; 21 | use std::fmt::Write as _; 22 | use std::fs::{create_dir_all, read, read_to_string, OpenOptions}; 23 | use std::io::Write as _; 24 | use std::path::{Path, PathBuf}; 25 | 26 | use anyhow::anyhow; 27 | use convert_case::{Case, Casing}; 28 | use indexmap::IndexMap; 29 | use itertools::Itertools; 30 | use subprocess::{Popen, PopenConfig, Redirection}; 31 | use tz::TimeZone; 32 | use walkdir::WalkDir; 33 | 34 | const GENERATED_FILE: &str = r#"// GENERATED FILE 35 | // ALL CHANGES MADE IN THIS FOLDER WILL BE LOST! 36 | 37 | "#; 38 | 39 | #[derive(Debug, Clone)] 40 | struct TzName { 41 | /// "Europe/Belfast" 42 | canon: String, 43 | /// "Europe/Guernsey" 44 | full: String, 45 | /// Some("europe") // Snake 46 | major: Option, 47 | /// "GUERNSEY" // UpperSnake 48 | minor: String, 49 | } 50 | 51 | impl PartialEq for TzName { 52 | fn eq(&self, other: &Self) -> bool { 53 | self.cmp(other) == Ordering::Equal 54 | } 55 | } 56 | 57 | impl Eq for TzName {} 58 | 59 | impl PartialOrd for TzName { 60 | fn partial_cmp(&self, other: &Self) -> Option { 61 | Some(self.cmp(other)) 62 | } 63 | } 64 | 65 | impl Ord for TzName { 66 | fn cmp(&self, other: &Self) -> Ordering { 67 | match self.major.is_some().cmp(&other.major.is_some()) { 68 | Ordering::Equal => match self.major.cmp(&other.major) { 69 | Ordering::Equal => self.minor.cmp(&other.minor), 70 | r => r, 71 | }, 72 | r => r, 73 | } 74 | } 75 | } 76 | 77 | impl TzName { 78 | fn new(base_path: &Path, path: &Path) -> Option { 79 | let mut path = path.iter().fuse(); 80 | for _ in base_path { 81 | path.next(); 82 | } 83 | let a = path.next().and_then(|o| o.to_str()); 84 | let b = path.next().and_then(|o| o.to_str()); 85 | let c = path.next().and_then(|o| o.to_str()); 86 | match [a, b, c] { 87 | [Some("etc"), ..] => None, 88 | [Some(a), None, None] => Some(Self { 89 | canon: "".to_owned(), 90 | full: a.to_owned(), 91 | major: None, 92 | minor: prepare_casing(a).to_case(Case::UpperSnake), 93 | }), 94 | [Some(a), Some(b), None] => Some(Self { 95 | canon: "".to_owned(), 96 | full: format!("{a}/{b}"), 97 | major: Some(prepare_casing(a).to_case(Case::Snake)), 98 | minor: prepare_casing(b).to_case(Case::UpperSnake), 99 | }), 100 | [Some(a), Some(b), Some(c)] => Some(Self { 101 | canon: "".to_owned(), 102 | full: format!("{a}/{b}/{c}"), 103 | major: Some(prepare_casing(&format!("{a}/{b}")).to_case(Case::Snake)), 104 | minor: prepare_casing(c).to_case(Case::UpperSnake), 105 | }), 106 | _ => None, 107 | } 108 | } 109 | } 110 | 111 | pub fn main() -> anyhow::Result<()> { 112 | let mut args = args().into_iter().fuse(); 113 | let _ = args.next(); // exe path 114 | 115 | let target_dir = PathBuf::from(args.next().unwrap_or_else(|| "tzdb/generated".to_owned())); 116 | create_dir_all(&target_dir)?; 117 | 118 | let entries_by_bytes = collect_entries_by_bytes(&mut args)?; 119 | let entries_by_major = collect_entries_by_major(&entries_by_bytes)?; 120 | 121 | gen_mod(&mut args, &target_dir)?; 122 | // generate lookup table 123 | gen_lookup_table(&entries_by_bytes, &target_dir)?; 124 | // generate exhaustive by-name test 125 | gen_test_all_names(&entries_by_bytes, &target_dir)?; 126 | // all known time zones as reference to (raw_)tzdata 127 | gen_time_zones(&entries_by_major, &target_dir)?; 128 | // list of known time zone names 129 | gen_tz_names(entries_by_major, &target_dir)?; 130 | // parsed time zone data by canonical name 131 | gen_tzdata(&entries_by_bytes, &target_dir)?; 132 | // raw time zone data by canonical name 133 | gen_raw_tzdata(entries_by_bytes, &target_dir)?; 134 | 135 | Ok(()) 136 | } 137 | 138 | fn gen_mod(args: &mut impl Iterator, target_dir: &Path) -> Result<(), anyhow::Error> { 139 | let hash_file = args.next().unwrap_or_else(|| "tzdb.tar.lz.sha".to_owned()); 140 | let hash_file = read_to_string(&hash_file)?; 141 | let (hash, version) = hash_file 142 | .trim() 143 | .split_once(" ") 144 | .ok_or_else(|| anyhow!("Hash file {hash_file:?} malformed."))?; 145 | let version = version.rsplit_once('/').unwrap_or(("", version)).1; 146 | let version = version.split_once('.').unwrap_or((version, "")).0; 147 | let version = version.rsplit_once('-').unwrap_or(("", version)).1; 148 | 149 | let r = format!( 150 | r#"{GENERATED_FILE} 151 | // MIT No Attribution 152 | // 153 | // Copyright 2022-2024 René Kijewski 154 | // 155 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 156 | // associated documentation files (the "Software"), to deal in the Software without restriction, 157 | // including without limitation the rights to use, copy, modify, merge, publish, distribute, 158 | // sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 159 | // furnished to do so. 160 | // 161 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 162 | // NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 163 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 164 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 165 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 166 | 167 | #![allow(unknown_lints)] 168 | #![allow(clippy::pedantic)] 169 | 170 | #[cfg(all(test, not(miri)))] 171 | mod test_all_names; 172 | 173 | pub(crate) mod by_name; 174 | mod raw_tzdata; 175 | mod tzdata; 176 | mod tz_names; 177 | 178 | /// All defined time zones statically accessible 179 | pub mod time_zone; 180 | 181 | /// The version of the source Time Zone Database 182 | pub const VERSION: &str = {version:?}; 183 | 184 | /// The SHA512 hash of the source Time Zone Database (using the "Complete Distribution") 185 | pub const VERSION_HASH: &str = {hash:?}; 186 | 187 | #[allow(unreachable_pub)] // false positive 188 | pub use self::tz_names::TZ_NAMES; 189 | "# 190 | ); 191 | write_string(r, target_dir.join("mod.rs"))?; 192 | Ok(()) 193 | } 194 | 195 | fn collect_entries_by_major(entries_by_bytes: &IndexMap, Vec>) -> anyhow::Result, Vec<&TzName>)>> { 196 | let entries_by_major = entries_by_bytes 197 | .values() 198 | .flat_map(|entries| entries.iter()) 199 | .map(|tz_entry| (tz_entry.major.as_deref(), tz_entry)) 200 | .sorted_by(|(l, _), (r, _)| match (l, r) { 201 | (None, None) => Ordering::Equal, 202 | (None, Some(_)) => Ordering::Greater, 203 | (Some(_), None) => Ordering::Less, 204 | (Some(l), Some(r)) => l.cmp(r), 205 | }) 206 | .chunk_by(|(k, _)| k.map(|s| s.to_owned())) 207 | .into_iter() 208 | .map(|(major, entries)| { 209 | let mut entries = entries.map(|(_, e)| e).collect_vec(); 210 | entries.sort(); 211 | (major, entries) 212 | }) 213 | .collect_vec(); 214 | 215 | let max_len = entries_by_major 216 | .iter() 217 | .flat_map(|(_, entries)| entries.iter()) 218 | .map(|entry| entry.full.len()) 219 | .max() 220 | .unwrap(); 221 | assert!(max_len <= 32); 222 | 223 | Ok(entries_by_major) 224 | } 225 | 226 | fn collect_entries_by_bytes(args: &mut impl Iterator) -> anyhow::Result, Vec>> { 227 | let mut base_path = args 228 | .next() 229 | .unwrap_or_else(|| "/usr/share/zoneinfo/posix/".to_owned()); 230 | while base_path.ends_with('/') { 231 | if base_path.len() == 1 { 232 | break; 233 | } 234 | base_path.pop(); 235 | } 236 | if base_path.is_empty() { 237 | base_path.push('.'); 238 | } 239 | if !base_path.ends_with('/') { 240 | base_path.push('/'); 241 | } 242 | let base_path = Path::new(&base_path).canonicalize()?; 243 | 244 | let mut entries_by_bytes: IndexMap, Vec> = IndexMap::new(); 245 | let walkdir = WalkDir::new(&base_path) 246 | .min_depth(1) 247 | .contents_first(true) 248 | .follow_links(true); 249 | for entry in walkdir { 250 | let entry = entry?; 251 | if !entry.file_type().is_file() { 252 | continue; 253 | } 254 | let path = base_path.join(entry.path()); 255 | let Ok(bytes) = read(&path) else { 256 | continue; 257 | }; 258 | if !TimeZone::from_tz_data(&bytes).is_ok() { 259 | continue; 260 | } 261 | let Some(tz_entry) = TzName::new(&base_path, &path) else { 262 | continue; 263 | }; 264 | entries_by_bytes.entry(bytes).or_default().push(tz_entry); 265 | } 266 | for entries in entries_by_bytes.values_mut() { 267 | entries.sort(); 268 | let canon = prepare_casing(&entries.first().unwrap().full).to_case(Case::UpperSnake); 269 | for entry in entries { 270 | entry.canon = canon.clone(); 271 | } 272 | } 273 | entries_by_bytes.sort_by(|_, l, _, r| l[0].canon.cmp(&r[0].canon)); 274 | 275 | Ok(entries_by_bytes) 276 | } 277 | 278 | fn gen_raw_tzdata(entries_by_bytes: IndexMap, Vec>, target_dir: &Path) -> anyhow::Result<()> { 279 | let mut r = GENERATED_FILE.to_owned(); 280 | writeln!(r, "#![allow(unknown_lints)]")?; 281 | writeln!(r, "#![allow(clippy::octal_escapes)]")?; 282 | writeln!(r)?; 283 | for (bytes, entries) in &entries_by_bytes { 284 | writeln!( 285 | r, 286 | "pub(crate) const {}: &[u8] = b\"{}\";", 287 | &entries[0].canon, 288 | hex_encode(bytes)?, 289 | )?; 290 | } 291 | write_string(r, target_dir.join("raw_tzdata.rs"))?; 292 | Ok(()) 293 | } 294 | 295 | fn gen_tzdata(entries_by_bytes: &IndexMap, Vec>, target_dir: &Path) -> anyhow::Result<()> { 296 | let mut r = GENERATED_FILE.to_owned(); 297 | writeln!(r, "use tz::timezone::RuleDay;")?; 298 | writeln!(r, "use tz::timezone::TransitionRule;")?; 299 | writeln!(r, "use tz::TimeZoneRef;")?; 300 | writeln!(r)?; 301 | writeln!(r, "use crate::new_alternate_time;")?; 302 | writeln!(r, "use crate::new_local_time_type;")?; 303 | writeln!(r, "use crate::new_month_week_day;")?; 304 | writeln!(r, "use crate::new_time_zone_ref;")?; 305 | writeln!(r, "use crate::new_transition;")?; 306 | writeln!(r)?; 307 | for (bytes, entries) in entries_by_bytes { 308 | writeln!(r)?; 309 | writeln!( 310 | r, 311 | "pub(crate) const {}: TimeZoneRef<'static> = {};", 312 | &entries[0].canon, 313 | tz_convert(bytes), 314 | )?; 315 | } 316 | write_string(r, target_dir.join("tzdata.rs"))?; 317 | Ok(()) 318 | } 319 | 320 | fn gen_tz_names(entries_by_major: Vec<(Option, Vec<&TzName>)>, target_dir: &Path) -> anyhow::Result<()> { 321 | let mut time_zones_list = entries_by_major 322 | .iter() 323 | .flat_map(|(_, entries)| entries.iter()) 324 | .map(|entry| entry.full.as_str()) 325 | .collect_vec(); 326 | time_zones_list.sort_by_key(|l| l.to_ascii_lowercase()); 327 | 328 | let mut r = GENERATED_FILE.to_owned(); 329 | writeln!(r, "/// A list of all known time zones")?; 330 | writeln!(r, "pub const TZ_NAMES: &[&str] = &[",)?; 331 | for name in time_zones_list { 332 | writeln!(r, " {:?},", name)?; 333 | } 334 | writeln!(r, "];")?; 335 | write_string(r, target_dir.join("tz_names.rs"))?; 336 | Ok(()) 337 | } 338 | 339 | fn gen_time_zones(entries_by_major: &Vec<(Option, Vec<&TzName>)>, target_dir: &Path) -> anyhow::Result<()> { 340 | let mut r = GENERATED_FILE.to_owned(); 341 | for (folder, entries) in entries_by_major { 342 | if let Some(folder) = folder { 343 | let doc = entries[0].full.as_str(); 344 | let doc = match doc.rsplit_once('/') { 345 | Some((doc, _)) => doc, 346 | None => doc, 347 | }; 348 | writeln!(r, "/// {doc}")?; 349 | writeln!(r, "pub mod {folder} {{")?; 350 | } 351 | for entry in entries { 352 | writeln!(r, " /// Time zone data for `{:?}`", entry.full)?; 353 | writeln!( 354 | r, 355 | "pub const {}: tz::TimeZoneRef<'static> = crate::generated::tzdata::{};", 356 | entry.minor, entry.canon, 357 | )?; 358 | } 359 | 360 | for entry in entries { 361 | writeln!( 362 | r, 363 | " /// Raw, unparsed time zone data for `{:?}`", 364 | entry.full 365 | )?; 366 | writeln!( 367 | r, 368 | "pub const RAW_{}: &[u8] = crate::generated::raw_tzdata::{};", 369 | entry.minor, entry.canon, 370 | )?; 371 | } 372 | 373 | if folder.is_some() { 374 | writeln!(r, "}}")?; 375 | } 376 | } 377 | write_string(r, target_dir.join("time_zone.rs"))?; 378 | Ok(()) 379 | } 380 | 381 | fn gen_test_all_names(entries_by_bytes: &IndexMap, Vec>, target_dir: &Path) -> anyhow::Result<()> { 382 | let mut r = GENERATED_FILE.to_owned(); 383 | writeln!(r, "#[test]")?; 384 | writeln!(r, "fn test() {{")?; 385 | writeln!(r, " use crate::{{find_raw, find_tz, time_zone}};")?; 386 | writeln!(r)?; 387 | writeln!( 388 | r, 389 | " const TIME_ZONES: &[(&tz::TimeZoneRef<'static>, &[u8], &[&[u8]])] = &[" 390 | )?; 391 | for entries in entries_by_bytes.values() { 392 | for entry in entries { 393 | let name = match entry.major { 394 | Some(ref major) => format!("{}::{}", major, &entry.minor), 395 | None => format!("{}", &entry.minor), 396 | }; 397 | let raw_name = match entry.major { 398 | Some(ref major) => format!("{}::RAW_{}", major, &entry.minor), 399 | None => format!("RAW_{}", &entry.minor), 400 | }; 401 | 402 | writeln!(r, " (")?; 403 | writeln!(r, " &time_zone::{name},")?; 404 | writeln!(r, " time_zone::{raw_name},")?; 405 | writeln!(r, " &[")?; 406 | for f in [ 407 | |s: &str| s.to_owned(), 408 | |s: &str| s.to_ascii_lowercase(), 409 | |s: &str| s.to_ascii_uppercase(), 410 | |s: &str| { 411 | s.chars() 412 | .map(|c| match c { 413 | 'A'..='Z' => c.to_ascii_lowercase(), 414 | 'a'..='z' => c.to_ascii_uppercase(), 415 | c => c, 416 | }) 417 | .collect() 418 | }, 419 | |s: &str| { 420 | s.chars() 421 | .enumerate() 422 | .map(|(i, c)| { 423 | if i % 2 == 0 { 424 | c.to_ascii_uppercase() 425 | } else { 426 | c.to_ascii_lowercase() 427 | } 428 | }) 429 | .collect() 430 | }, 431 | |s: &str| { 432 | s.chars() 433 | .enumerate() 434 | .map(|(i, c)| { 435 | if i % 2 == 1 { 436 | c.to_ascii_uppercase() 437 | } else { 438 | c.to_ascii_lowercase() 439 | } 440 | }) 441 | .collect() 442 | }, 443 | ] { 444 | writeln!(r, " b{:?},", f(&entry.full))?; 445 | } 446 | writeln!(r, " ],")?; 447 | writeln!(r, " ),")?; 448 | } 449 | } 450 | writeln!(r, " ];")?; 451 | writeln!(r)?; 452 | writeln!( 453 | r, 454 | " for &(tz, raw, names) in TIME_ZONES {{ for name in names {{", 455 | )?; 456 | writeln!( 457 | r, 458 | " assert_eq!(Some(tz), find_tz(name), \"find_tz({{:?}})\", name);", 459 | )?; 460 | writeln!( 461 | r, 462 | " assert_eq!(Some(raw), find_raw(name), \"find_raw({{:?}})\", name);", 463 | )?; 464 | writeln!(r, " }} }}")?; 465 | writeln!(r, "}}")?; 466 | write_string(r, target_dir.join("test_all_names.rs"))?; 467 | Ok(()) 468 | } 469 | 470 | fn gen_lookup_table(entries_by_bytes: &IndexMap, Vec>, target_dir: &Path) -> anyhow::Result<()> { 471 | let mut keywords = String::new(); 472 | writeln!( 473 | keywords, 474 | "struct keyword {{ const char* name; const char* canon; }}" 475 | )?; 476 | writeln!(keywords, "%%")?; 477 | for entries in entries_by_bytes.values() { 478 | for entry in entries { 479 | writeln!(keywords, "{:?}, {:?}", entry.full, entry.canon)?; 480 | } 481 | } 482 | writeln!(keywords, "%%")?; 483 | let file = OpenOptions::new() 484 | .create(true) 485 | .write(true) 486 | .truncate(true) 487 | .open(target_dir.join("by_name.rs"))?; 488 | let mut gperf = Popen::create( 489 | &["/usr/bin/env", "python3", "generate_lookup_table.py"], 490 | PopenConfig { 491 | stdin: Redirection::Pipe, 492 | stdout: Redirection::File(file), 493 | cwd: Some(var_os("CARGO_MANIFEST_DIR").ok_or(anyhow!("!CARGO_MANIFEST_DIR"))?), 494 | ..PopenConfig::default() 495 | }, 496 | )?; 497 | gperf.communicate(Some(&keywords))?; 498 | Ok(()) 499 | } 500 | 501 | fn write_string(mut s: String, f: PathBuf) -> std::io::Result<()> { 502 | if !s.ends_with("\n") { 503 | s.push('\n'); 504 | } 505 | std::fs::OpenOptions::new() 506 | .create(true) 507 | .write(true) 508 | .truncate(true) 509 | .open(f)? 510 | .write_all(s.as_bytes()) 511 | } 512 | 513 | fn prepare_casing(name: &str) -> String { 514 | name.replace('/', " ") 515 | .replace("GMT+", " GMT plus ") 516 | .replace("GMT-", " GMT minus ") 517 | } 518 | 519 | fn tz_convert(bytes: &[u8]) -> crate::parse::TimeZone { 520 | let tz = TimeZone::from_tz_data(bytes).unwrap(); 521 | let s = format!("{:?}", tz); 522 | let s = s.replace('{', "("); 523 | let s = s.replace('}', ")"); 524 | ron::from_str::(&s).unwrap() 525 | } 526 | 527 | fn hex_encode(v: &[u8]) -> Result { 528 | let mut s = String::with_capacity(v.len() * 4); 529 | for &b in v { 530 | match b { 531 | 0 => s.push_str("\\0"), 532 | c @ (1..=31 | 127..=255 | b'"' | b'\\') => write!(s, "\\x{c:02x}")?, 533 | c @ 32..=126 => s.push(c as char), 534 | } 535 | } 536 | Ok(s) 537 | } 538 | --------------------------------------------------------------------------------