zip_to_tz 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '0681e9467076100fc6da8cfdc5f163d7cfa47901c374b35398a59d22cc275ed5'
4
+ data.tar.gz: 7d0c91741d0b2e5bd31804601bed3fae9caaad4c35d13ae44c9267918980fed8
5
+ SHA512:
6
+ metadata.gz: ac0db3eff352bc9ba29a12b4bec7423de3ace46de24b32e81dfa1d2cf57528a6400486cb8faa4a8bffc34da2531070e681b9fff7da77906f7b2ada9378ab6e54
7
+ data.tar.gz: b47a7273adc93a573e74ef66ea4930d7be75fc332d0378145cd653126ffc83dfa5eaba01c4ab115f6f96f8a527fdf7b63f0fe43ccffd1cd397ecfb8fe45a04a0
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /.rspec_status
10
+ Gemfile.lock
11
+ pkg/
12
+ *.gem
data/AGENTS.md ADDED
@@ -0,0 +1,63 @@
1
+ # AGENTS.md
2
+
3
+ ## What this is
4
+
5
+ `zip_to_tz` is a Ruby gem, a port of the Node.js [zipToTz](https://github.com/pmmonier/zipToTz)
6
+ library. It maps a US zip code to a timezone — either the full IANA name (`America/New_York`) or
7
+ a short abbreviation (`EDT`).
8
+
9
+ ## Structure
10
+
11
+ - `lib/zip_to_tz.rb` — module entry point. `ZipToTz.full`/`ZipToTz.short` are convenience methods
12
+ backed by one memoized `Converter` instance.
13
+ - `lib/zip_to_tz/converter.rb` — `ZipToTz::Converter`, the actual lookup logic.
14
+ - `lib/zip_to_tz/errors.rb` — `ZipToTz::Error` and its two subclasses, `InvalidZipCodeError` and
15
+ `NotFoundError`.
16
+ - `lib/zip_to_tz/data/timezones_to_zipcodes.yml` — the only data file. Maps each IANA timezone
17
+ name to its list of zip codes (~83k entries total). `Converter` inverts this into a
18
+ zip → timezone hash on first use, cached at the class level (`Converter.index`), shared by all
19
+ instances.
20
+ - `spec/` — RSpec tests, mirrors `lib/` layout (`spec/zip_to_tz_spec.rb` for the module-level API,
21
+ `spec/zip_to_tz/converter_spec.rb` for the `Converter` class).
22
+
23
+ ## How `short` works
24
+
25
+ There is no separate short-name data file. `Converter::ABBREVIATIONS` is a small hard-coded table
26
+ (one entry per timezone actually present in the dataset) mapping full IANA name → abbreviation.
27
+ `#short` calls `#full` and looks up the result in that table, so the two methods can never
28
+ disagree. (Upstream ships two independently-maintained YAML files for this and they had drifted
29
+ out of sync for one zip code — that's why this port collapses them into one source of truth.)
30
+
31
+ The abbreviations are static per-zone, not date-aware — e.g. `America/New_York` is always `EDT`
32
+ here, never `EST`, matching upstream's behavior. This is a known simplification, not a bug to fix
33
+ unless asked.
34
+
35
+ ## Dev workflow
36
+
37
+ ```bash
38
+ bundle install # install dependencies
39
+ bundle exec rspec # run the test suite
40
+ gem build zip_to_tz.gemspec # build the .gem
41
+ gem install ./zip_to_tz-<version>.gem # install it locally
42
+ ```
43
+
44
+ Run `bundle exec rspec` after any change to `lib/` or the data file before considering the work
45
+ done — the converter spec includes a full-dataset consistency check (`short` vs `full` for every
46
+ one of the ~83k zip codes), which is cheap to run and catches data regressions immediately.
47
+
48
+ ## Data file gotchas
49
+
50
+ The YAML data mixes quoted (`'00100'`) and unquoted (`00108`) zip-code scalars, carried over from
51
+ upstream. This matters because YAML 1.1 parsers can misread a leading-zero numeric-looking scalar
52
+ as an octal integer. It happens to be safe as-is: every unquoted entry in the current file fails
53
+ Ruby's octal pattern (it contains an 8 or 9), so Psych always resolves it back to a string. If the
54
+ data file is ever regenerated or replaced, re-verify this — a quick way is to round-trip every zip
55
+ in the file through `Converter#full` and assert the key format survives (`\A\d{5}\z`).
56
+
57
+ ## Error semantics (must stay compatible with upstream's intent)
58
+
59
+ - Non-5-digit or non-numeric input → `ZipToTz::InvalidZipCodeError` ("Invalid format or zipCode
60
+ length" in upstream).
61
+ - Valid-format zip with no mapping → `ZipToTz::NotFoundError` ("Not found" in upstream).
62
+
63
+ Both inherit from `ZipToTz::Error`.
data/CLAUDE.md ADDED
@@ -0,0 +1,14 @@
1
+ @AGENTS.md
2
+
3
+ ## Claude-specific instructions
4
+
5
+ - Don't add error handling, validation, or abstractions beyond what's described in AGENTS.md —
6
+ this is a small, deliberately minimal port. Three similar lines beat a premature helper.
7
+ - Default to no comments in code; the one exception already made (`Converter::ABBREVIATIONS`) is
8
+ there because the reasoning (why these specific 7 zones, why static per-zone) isn't obvious
9
+ from the code alone.
10
+ - Never regenerate or hand-edit `lib/zip_to_tz/data/timezones_to_zipcodes.yml` without re-running
11
+ the round-trip check described in AGENTS.md's "Data file gotchas" section — a bad edit here
12
+ fails silently (wrong timezone returned) rather than raising.
13
+ - Before reporting any change to `lib/` or the data file as complete, run `bundle exec rspec` and
14
+ confirm all examples pass, including the full-dataset consistency spec.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2026 Jeff Miller
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to
7
+ deal in the Software without restriction, including without limitation the
8
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9
+ sell copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
+ DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # ZipToTz
2
+
3
+ A Ruby port of the Node.js [zipToTz](https://github.com/pmmonier/zipToTz) library. Looks up the
4
+ IANA timezone name or abbreviation for a US zip code, using an updated (2020) zip code list.
5
+
6
+ ## Installation
7
+
8
+ Install the gem and add to the application's Gemfile by executing:
9
+
10
+ ```bash
11
+ bundle add zip_to_tz
12
+ ```
13
+
14
+ If bundler is not being used to manage dependencies, install the gem by executing:
15
+
16
+ ```bash
17
+ gem install zip_to_tz
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```ruby
23
+ require "zip_to_tz"
24
+
25
+ ZipToTz.full("33487") # => "America/New_York"
26
+ ZipToTz.short("33487") # => "EDT"
27
+ ```
28
+
29
+ Or use the `Converter` class directly:
30
+
31
+ ```ruby
32
+ converter = ZipToTz::Converter.new
33
+ converter.full("33487") # => "America/New_York"
34
+ converter.short("33487") # => "EDT"
35
+ ```
36
+
37
+ ### Input format
38
+
39
+ The zip code must be exactly 5 digits (whitespace is stripped first). ZIP+4 codes are not
40
+ truncated — `"33487-1234"` raises `InvalidZipCodeError` rather than being treated as `"33487"`.
41
+
42
+ ### Supported timezones
43
+
44
+ Only US zip codes are covered, spanning these seven zones:
45
+
46
+ | IANA name | Abbreviation |
47
+ | ---------------------- | ------------ |
48
+ | `America/New_York` | `EDT` |
49
+ | `America/Chicago` | `CDT` |
50
+ | `America/Denver` | `MDT` |
51
+ | `America/Los_Angeles` | `PDT` |
52
+ | `America/Phoenix` | `MST` |
53
+ | `America/Anchorage` | `AKDT` |
54
+ | `Pacific/Honolulu` | `HST` |
55
+
56
+ `short` always returns the abbreviation above, even in months when the zone is actually on
57
+ standard time — e.g. `ZipToTz.short("33487")` returns `EDT` year-round, never `EST`. Arizona and
58
+ Hawaii don't observe daylight saving, so their zones use a standard-time abbreviation instead.
59
+ This is a known simplification inherited from upstream, not a date-aware lookup.
60
+
61
+ ### Errors
62
+
63
+ - `ZipToTz::InvalidZipCodeError` — raised if the input isn't exactly 5 digits.
64
+ - `ZipToTz::NotFoundError` — raised if the zip code has no timezone mapping.
65
+
66
+ Both inherit from `ZipToTz::Error`.
67
+
68
+ ```ruby
69
+ begin
70
+ ZipToTz.full("abc")
71
+ rescue ZipToTz::Error => e
72
+ # handle error
73
+ end
74
+ ```
75
+
76
+ ### Data
77
+
78
+ `short` is derived from `full`, so both are always consistent for a given zip code — see
79
+ [AGENTS.md](AGENTS.md) for why and how.
80
+
81
+ ## Development
82
+
83
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to
84
+ run the tests. You can also run `bin/console` for an interactive prompt that will allow you to
85
+ experiment.
86
+
87
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new
88
+ version, update the version number in `version.rb`, and then run `bundle exec rake release`.
89
+
90
+ ## License
91
+
92
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
93
+
94
+ ## Credit
95
+
96
+ Ported from the original JavaScript/TypeScript implementation by
97
+ [pmmonier](https://github.com/pmmonier/zipToTz).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module ZipToTz
6
+ # Looks up IANA timezone names or abbreviations for US zip codes.
7
+ #
8
+ # tz = ZipToTz::Converter.new
9
+ # tz.full("33487") # => "America/New_York"
10
+ # tz.short("33487") # => "EDT"
11
+ class Converter
12
+ DATA_FILE = File.expand_path("data/timezones_to_zipcodes.yml", __dir__)
13
+ ZIP_FORMAT = /\A\d{5}\z/
14
+
15
+ # The upstream zip list only spans the timezones below, each pinned to a
16
+ # single abbreviation (Arizona and Hawaii don't observe daylight saving,
17
+ # so their zones keep a standard-time abbreviation year-round).
18
+ ABBREVIATIONS = {
19
+ "America/New_York" => "EDT",
20
+ "America/Chicago" => "CDT",
21
+ "America/Denver" => "MDT",
22
+ "America/Los_Angeles" => "PDT",
23
+ "America/Phoenix" => "MST",
24
+ "Pacific/Honolulu" => "HST",
25
+ "America/Anchorage" => "AKDT"
26
+ }.freeze
27
+
28
+ def full(zip)
29
+ self.class.index.fetch(normalize(zip)) { raise NotFoundError, "Not found" }
30
+ end
31
+
32
+ def short(zip)
33
+ timezone = full(zip)
34
+ ABBREVIATIONS.fetch(timezone) { raise NotFoundError, "Not found" }
35
+ end
36
+
37
+ private
38
+
39
+ def normalize(zip)
40
+ value = zip.to_s.gsub(/\s/, "")
41
+ raise InvalidZipCodeError, "Invalid format or zipCode length" unless value.match?(ZIP_FORMAT)
42
+
43
+ value
44
+ end
45
+
46
+ class << self
47
+ # Shared across all instances: the zip list is static, so there's no
48
+ # reason for every Converter.new to re-parse and re-index the data.
49
+ def index
50
+ @index ||= build_index
51
+ end
52
+
53
+ private
54
+
55
+ def build_index
56
+ data = YAML.safe_load_file(DATA_FILE, permitted_classes: [Integer])
57
+ index = {}
58
+ data.each do |timezone, zips|
59
+ zips.each { |zip| index[zip.to_s] = timezone }
60
+ end
61
+ index.freeze
62
+ end
63
+ end
64
+ end
65
+ end