geo_states 0.1.2 → 0.1.3
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 +4 -4
- data/README.md +64 -2
- data/Rakefile +3 -3
- data/config/geo_states.example.yml +14 -0
- data/lib/geo_states/config_loader.rb +88 -0
- data/lib/geo_states/data_loader.rb +18 -17
- data/lib/geo_states/version.rb +1 -1
- data/lib/geo_states.rb +37 -2
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b757132bddec9472e730189fa998a1ef255440bb12eec76f407c9242b07b685
|
|
4
|
+
data.tar.gz: 0b5f64fde99b17656d2ced4965c5f766dbcc9b9f533d7ee14389ce83e0c93baa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 29f3e3ccc65d2b2f7b5c03f2ec807e754dc4f3c3a4d79b940a2bb91dd32b485e7d5da378a62d511c7e3999cb14babdc661f0b310c7c053870dc4e49a81d897fc
|
|
7
|
+
data.tar.gz: b06009b62f7580a61cbaba751652c820963046b116d334c0208e45eec37316e45d031c8974caeffd6bb1051f88ed7f0ac28e2cb3bd25c03421526739099c3196
|
data/README.md
CHANGED
|
@@ -25,9 +25,9 @@ require "geo_states"
|
|
|
25
25
|
GeoStates.all_countries
|
|
26
26
|
# => [{:country=>"Mexico", :iso2=>"MX", :iso3=>"MEX", :states=>[...]}, ...]
|
|
27
27
|
|
|
28
|
-
# Find country by
|
|
28
|
+
# Find country by any identifier (ISO2, ISO3, M49, or name)
|
|
29
29
|
GeoStates.find_country("MX")
|
|
30
|
-
# => {:country=>"Mexico", :m49=>
|
|
30
|
+
# => {:country=>"Mexico", :m49=>"484", :iso3=>"MEX", :iso2=>"MX", :currency=>"MXN", ...}
|
|
31
31
|
|
|
32
32
|
# Find by ISO3, M49, or name
|
|
33
33
|
GeoStates.find_country_by_iso3("MEX")
|
|
@@ -37,6 +37,14 @@ GeoStates.find_country_by_name("Mexico")
|
|
|
37
37
|
# Get states for a country (by iso2, iso3, m49, or name)
|
|
38
38
|
GeoStates.states_for("MX")
|
|
39
39
|
# => [{:name=>"Aguascalientes", :state_code=>"AGU", :iso3=>"AGU", :iso2=>"AG"}, ...]
|
|
40
|
+
|
|
41
|
+
# Find a specific state
|
|
42
|
+
GeoStates.find_state("MX", "CMX")
|
|
43
|
+
# => {:name=>"Mexico City", :state_code=>"CMX", :iso3=>"CMX", :iso2=>"CM"}
|
|
44
|
+
|
|
45
|
+
# With user config: CDMX, DF, etc. resolve to Mexico City (see User config below)
|
|
46
|
+
GeoStates.find_state("MX", "CDMX")
|
|
47
|
+
# => {:name=>"Mexico City", :state_code=>"CMX", ...}
|
|
40
48
|
```
|
|
41
49
|
|
|
42
50
|
### Country format
|
|
@@ -57,6 +65,60 @@ Each state has:
|
|
|
57
65
|
- `:iso3` — same as state_code if not provided
|
|
58
66
|
- `:iso2` — derived from state_code if not provided
|
|
59
67
|
|
|
68
|
+
### Example: what the data looks like
|
|
69
|
+
|
|
70
|
+
**Country** (e.g. `GeoStates.find_country("MX")`):
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
{
|
|
74
|
+
country: "Mexico",
|
|
75
|
+
m49: "484",
|
|
76
|
+
iso3: "MEX",
|
|
77
|
+
iso2: "MX",
|
|
78
|
+
currency: "MXN",
|
|
79
|
+
currency_symbol: "$",
|
|
80
|
+
phone_code: "+52",
|
|
81
|
+
flag: "https://upload.wikimedia.org/wikipedia/commons/f/fc/Flag_of_Mexico.svg",
|
|
82
|
+
states: [
|
|
83
|
+
{ name: "Aguascalientes", state_code: "AGU", iso3: "AGU", iso2: "AG" },
|
|
84
|
+
{ name: "Baja California", state_code: "BCN", iso3: "BCN", iso2: "BC" },
|
|
85
|
+
# ... 30 more states
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**States** (e.g. `GeoStates.states_for("MX")` — first two):
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
[
|
|
94
|
+
{ name: "Aguascalientes", state_code: "AGU", iso3: "AGU", iso2: "AG" },
|
|
95
|
+
{ name: "Baja California", state_code: "BCN", iso3: "BCN", iso2: "BC" }
|
|
96
|
+
]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### User config for state aliases
|
|
100
|
+
|
|
101
|
+
You can add a YAML file to define aliases (e.g. CDMX, DF for Mexico City):
|
|
102
|
+
|
|
103
|
+
1. Create `config/geo_states.yml` or `geo_states.yml` in your project root
|
|
104
|
+
2. Or set `GEO_STATES_CONFIG_PATH` to the config file path
|
|
105
|
+
|
|
106
|
+
Example `config/geo_states.yml`:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
MX:
|
|
110
|
+
state_aliases:
|
|
111
|
+
CDMX: DF
|
|
112
|
+
DF: DF
|
|
113
|
+
DISTRITO FEDERAL: DF
|
|
114
|
+
CIUDAD DE MEXICO: DF
|
|
115
|
+
MEXICO CITY: DF
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Values map to the `state_code` in the bundled data. For Mexico City the data uses `CMX`; `DF` is a built-in alias that maps to `CMX`. So `CDMX` → `DF` → `CMX` works when you have the config.
|
|
119
|
+
|
|
120
|
+
See `config/geo_states.example.yml` for a copy-paste template.
|
|
121
|
+
|
|
60
122
|
## Data sources
|
|
61
123
|
|
|
62
124
|
The bundled data in `lib/geo_states/data/countries.json` is the single source. It includes:
|
data/Rakefile
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
-
Dir.glob(
|
|
8
|
+
Dir.glob('lib/tasks/**/*.rake').each { |r| load r }
|
|
9
9
|
|
|
10
10
|
task default: :spec
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# GeoStates: user config for state aliases
|
|
2
|
+
# Copy to config/geo_states.yml or geo_states.yml in your project root.
|
|
3
|
+
# Or set GEO_STATES_CONFIG_PATH to the config file path.
|
|
4
|
+
#
|
|
5
|
+
# Values must map to state_code in bundled data (e.g. CMX for Mexico City).
|
|
6
|
+
# DF is supported as built-in alias for Mexico City (CMX).
|
|
7
|
+
|
|
8
|
+
MX:
|
|
9
|
+
state_aliases:
|
|
10
|
+
CDMX: DF
|
|
11
|
+
DF: DF
|
|
12
|
+
DISTRITO FEDERAL: DF
|
|
13
|
+
CIUDAD DE MEXICO: DF
|
|
14
|
+
MEXICO CITY: DF
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module GeoStates
|
|
6
|
+
# Loads user-provided YAML config for state aliases (e.g. CDMX => CMX for Mexico).
|
|
7
|
+
module ConfigLoader
|
|
8
|
+
CONFIG_FILENAMES = %w[geo_states.yml geo_states.yaml].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# @return [Hash] country_iso2 => { alias => state_code }
|
|
12
|
+
def state_aliases
|
|
13
|
+
@state_aliases ||= load_state_aliases
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Reset cached config (useful for tests).
|
|
17
|
+
def reset!
|
|
18
|
+
@state_aliases = nil
|
|
19
|
+
@config_path = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param path [String] Explicit config file path
|
|
23
|
+
def config_path=(path)
|
|
24
|
+
@config_path = path
|
|
25
|
+
@state_aliases = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Resolve state identifier via user config. Returns canonical state_code or nil.
|
|
29
|
+
# @param country_iso2 [String] e.g. "MX"
|
|
30
|
+
# @param state_id [String] e.g. "CDMX", "DF"
|
|
31
|
+
# @return [String, nil] resolved state_code (e.g. "CMX") or nil
|
|
32
|
+
def resolve_alias(country_iso2, state_id)
|
|
33
|
+
return nil if country_iso2.nil? || state_id.nil?
|
|
34
|
+
|
|
35
|
+
iso2 = country_iso2.to_s.upcase
|
|
36
|
+
id = state_id.to_s.strip
|
|
37
|
+
|
|
38
|
+
aliases = state_aliases[iso2]
|
|
39
|
+
return nil unless aliases.is_a?(Hash)
|
|
40
|
+
|
|
41
|
+
# Support both flat and nested structure:
|
|
42
|
+
# MX: { CDMX: CMX } or MX: { state_aliases: { CDMX: CMX } }
|
|
43
|
+
map = aliases['state_aliases'] || aliases
|
|
44
|
+
return nil unless map.is_a?(Hash)
|
|
45
|
+
|
|
46
|
+
val = map[id] || map[id.upcase]
|
|
47
|
+
val.to_s if val
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def load_state_aliases
|
|
53
|
+
path = locate_config
|
|
54
|
+
return {} unless path && File.file?(path)
|
|
55
|
+
|
|
56
|
+
raw = YAML.load_file(path)
|
|
57
|
+
return {} unless raw.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
raw.transform_keys { |k| k.to_s.upcase }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def locate_config
|
|
63
|
+
return @config_path if defined?(@config_path) && @config_path
|
|
64
|
+
|
|
65
|
+
if ENV['GEO_STATES_CONFIG_PATH']
|
|
66
|
+
p = ENV['GEO_STATES_CONFIG_PATH'].strip
|
|
67
|
+
return p if !p.empty? && File.file?(p)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
search_paths.each do |dir|
|
|
71
|
+
CONFIG_FILENAMES.each do |name|
|
|
72
|
+
path = File.join(dir, name)
|
|
73
|
+
return path if File.file?(path)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def search_paths
|
|
81
|
+
paths = []
|
|
82
|
+
paths << Dir.pwd
|
|
83
|
+
paths << File.join(Dir.pwd, 'config') if Dir.pwd
|
|
84
|
+
paths.uniq
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'json'
|
|
4
4
|
|
|
5
5
|
module GeoStates
|
|
6
6
|
# Loads country/state data from bundled JSON (no external API at runtime).
|
|
7
7
|
module DataLoader
|
|
8
|
-
DATA_DIR = File.expand_path(
|
|
9
|
-
COUNTRIES_FILE = File.join(DATA_DIR,
|
|
8
|
+
DATA_DIR = File.expand_path('data', __dir__)
|
|
9
|
+
COUNTRIES_FILE = File.join(DATA_DIR, 'countries.json')
|
|
10
10
|
|
|
11
11
|
class << self
|
|
12
12
|
def load_countries
|
|
@@ -35,35 +35,36 @@ module GeoStates
|
|
|
35
35
|
def find_by_m49(m49)
|
|
36
36
|
code = m49.to_s.strip
|
|
37
37
|
return nil if code.empty?
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
code = code.rjust(3, '0') if code.length < 3
|
|
39
40
|
countries.find { |c| c[:m49].to_s == code }
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
private
|
|
43
44
|
|
|
44
45
|
def normalize_country(raw)
|
|
45
|
-
states = (raw[
|
|
46
|
+
states = (raw['states'] || []).map { |s| normalize_state(s) }
|
|
46
47
|
{
|
|
47
|
-
country: raw[
|
|
48
|
-
m49: raw[
|
|
49
|
-
iso3: raw[
|
|
50
|
-
iso2: raw[
|
|
51
|
-
currency: raw[
|
|
52
|
-
currency_symbol: raw[
|
|
53
|
-
phone_code: raw[
|
|
54
|
-
flag: raw[
|
|
48
|
+
country: raw['name'],
|
|
49
|
+
m49: raw['m49'],
|
|
50
|
+
iso3: raw['iso3'],
|
|
51
|
+
iso2: raw['iso2'],
|
|
52
|
+
currency: raw['currency'],
|
|
53
|
+
currency_symbol: raw['currency_symbol'],
|
|
54
|
+
phone_code: raw['phone_code'],
|
|
55
|
+
flag: raw['flag'],
|
|
55
56
|
states: states
|
|
56
57
|
}
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
def normalize_state(raw)
|
|
60
|
-
code = raw[
|
|
61
|
+
code = raw['state_code'].to_s
|
|
61
62
|
iso2 = code.length >= 2 ? code[0, 2].upcase : code.upcase
|
|
62
63
|
{
|
|
63
|
-
name: raw[
|
|
64
|
+
name: raw['name'],
|
|
64
65
|
state_code: code,
|
|
65
|
-
iso3: raw[
|
|
66
|
-
iso2: raw[
|
|
66
|
+
iso3: raw['iso3'] || code,
|
|
67
|
+
iso2: raw['iso2'] || iso2
|
|
67
68
|
}
|
|
68
69
|
end
|
|
69
70
|
end
|
data/lib/geo_states/version.rb
CHANGED
data/lib/geo_states.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
|
|
3
|
+
require_relative 'geo_states/version'
|
|
4
|
+
require_relative 'geo_states/config_loader'
|
|
5
|
+
require 'geo_states/data_loader'
|
|
5
6
|
|
|
6
7
|
module GeoStates
|
|
7
8
|
class Error < StandardError; end
|
|
@@ -57,4 +58,38 @@ module GeoStates
|
|
|
57
58
|
country = find_country(identifier)
|
|
58
59
|
country&.dig(:states) || []
|
|
59
60
|
end
|
|
61
|
+
|
|
62
|
+
# Find a state within a country. Accepts user YAML config for aliases (e.g. CDMX => CMX).
|
|
63
|
+
# Config file: config/geo_states.yml or geo_states.yml, or GEO_STATES_CONFIG_PATH env.
|
|
64
|
+
# @param country_id [String] ISO2, ISO3, M49, or country name (e.g. "MX", "Mexico")
|
|
65
|
+
# @param state_id [String] state_code, name, or alias from user config (e.g. "CMX", "CDMX", "Mexico City")
|
|
66
|
+
# @return [Hash, nil] state hash with :name, :state_code, :iso2, :iso3, or nil
|
|
67
|
+
def self.find_state(country_id, state_id)
|
|
68
|
+
country = find_country(country_id)
|
|
69
|
+
return nil unless country
|
|
70
|
+
|
|
71
|
+
canonical = resolve_state_id(country[:iso2], state_id.to_s)
|
|
72
|
+
return nil if canonical.nil? || canonical.empty?
|
|
73
|
+
|
|
74
|
+
states = country[:states] || []
|
|
75
|
+
states.find do |s|
|
|
76
|
+
s[:state_code].to_s.casecmp(canonical) == 0 ||
|
|
77
|
+
s[:name].to_s.casecmp(canonical) == 0
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Resolve state identifier: user config first, then built-in aliases (e.g. DF -> CMX for Mexico).
|
|
82
|
+
# @param iso2 [String] country ISO2
|
|
83
|
+
# @param state_id [String] user input
|
|
84
|
+
# @return [String] canonical state_code for lookup
|
|
85
|
+
def self.resolve_state_id(iso2, state_id)
|
|
86
|
+
id = state_id.to_s.strip
|
|
87
|
+
return nil if id.empty?
|
|
88
|
+
|
|
89
|
+
canonical = ConfigLoader.resolve_alias(iso2, id) || id
|
|
90
|
+
# Built-in: Mexico DF -> CMX (bundled data uses CMX; user config may map CDMX => DF)
|
|
91
|
+
return 'CMX' if iso2.to_s.upcase == 'MX' && canonical.to_s.upcase == 'DF'
|
|
92
|
+
|
|
93
|
+
canonical
|
|
94
|
+
end
|
|
60
95
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: geo_states
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alberto Osnaya (@elosnaya)
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Lists countries and their states (or provinces, regions) in a consistent
|
|
14
14
|
format. Data is bundled with the gem.
|
|
@@ -21,9 +21,11 @@ files:
|
|
|
21
21
|
- LICENSE.txt
|
|
22
22
|
- README.md
|
|
23
23
|
- Rakefile
|
|
24
|
+
- config/geo_states.example.yml
|
|
24
25
|
- geo_states-0.1.0.gem
|
|
25
26
|
- geo_states/data/countries.json
|
|
26
27
|
- lib/geo_states.rb
|
|
28
|
+
- lib/geo_states/config_loader.rb
|
|
27
29
|
- lib/geo_states/data/countries.json
|
|
28
30
|
- lib/geo_states/data_loader.rb
|
|
29
31
|
- lib/geo_states/version.rb
|