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 +7 -0
- data/.gitignore +12 -0
- data/AGENTS.md +63 -0
- data/CLAUDE.md +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +97 -0
- data/Rakefile +8 -0
- data/lib/zip_to_tz/converter.rb +65 -0
- data/lib/zip_to_tz/data/timezones_to_zipcodes.yml +83049 -0
- data/lib/zip_to_tz/errors.rb +7 -0
- data/lib/zip_to_tz/version.rb +5 -0
- data/lib/zip_to_tz.rb +25 -0
- data/sig/zip_to_tz.rbs +4 -0
- metadata +55 -0
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
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,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
|