tzf2 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # tzf2
2
+
3
+ Offline latitude and longitude to IANA timezone lookup for Ruby. The gem wraps [tzf-rs](https://github.com/ringsaturn/tzf-rs) 2.0 and embeds current ocean-inclusive boundary data from [tzf-dist](https://github.com/ringsaturn/tzf-dist). Lookups do not use the network.
4
+
5
+ This gem is a drop-in successor to [`tzf`](https://github.com/HarlemSquirrel/tzf-rb) for Rails apps that already call `TZF.tz_name(lat, lng)`. The published gem name is `tzf2` so it does not collide with HarlemSquirrel's gem.
6
+
7
+ ## Install
8
+
9
+ You need Ruby 3.2 or newer (including 4.0).
10
+
11
+ Precompiled native gems ship for `x86_64-linux`, `aarch64-linux`, `x86_64-darwin`, and `arm64-darwin`. Other platforms compile from source and need clang plus Rust 1.88 or newer.
12
+
13
+ ```ruby
14
+ # Gemfile
15
+ gem "tzf2"
16
+ ```
17
+
18
+ Then run:
19
+
20
+ ```bash
21
+ bundle install
22
+ ```
23
+
24
+ `require "tzf2"` and `require "tzf"` both load the `TZF` module.
25
+
26
+ ## Usage
27
+
28
+ Arguments are latitude first, then longitude. That matches the existing `tzf` gem. It is the opposite of the Rust and Python APIs.
29
+
30
+ ```ruby
31
+ require "tzf2"
32
+
33
+ TZF.tz_name(40.7477, -73.9935)
34
+ # => "America/New_York"
35
+
36
+ TZF.tz_names(44.04, 87.416)
37
+ # => ["Asia/Shanghai", "Asia/Urumqi"]
38
+
39
+ TZF.data_version
40
+ # => "2026c"
41
+
42
+ TZF.engine_version
43
+ # => "2.0.0"
44
+ ```
45
+
46
+ `tz_name` returns one IANA identifier. `tz_names` returns every match, sorted, which matters on shared borders.
47
+
48
+ Invalid coordinates raise `TZF::InvalidCoordinatesError`.
49
+
50
+ ```ruby
51
+ TZF.tz_name(91, 0)
52
+ # TZF::InvalidCoordinatesError: latitude 91.0 is outside -90..90
53
+ ```
54
+
55
+ A valid point with no covering polygon raises `TZF::UncoveredCoordinateError`. Rescue that in the application if you need a fallback. This gem does not guess a neighbor zone.
56
+
57
+ ```ruby
58
+ TZF.tz_name(-54.1, -36.1)
59
+ # TZF::UncoveredCoordinateError: no timezone covers latitude -54.1, longitude -36.1
60
+ ```
61
+
62
+ `raw_tz_name` and `raw_tz_names` return the engine result without raising (`""` / `[]` on a miss).
63
+
64
+ ## Global coverage
65
+
66
+ The embedded dataset is timezone-boundary-builder `timezones-with-oceans` as packaged by tzf-dist release `2026c`. That product includes land zones, territorial waters, polar regions, and open-ocean `Etc/GMT*` zones. It does not tile the sphere without gaps.
67
+
68
+ Lite simplification and a few source/encoding slivers leave hairline holes. A 10-degree grid is fully covered in `spec/fixtures/differential_baseline.json`. A 0.1-degree walk is not. Full-precision `tzf-dist` data closes some lite holes and still misses others.
69
+
70
+ This gem returns `UncoveredCoordinateError` on those points. It does not snap to a neighbor.
71
+
72
+ This release reports 444 timezone names.
73
+
74
+ The default dataset is the lite `.tzb` file. Simplified boundaries stay within about 111 m of the full-precision border. See the [tzf-rs accuracy notes](https://github.com/ringsaturn/tzf-rs#accuracy).
75
+
76
+ ## Ocean timezone semantics
77
+
78
+ Open ocean uses POSIX-signed `Etc/GMT*` identifiers.
79
+
80
+ - `Etc/GMT+8` is UTC-08:00.
81
+ - `Etc/GMT-5` is UTC+05:00.
82
+
83
+ Point Nemo (`-48.876667, -123.393333`) returns `Etc/GMT+8`. Coastal points inside territorial waters keep the land zone.
84
+
85
+ ## Data provenance
86
+
87
+ | Layer | Source | License |
88
+ | --- | --- | --- |
89
+ | Ruby and Rust wrapper | this repository | MIT, see `LICENSE` |
90
+ | Lookup engine | [tzf-rs](https://github.com/ringsaturn/tzf-rs) 2.0.0 | MIT |
91
+ | Packed `.tzb` data | [tzf-dist](https://github.com/ringsaturn/tzf-dist) `2026c` | ODbL, see `LICENSE_DATA` |
92
+ | Original boundaries | [timezone-boundary-builder](https://github.com/evansiroky/timezone-boundary-builder) | ODbL |
93
+
94
+ The original database is built from OpenStreetMap. `NOTICE` names both upstreams.
95
+
96
+ ## Version reporting
97
+
98
+ - `TZF::VERSION` is this gem.
99
+ - `TZF.engine_version` is the compiled `tzf-rs` crate.
100
+ - `TZF.data_version` is the tzf-dist / timezone-boundary-builder release inside the binary.
101
+
102
+ Pin all three when you record a production lookup result.
103
+
104
+ ## Upgrades
105
+
106
+ 1. Bump `tzf-rs` in `ext/tzf2/Cargo.toml`.
107
+ 2. Run `cargo update -p tzf-rs` and `bundle exec rake compile`.
108
+ 3. Run `bundle exec rake spec`.
109
+ 4. Run `bundle exec ruby bin/differential`.
110
+ 5. If the report lists timezone-id, timezone-names, UTC-offset, newly covered, or uncovered points, decide whether the new data is intended.
111
+ 6. To accept the new table, run `bundle exec rake differential:write` and update `spec/fixtures/locations.yml`.
112
+ 7. Set `TZF.engine_version` in `ext/tzf2/src/lib.rs` to the new crate version.
113
+
114
+ ## Testing
115
+
116
+ ```bash
117
+ bin/setup
118
+ bundle exec rake
119
+ bundle exec rake differential
120
+ ```
121
+
122
+ Specs cover major cities, ocean zones including Point Nemo, polar and antimeridian points, shared borders, latitude/longitude order, invalid input, and thread safety.
123
+
124
+ A 10-degree world grid and a 0.1-degree world grid are looked up in a standalone Rust binary (`crates/grid_parity`) and again through `TZF.raw_tz_name` / `TZF.raw_tz_names`. The answers must match, including empty engine results. That checks the Ruby wrapper against tzf-rs, not against a pinned Ruby table.
125
+
126
+ The differential suite compares the current engine to `spec/fixtures/differential_baseline.json`. It reports timezone-id changes, all-match list changes, UTC-offset changes at `2026-01-15T12:00:00Z`, and points that gained or lost coverage.
127
+
128
+ ## Rollback
129
+
130
+ Keep the previous `tzf2` git SHA in the Gemfile if a data upgrade changes production answers.
131
+
132
+ ```ruby
133
+ gem "tzf2", github: "joseph-lozano/tzf-2-rb", ref: "<known-good-sha>"
134
+ ```
135
+
136
+ To go back to HarlemSquirrel's gem, restore `gem "tzf"` and `require "tzf"`. The call shape is the same. Answers can differ because that gem still tracks tzf-rs 1.x.
137
+
138
+ ## Supported platforms
139
+
140
+ CI compiles and tests Ruby 3.2, 3.3, 3.4, and 4.0 on Ubuntu, plus Ruby 3.4 on `ubuntu-24.04-arm`, `macos-15-intel`, and `macos-latest`.
141
+
142
+ Release builds precompiled gems for Linux x86_64, Linux ARM64, Intel Mac, and Apple Silicon. Windows is not supported.
143
+
144
+ ## License
145
+
146
+ MIT for the wrapper. ODbL for the embedded boundary database. See `LICENSE`, `LICENSE_DATA`, and `NOTICE`.
@@ -0,0 +1,16 @@
1
+ [package]
2
+ name = "tzf2"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ license = "MIT"
6
+ publish = false
7
+
8
+ [lib]
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ magnus = { version = "0.8" }
13
+ tzf-rs = { version = "2.0.0", default-features = false, features = ["bundled"] }
14
+ rb-sys = { version = "0.9", default-features = false, features = [
15
+ "stable-api-compiled-fallback",
16
+ ] }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("tzf2/tzf2")
@@ -0,0 +1,39 @@
1
+ use std::sync::LazyLock;
2
+
3
+ use magnus::{function, prelude::*, Error, Ruby};
4
+ use tzf_rs::DefaultFinder;
5
+
6
+ static FINDER: LazyLock<DefaultFinder> = LazyLock::new(DefaultFinder::new);
7
+
8
+ const ENGINE_VERSION: &str = "2.0.0";
9
+
10
+ fn raw_tz_name(lat: f64, lng: f64) -> &'static str {
11
+ FINDER.get_tz_name(lng, lat)
12
+ }
13
+
14
+ fn raw_tz_names(lat: f64, lng: f64) -> Vec<&'static str> {
15
+ FINDER.get_tz_names(lng, lat)
16
+ }
17
+
18
+ fn raw_data_version() -> &'static str {
19
+ FINDER.data_version()
20
+ }
21
+
22
+ fn raw_engine_version() -> &'static str {
23
+ ENGINE_VERSION
24
+ }
25
+
26
+ fn raw_timezone_names() -> Vec<&'static str> {
27
+ FINDER.timezonenames()
28
+ }
29
+
30
+ #[magnus::init]
31
+ fn init(ruby: &Ruby) -> Result<(), Error> {
32
+ let module = ruby.define_module("TZF")?;
33
+ module.define_singleton_method("raw_tz_name", function!(raw_tz_name, 2))?;
34
+ module.define_singleton_method("raw_tz_names", function!(raw_tz_names, 2))?;
35
+ module.define_singleton_method("raw_data_version", function!(raw_data_version, 0))?;
36
+ module.define_singleton_method("raw_engine_version", function!(raw_engine_version, 0))?;
37
+ module.define_singleton_method("raw_timezone_names", function!(raw_timezone_names, 0))?;
38
+ Ok(())
39
+ }
data/lib/tzf.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tzf2"
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TZF
4
+ class Coordinates
5
+ attr_reader :latitude, :longitude
6
+
7
+ def self.parse(latitude, longitude)
8
+ new(latitude, longitude)
9
+ end
10
+
11
+ def initialize(latitude, longitude)
12
+ @latitude = finite_float(latitude, "latitude")
13
+ @longitude = finite_float(longitude, "longitude")
14
+ unless (-90.0..90.0).cover?(@latitude)
15
+ raise InvalidCoordinatesError, "latitude #{@latitude} is outside -90..90"
16
+ end
17
+ unless (-180.0..180.0).cover?(@longitude)
18
+ raise InvalidCoordinatesError, "longitude #{@longitude} is outside -180..180"
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def finite_float(value, name)
25
+ unless value.is_a?(Numeric)
26
+ raise InvalidCoordinatesError, "#{name} must be numeric, got #{value.class}"
27
+ end
28
+
29
+ float = Float(value)
30
+ unless float.finite?
31
+ raise InvalidCoordinatesError, "#{name} must be a finite number"
32
+ end
33
+
34
+ float
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TZF
4
+ class Error < StandardError; end
5
+
6
+ class InvalidCoordinatesError < Error; end
7
+
8
+ class UncoveredCoordinateError < Error; end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TZF
4
+ VERSION = "0.0.1"
5
+ end
data/lib/tzf2.rb ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tzf2/version"
4
+ require_relative "tzf2/errors"
5
+ require_relative "tzf2/coordinates"
6
+
7
+ ruby_minor_version = RUBY_VERSION.split(".")[0..1].join(".")
8
+ versioned = File.join(__dir__, "tzf2", ruby_minor_version, "tzf2")
9
+ if File.exist?("#{versioned}.bundle") || File.exist?("#{versioned}.so")
10
+ require versioned
11
+ else
12
+ require_relative "tzf2/tzf2"
13
+ end
14
+
15
+ module TZF
16
+ class << self
17
+ def tz_name(latitude, longitude)
18
+ coords = Coordinates.parse(latitude, longitude)
19
+ name = raw_tz_name(coords.latitude, coords.longitude)
20
+ raise uncovered_error(coords) if name.empty?
21
+
22
+ name
23
+ end
24
+
25
+ def tz_names(latitude, longitude)
26
+ coords = Coordinates.parse(latitude, longitude)
27
+ names = raw_tz_names(coords.latitude, coords.longitude)
28
+ raise uncovered_error(coords) if names.empty?
29
+
30
+ names
31
+ end
32
+
33
+ def data_version
34
+ raw_data_version
35
+ end
36
+
37
+ def engine_version
38
+ raw_engine_version
39
+ end
40
+
41
+ def timezone_names
42
+ raw_timezone_names
43
+ end
44
+
45
+ private
46
+
47
+ def uncovered_error(coords)
48
+ UncoveredCoordinateError.new(
49
+ "no timezone covers latitude #{coords.latitude}, longitude #{coords.longitude}"
50
+ )
51
+ end
52
+ end
53
+ end
data/sig/tzf.rbs ADDED
@@ -0,0 +1,26 @@
1
+ module TZF
2
+ VERSION: String
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class InvalidCoordinatesError < Error
8
+ end
9
+
10
+ class UncoveredCoordinateError < Error
11
+ end
12
+
13
+ class Coordinates
14
+ attr_reader latitude: Float
15
+ attr_reader longitude: Float
16
+
17
+ def self.parse: (Numeric latitude, Numeric longitude) -> Coordinates
18
+ def initialize: (Numeric latitude, Numeric longitude) -> void
19
+ end
20
+
21
+ def self.tz_name: (Numeric latitude, Numeric longitude) -> String
22
+ def self.tz_names: (Numeric latitude, Numeric longitude) -> Array[String]
23
+ def self.data_version: () -> String
24
+ def self.engine_version: () -> String
25
+ def self.timezone_names: () -> Array[String]
26
+ end
data/tzf2.gemspec ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/tzf2/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "tzf2"
7
+ spec.version = TZF::VERSION
8
+ spec.authors = ["Joseph Lozano"]
9
+ spec.email = ["me@lozanojoseph.com"]
10
+
11
+ spec.summary = "Offline latitude/longitude to IANA timezone lookup via tzf-rs 2"
12
+ spec.description = <<~DESC
13
+ Ruby wrapper around tzf-rs v2 for offline timezone lookup from WGS84
14
+ coordinates. Embeds current ocean-inclusive timezone-boundary-builder data
15
+ from tzf-dist. No runtime network access.
16
+ DESC
17
+ spec.homepage = "https://github.com/joseph-lozano/tzf-2-rb"
18
+ spec.license = "MIT"
19
+ spec.required_ruby_version = ">= 3.2.0"
20
+ spec.required_rubygems_version = ">= 3.4.6"
21
+
22
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
23
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
24
+ spec.metadata["homepage_uri"] = spec.homepage
25
+ spec.metadata["rubygems_mfa_required"] = "true"
26
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
27
+
28
+ spec.files = %w[
29
+ CHANGELOG.md
30
+ Cargo.lock
31
+ Cargo.toml
32
+ LICENSE
33
+ LICENSE_DATA
34
+ NOTICE
35
+ README.md
36
+ ext/tzf2/Cargo.toml
37
+ ext/tzf2/extconf.rb
38
+ ext/tzf2/src/lib.rs
39
+ lib/tzf.rb
40
+ lib/tzf2.rb
41
+ lib/tzf2/coordinates.rb
42
+ lib/tzf2/errors.rb
43
+ lib/tzf2/version.rb
44
+ sig/tzf.rbs
45
+ tzf2.gemspec
46
+ ]
47
+ spec.bindir = "exe"
48
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
49
+ spec.require_paths = ["lib"]
50
+ spec.extensions = ["ext/tzf2/extconf.rb"]
51
+
52
+ spec.add_dependency "rb_sys", "~> 0.9.117"
53
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tzf2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Joseph Lozano
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rb_sys
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.9.117
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.9.117
26
+ description: |
27
+ Ruby wrapper around tzf-rs v2 for offline timezone lookup from WGS84
28
+ coordinates. Embeds current ocean-inclusive timezone-boundary-builder data
29
+ from tzf-dist. No runtime network access.
30
+ email:
31
+ - me@lozanojoseph.com
32
+ executables: []
33
+ extensions:
34
+ - ext/tzf2/extconf.rb
35
+ extra_rdoc_files: []
36
+ files:
37
+ - CHANGELOG.md
38
+ - Cargo.lock
39
+ - Cargo.toml
40
+ - LICENSE
41
+ - LICENSE_DATA
42
+ - NOTICE
43
+ - README.md
44
+ - ext/tzf2/Cargo.toml
45
+ - ext/tzf2/extconf.rb
46
+ - ext/tzf2/src/lib.rs
47
+ - lib/tzf.rb
48
+ - lib/tzf2.rb
49
+ - lib/tzf2/coordinates.rb
50
+ - lib/tzf2/errors.rb
51
+ - lib/tzf2/version.rb
52
+ - sig/tzf.rbs
53
+ - tzf2.gemspec
54
+ homepage: https://github.com/joseph-lozano/tzf-2-rb
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ bug_tracker_uri: https://github.com/joseph-lozano/tzf-2-rb/issues
59
+ changelog_uri: https://github.com/joseph-lozano/tzf-2-rb/blob/main/CHANGELOG.md
60
+ homepage_uri: https://github.com/joseph-lozano/tzf-2-rb
61
+ rubygems_mfa_required: 'true'
62
+ allowed_push_host: https://rubygems.org
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 3.2.0
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 3.4.6
76
+ requirements: []
77
+ rubygems_version: 3.6.9
78
+ specification_version: 4
79
+ summary: Offline latitude/longitude to IANA timezone lookup via tzf-rs 2
80
+ test_files: []