civitas 1.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE.txt +22 -0
- data/README.md +173 -0
- data/Rakefile +2 -0
- data/civitas.gemspec +32 -0
- data/lib/civitas/version.rb +3 -0
- data/lib/civitas.rb +353 -0
- data/lib/db/GeoLite2-City-Locations-en.csv +114875 -0
- data/lib/db/cities.us +22186 -0
- data/lib/db/countries.yml +252 -0
- data/lib/db/states-replace.yml +3 -0
- data/lib/db/states.us +52 -0
- data/spec/city_state_spec.rb +31 -0
- data/spec/spec_helper.rb +1 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 28037fc6995f149ecd6fcd77de06bb73314f350543cff25588566d14e4a1b5be
|
4
|
+
data.tar.gz: c3491d5efd566a0935fb25fb806b7664e597998d0f2dbdeedd2a39534f9fd3c6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 904e5eb9643e640adfdebcce66fe1e30515c7bd97874dda3dbf387d10c4e6a8b60fa3fbe0bce858770807ff8567e72d6b4cb79672ca60c099a6407ceac337763
|
7
|
+
data.tar.gz: 9e77e35ec9febda0aed5284b6acdb4a4550742e05e84f1dbd76ffb993da7635decc31b31b815af6c12b6b1e9a3309e39d65f0960c483894848180b31a9b2ca37
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [1.1.0] - 2023-08-14
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Ruby 3 support.
|
12
|
+
- Development dependencies: `rspec 3.10`.
|
13
|
+
- Unit tests with RSpec.
|
14
|
+
|
15
|
+
### Removed
|
16
|
+
- Ruby 2.5 support. Minimum Ruby version is now 2.6.0.
|
17
|
+
- `Gemfile.lock` from version control.
|
18
|
+
|
19
|
+
### Changed
|
20
|
+
- Update bundled MaxMind database.
|
21
|
+
- Upgrade runtime dependencies: minimum Rake is 11.0, minimum Rubyzip is 2.3.
|
22
|
+
|
23
|
+
## [0.1.0] - 2020-03-25
|
24
|
+
|
25
|
+
### Added
|
26
|
+
- Methods `set_license_key(license_key)` and `set_maxmind_zip_url(url)`.
|
27
|
+
|
28
|
+
### Removed
|
29
|
+
- Rails-specific code for broader Ruby compatibility.
|
30
|
+
|
31
|
+
### Changed
|
32
|
+
- Improve methods for renaming and adding missing cities.
|
33
|
+
- Update bundled MaxMind database.
|
34
|
+
- Upgrade dependencies for security and compatibility.
|
35
|
+
|
36
|
+
### Fixed
|
37
|
+
- Duplicated city entries. `CS.cities(:CA, :US)` multiple `Burbank` entries.
|
38
|
+
- Calling `CS.cities(nil)` returns random values.
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2023 Daniel Loureiro
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
# Civitas Ruby Gem
|
2
|
+
|
3
|
+
The `civitas` gem offers a straightforward way to retrieve lists of states for any given country and cities for any state. It's built on the MaxMind database, making it a reliable source for such data.
|
4
|
+
|
5
|
+
## Compatibility
|
6
|
+
|
7
|
+
This branch (`main`) is compatible with **Ruby 3 and higher**. If you are using Ruby 2, please refer to the `v0` branch.
|
8
|
+
|
9
|
+
### Ruby 3 and Higher
|
10
|
+
|
11
|
+
- This is the primary development branch.
|
12
|
+
- New features and improvements will be added here.
|
13
|
+
- Ensure you have Ruby 3 or higher to use the versions from this branch.
|
14
|
+
|
15
|
+
### Ruby 2 Support
|
16
|
+
|
17
|
+
- For Ruby 2 support, please refer to the `v0` branch.
|
18
|
+
- The `v0` branch is in maintenance mode, which means it will only receive bug fixes and will not get any new features.
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Add the gem to your Gemfile:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'civitas'
|
26
|
+
```
|
27
|
+
|
28
|
+
Then, run:
|
29
|
+
|
30
|
+
```bash
|
31
|
+
$ bundle install
|
32
|
+
```
|
33
|
+
|
34
|
+
## Listing States:
|
35
|
+
|
36
|
+
Retrieve a list of states for a specified country:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
CS.states(:US)
|
40
|
+
```
|
41
|
+
**Note:** The gem is case-insensitive. You can use variations like `:US`, `:us`, `:Us`, `"us"`, and `"US"`.
|
42
|
+
|
43
|
+
## Listing Cities:
|
44
|
+
|
45
|
+
Retrieve a list of cities for a specified state and country:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
CS.cities(:AK, :US)
|
49
|
+
```
|
50
|
+
|
51
|
+
You can also specify the country, though it's optional. The gem remembers the last country you used:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
CS.states(:BR)
|
55
|
+
|
56
|
+
CS.cities(:TO) # This will use Brazil (BR) as the country
|
57
|
+
```
|
58
|
+
|
59
|
+
Miscellaneous Notes:
|
60
|
+
- The country is an optional argument. The gem always uses the last country that you used.
|
61
|
+
|
62
|
+
## Listing Countries:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
CS.countries
|
66
|
+
```
|
67
|
+
|
68
|
+
## Missing cities and wrong names
|
69
|
+
To add missing cities or to rename wrong ones, create these files in your project folder:
|
70
|
+
`db/cities-lookup.yml` and `db/states-lookup.yml` and `db/countries-lookup.yml`:
|
71
|
+
|
72
|
+
### Renaming a country - `US` to `America`:
|
73
|
+
|
74
|
+
```yaml
|
75
|
+
# db/countries-lookup.yml
|
76
|
+
US: "America"
|
77
|
+
```
|
78
|
+
|
79
|
+
### Renaming a state - `California` to `Something Else`:
|
80
|
+
|
81
|
+
```yaml
|
82
|
+
# db/states-lookup.yml
|
83
|
+
US:
|
84
|
+
CA: Something Else
|
85
|
+
```
|
86
|
+
|
87
|
+
### Renaming a city:
|
88
|
+
|
89
|
+
```yaml
|
90
|
+
# db/cities-lookup.yml
|
91
|
+
US:
|
92
|
+
CA:
|
93
|
+
"Burbank": "Bur Bank"
|
94
|
+
```
|
95
|
+
|
96
|
+
### Adding a missing city:
|
97
|
+
|
98
|
+
```yaml
|
99
|
+
# db/cities-lookup.yml
|
100
|
+
US:
|
101
|
+
CA:
|
102
|
+
"My Town": "My Town"
|
103
|
+
```
|
104
|
+
|
105
|
+
### Suppressing a city (set it as a blank line):
|
106
|
+
```yaml
|
107
|
+
# db/cities-lookup.yml
|
108
|
+
US:
|
109
|
+
CA:
|
110
|
+
"Burbank": ""
|
111
|
+
```
|
112
|
+
|
113
|
+
### To use a different file instead of `db\cities-lookup.yml`:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
CS.set_cities_lookup_file('new-city-names.yml')
|
117
|
+
|
118
|
+
CS.set_states_lookup_file('new-state-names.yml')
|
119
|
+
|
120
|
+
CS.set_countries_lookup_file('new-country-names.yml')
|
121
|
+
```
|
122
|
+
|
123
|
+
## Updating MaxMind database
|
124
|
+
MaxMind update their databases weekly on Tuesdays.
|
125
|
+
|
126
|
+
Since Dec 30, 2019, MaxMind requires a license key (for free) to get download updates.
|
127
|
+
|
128
|
+
To get the license key:
|
129
|
+
1. Sign up for a MaxMind account: https://www.maxmind.com/en/geolite2/signup
|
130
|
+
2. Create a license key: https://www.maxmind.com/en/accounts/current/license-key
|
131
|
+
3. There's no need to download anything.
|
132
|
+
|
133
|
+
To update:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
CS.set_license_key('MY_KEY')
|
137
|
+
|
138
|
+
CS.update
|
139
|
+
```
|
140
|
+
**Note:** Replace `MY_KEY` with your actual license key.
|
141
|
+
|
142
|
+
## Manually setting a database file:
|
143
|
+
|
144
|
+
You can use an alternative database file instead of downloading from MaxMind servers:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
CS.set_maxmind_zip_url('/home/daniel/GeoLite2-City-CSV_20200324.zip')
|
148
|
+
|
149
|
+
CS.update
|
150
|
+
```
|
151
|
+
|
152
|
+
or
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
CS.set_maxmind_zip_url('https://example.com/GeoLite2-City-CSV_20200324.zip')
|
156
|
+
|
157
|
+
CS.update
|
158
|
+
```
|
159
|
+
|
160
|
+
The file has to be a ZIP file. And it has to contain a CVS file named `GeoLite2-City-Locations-en.csv`. This file must be in MaxMind's GeoLite2 City's format.
|
161
|
+
|
162
|
+
## Changelog
|
163
|
+
See [CHANGELOG.md](CHANGELOG.md)
|
164
|
+
|
165
|
+
## How the original `city-state` gem was created
|
166
|
+
https://learnwithdaniel.com/2015/02/citystate-list-of-countries-cities-and-states-ruby/
|
167
|
+
|
168
|
+
## civitas License
|
169
|
+
**civitas** is a open source project forked from `city-state` by Daniel Loureiro with a MIT license. Also, it uses MaxMind open source database.
|
170
|
+
|
171
|
+
## MaxMind License
|
172
|
+
Database and Contents Copyright (c) 2020 MaxMind, Inc.
|
173
|
+
This work is licensed under the Creative Commons Attribution 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/.
|
data/Rakefile
ADDED
data/civitas.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'civitas/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "civitas"
|
8
|
+
spec.version = CS::VERSION
|
9
|
+
spec.authors = ["Daniel Loureiro"]
|
10
|
+
spec.email = ["loureirorg@gmail.com"]
|
11
|
+
spec.summary = %q{Simple list of cities and states of the world}
|
12
|
+
spec.description = %q{Useful to make forms and validations. It uses MaxMind database.}
|
13
|
+
spec.homepage = "https://github.com/duduribeiro/civitas"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = [
|
17
|
+
'Rakefile',
|
18
|
+
'README.md',
|
19
|
+
'LICENSE.txt',
|
20
|
+
'CHANGELOG.md',
|
21
|
+
'civitas.gemspec'
|
22
|
+
] + Dir["lib/**/*"] + Dir["spec/**/*"]
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.required_ruby_version = '>= 2.6.0'
|
26
|
+
|
27
|
+
spec.add_runtime_dependency "rubyzip", ">= 2.3"
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", ">= 1.7"
|
30
|
+
spec.add_development_dependency "rake", ">= 11.0"
|
31
|
+
spec.add_development_dependency 'rspec', '~> 3.10'
|
32
|
+
end
|
data/lib/civitas.rb
ADDED
@@ -0,0 +1,353 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require "civitas/version"
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module CS
|
6
|
+
# CS constants
|
7
|
+
FILES_FOLDER = File.expand_path('../db', __FILE__)
|
8
|
+
MAXMIND_DB_FN = File.join(FILES_FOLDER, "GeoLite2-City-Locations-en.csv")
|
9
|
+
COUNTRIES_FN = File.join(FILES_FOLDER, "countries.yml")
|
10
|
+
DEFAULT_CITIES_LOOKUP_FN = 'db/cities-lookup.yml'
|
11
|
+
DEFAULT_STATES_LOOKUP_FN = 'db/states-lookup.yml'
|
12
|
+
DEFAULT_COUNTRIES_LOOKUP_FN = 'db/countries-lookup.yml'
|
13
|
+
|
14
|
+
@countries, @states, @cities = [{}, {}, {}]
|
15
|
+
@current_country = nil # :US, :BR, :GB, :JP, ...
|
16
|
+
@maxmind_zip_url = nil
|
17
|
+
@license_key = nil
|
18
|
+
|
19
|
+
# lookup tables for state/cities renaming
|
20
|
+
@cities_lookup_fn = nil
|
21
|
+
@cities_lookup = nil
|
22
|
+
@states_lookup_fn = nil
|
23
|
+
@states_lookup = nil
|
24
|
+
@countries_lookup_fn = nil
|
25
|
+
@countries_lookup = nil
|
26
|
+
|
27
|
+
def self.set_maxmind_zip_url(maxmind_zip_url)
|
28
|
+
@maxmind_zip_url = maxmind_zip_url
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.set_license_key(license_key)
|
32
|
+
url = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City-CSV&license_key=#{license_key}&suffix=zip"
|
33
|
+
@license_key = license_key
|
34
|
+
self.set_maxmind_zip_url(url)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.update_maxmind
|
38
|
+
require "open-uri"
|
39
|
+
require "zip"
|
40
|
+
|
41
|
+
# get zipped file
|
42
|
+
return false if !@maxmind_zip_url
|
43
|
+
f_zipped = URI.open(@maxmind_zip_url)
|
44
|
+
|
45
|
+
# unzip file:
|
46
|
+
# recursively searches for "GeoLite2-City-Locations-en"
|
47
|
+
Zip::File.open(f_zipped) do |zip_file|
|
48
|
+
zip_file.each do |entry|
|
49
|
+
if self.present?(entry.name["GeoLite2-City-Locations-en"])
|
50
|
+
fn = entry.name.split("/").last
|
51
|
+
entry.extract(File.join(FILES_FOLDER, fn)) { true } # { true } is to overwrite
|
52
|
+
break
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.update
|
60
|
+
self.update_maxmind # update via internet
|
61
|
+
Dir[File.join(FILES_FOLDER, "states.*")].each do |state_fn|
|
62
|
+
self.install(state_fn.split(".").last.upcase.to_sym) # reinstall country
|
63
|
+
end
|
64
|
+
@countries, @states, @cities = [{}, {}, {}] # invalidades cache
|
65
|
+
File.delete COUNTRIES_FN # force countries.yml to be generated at next call of CS.countries
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
# constants: CVS position
|
70
|
+
ID = 0
|
71
|
+
COUNTRY = 4
|
72
|
+
COUNTRY_LONG = 5
|
73
|
+
STATE = 6
|
74
|
+
STATE_LONG = 7
|
75
|
+
CITY = 10
|
76
|
+
|
77
|
+
def self.install(country)
|
78
|
+
# get CSV if doesn't exists
|
79
|
+
update_maxmind unless File.exist? MAXMIND_DB_FN
|
80
|
+
|
81
|
+
# normalize "country"
|
82
|
+
country = country.to_s.upcase
|
83
|
+
|
84
|
+
# some state codes are empty: we'll use "states-replace" in these cases
|
85
|
+
states_replace_fn = File.join(FILES_FOLDER, "states-replace.yml")
|
86
|
+
states_replace = self.symbolize_keys(YAML::load_file(states_replace_fn))
|
87
|
+
states_replace = states_replace[country.to_sym] || {} # we need just this country
|
88
|
+
states_replace_inv = states_replace.invert # invert key with value, to ease the search
|
89
|
+
|
90
|
+
# read CSV line by line
|
91
|
+
cities = {}
|
92
|
+
states = {}
|
93
|
+
File.foreach(MAXMIND_DB_FN) do |line|
|
94
|
+
rec = line.split(",")
|
95
|
+
next if rec[COUNTRY] != country
|
96
|
+
next if (self.blank?(rec[STATE]) && self.blank?(rec[STATE_LONG])) || self.blank?(rec[CITY])
|
97
|
+
|
98
|
+
# some state codes are empty: we'll use "states-replace" in these cases
|
99
|
+
rec[STATE] = states_replace_inv[rec[STATE_LONG]] if self.blank?(rec[STATE])
|
100
|
+
rec[STATE] = rec[STATE_LONG] if self.blank?(rec[STATE]) # there's no correspondent in states-replace: we'll use the long name as code
|
101
|
+
|
102
|
+
# some long names are empty: we'll use "states-replace" to get the code
|
103
|
+
rec[STATE_LONG] = states_replace[rec[STATE]] if self.blank?(rec[STATE_LONG])
|
104
|
+
|
105
|
+
# normalize
|
106
|
+
rec[STATE] = rec[STATE].to_sym
|
107
|
+
rec[CITY].gsub!(/\"/, "") # sometimes names come with a "\" char
|
108
|
+
rec[STATE_LONG].gsub!(/\"/, "") # sometimes names come with a "\" char
|
109
|
+
|
110
|
+
# cities list: {TX: ["Texas City", "Another", "Another 2"]}
|
111
|
+
cities.merge!({rec[STATE] => []}) if ! states.has_key?(rec[STATE])
|
112
|
+
cities[rec[STATE]] << rec[CITY]
|
113
|
+
|
114
|
+
# states list: {TX: "Texas", CA: "California"}
|
115
|
+
if ! states.has_key?(rec[STATE])
|
116
|
+
state = {rec[STATE] => rec[STATE_LONG]}
|
117
|
+
states.merge!(state)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# sort
|
122
|
+
cities = Hash[cities.sort]
|
123
|
+
states = Hash[states.sort]
|
124
|
+
cities.each { |k, v| cities[k].sort! }
|
125
|
+
|
126
|
+
# save to states.us and cities.us
|
127
|
+
states_fn = File.join(FILES_FOLDER, "states.#{country.downcase}")
|
128
|
+
cities_fn = File.join(FILES_FOLDER, "cities.#{country.downcase}")
|
129
|
+
File.open(states_fn, "w") { |f| f.write states.to_yaml }
|
130
|
+
File.open(cities_fn, "w") { |f| f.write cities.to_yaml }
|
131
|
+
File.chmod(0666, states_fn, cities_fn) # force permissions to rw_rw_rw_ (issue #3)
|
132
|
+
true
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.current_country
|
136
|
+
return @current_country if self.present?(@current_country)
|
137
|
+
|
138
|
+
# we don't have used this method yet: discover by the file extension
|
139
|
+
fn = Dir[File.join(FILES_FOLDER, "cities.*")].last
|
140
|
+
@current_country = self.blank?(fn) ? nil : fn.split(".").last
|
141
|
+
|
142
|
+
# there's no files: we'll install and use :US
|
143
|
+
if self.blank?(@current_country)
|
144
|
+
@current_country = :US
|
145
|
+
self.install(@current_country)
|
146
|
+
|
147
|
+
# we find a file: normalize the extension to something like :US
|
148
|
+
else
|
149
|
+
@current_country = @current_country.to_s.upcase.to_sym
|
150
|
+
end
|
151
|
+
|
152
|
+
@current_country
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.current_country=(country)
|
156
|
+
@current_country = country.to_s.upcase.to_sym
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.cities(state, country = nil)
|
160
|
+
self.current_country = country if self.present?(country) # set as current_country
|
161
|
+
country = self.current_country
|
162
|
+
state = state.to_s.upcase.to_sym
|
163
|
+
|
164
|
+
# load the country file
|
165
|
+
if self.blank?(@cities[country])
|
166
|
+
cities_fn = File.join(FILES_FOLDER, "cities.#{country.to_s.downcase}")
|
167
|
+
self.install(country) if ! File.exist? cities_fn
|
168
|
+
@cities[country] = self.symbolize_keys(YAML::load_file(cities_fn))
|
169
|
+
|
170
|
+
# Remove duplicated cities
|
171
|
+
@cities[country].each do |key, value|
|
172
|
+
@cities[country][key] = value.uniq || []
|
173
|
+
end
|
174
|
+
|
175
|
+
# Process lookup table
|
176
|
+
lookup = get_cities_lookup(country)
|
177
|
+
if ! lookup.nil?
|
178
|
+
lookup.each do |state, new_values|
|
179
|
+
new_values.each do |old_value, new_value|
|
180
|
+
if new_value.nil? || self.blank?(new_value)
|
181
|
+
@cities[country][state].delete(old_value)
|
182
|
+
else
|
183
|
+
index = @cities[country][state].index(old_value)
|
184
|
+
if index.nil?
|
185
|
+
@cities[country][state] << new_value
|
186
|
+
else
|
187
|
+
@cities[country][state][index] = new_value
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
@cities[country][state] = @cities[country][state].sort # sort it alphabetically
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Return list
|
198
|
+
@cities[country][state]
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.set_cities_lookup_file(filename)
|
202
|
+
@cities_lookup_fn = filename
|
203
|
+
@cities_lookup = nil
|
204
|
+
end
|
205
|
+
|
206
|
+
def self.set_states_lookup_file(filename)
|
207
|
+
@states_lookup_fn = filename
|
208
|
+
@states_lookup = nil
|
209
|
+
end
|
210
|
+
|
211
|
+
def self.set_countries_lookup_file(filename)
|
212
|
+
@countries_lookup_fn = filename
|
213
|
+
@countries_lookup = nil
|
214
|
+
end
|
215
|
+
|
216
|
+
def self.get_cities_lookup(country)
|
217
|
+
# lookup file not loaded
|
218
|
+
if @cities_lookup.nil?
|
219
|
+
@cities_lookup_fn = DEFAULT_CITIES_LOOKUP_FN if @cities_lookup_fn.nil?
|
220
|
+
@cities_lookup_fn = File.expand_path(@cities_lookup_fn)
|
221
|
+
return nil if ! File.exist?(@cities_lookup_fn)
|
222
|
+
@cities_lookup = self.symbolize_keys(YAML::load_file(@cities_lookup_fn)) # force countries to be symbols
|
223
|
+
@cities_lookup.each { |key, value| @cities_lookup[key] = self.symbolize_keys(value) } # force states to be symbols
|
224
|
+
end
|
225
|
+
|
226
|
+
return nil if ! @cities_lookup.key?(country)
|
227
|
+
@cities_lookup[country]
|
228
|
+
end
|
229
|
+
|
230
|
+
def self.get_states_lookup(country)
|
231
|
+
# lookup file not loaded
|
232
|
+
if @states_lookup.nil?
|
233
|
+
@states_lookup_fn = DEFAULT_STATES_LOOKUP_FN if @states_lookup_fn.nil?
|
234
|
+
@states_lookup_fn = File.expand_path(@states_lookup_fn)
|
235
|
+
return nil if ! File.exist?(@states_lookup_fn)
|
236
|
+
@states_lookup = self.symbolize_keys(YAML::load_file(@states_lookup_fn)) # force countries to be symbols
|
237
|
+
@states_lookup.each { |key, value| @states_lookup[key] = self.symbolize_keys(value) } # force states to be symbols
|
238
|
+
end
|
239
|
+
|
240
|
+
return nil if ! @states_lookup.key?(country)
|
241
|
+
@states_lookup[country]
|
242
|
+
end
|
243
|
+
|
244
|
+
def self.get_countries_lookup
|
245
|
+
# lookup file not loaded
|
246
|
+
if @countries_lookup.nil?
|
247
|
+
@countries_lookup_fn = DEFAULT_COUNTRIES_LOOKUP_FN if @countries_lookup_fn.nil?
|
248
|
+
@countries_lookup_fn = File.expand_path(@countries_lookup_fn)
|
249
|
+
return nil if ! File.exist?(@countries_lookup_fn)
|
250
|
+
@countries_lookup = self.symbolize_keys(YAML::load_file(@countries_lookup_fn)) # force countries to be symbols
|
251
|
+
end
|
252
|
+
|
253
|
+
@countries_lookup
|
254
|
+
end
|
255
|
+
|
256
|
+
def self.states(country)
|
257
|
+
# Bugfix: https://github.com/loureirorg/city-state/issues/24
|
258
|
+
return {} if country.nil?
|
259
|
+
|
260
|
+
# Set it as current_country
|
261
|
+
self.current_country = country # set as current_country
|
262
|
+
country = self.current_country # normalized
|
263
|
+
|
264
|
+
# Load the country file
|
265
|
+
if self.blank?(@states[country])
|
266
|
+
states_fn = File.join(FILES_FOLDER, "states.#{country.to_s.downcase}")
|
267
|
+
self.install(country) if ! File.exist? states_fn
|
268
|
+
@states[country] = self.symbolize_keys(YAML::load_file(states_fn))
|
269
|
+
|
270
|
+
# Process lookup table
|
271
|
+
lookup = get_states_lookup(country)
|
272
|
+
if ! lookup.nil?
|
273
|
+
lookup.each do |key, value|
|
274
|
+
if value.nil? || self.blank?(value)
|
275
|
+
@states[country].delete(key)
|
276
|
+
else
|
277
|
+
@states[country][key] = value
|
278
|
+
end
|
279
|
+
end
|
280
|
+
@states[country] = @states[country].sort.to_h # sort it alphabetically
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Return list
|
285
|
+
@states[country] || {}
|
286
|
+
end
|
287
|
+
|
288
|
+
# list of all countries of the world (countries.yml)
|
289
|
+
def self.countries
|
290
|
+
if ! File.exist? COUNTRIES_FN
|
291
|
+
# countries.yml doesn't exists, extract from MAXMIND_DB
|
292
|
+
update_maxmind unless File.exist? MAXMIND_DB_FN
|
293
|
+
|
294
|
+
# reads CSV line by line
|
295
|
+
File.foreach(MAXMIND_DB_FN) do |line|
|
296
|
+
rec = line.split(",")
|
297
|
+
next if self.blank?(rec[COUNTRY]) || self.blank?(rec[COUNTRY_LONG]) # jump empty records
|
298
|
+
country = rec[COUNTRY].to_s.upcase.to_sym # normalize to something like :US, :BR
|
299
|
+
if self.blank?(@countries[country])
|
300
|
+
long = rec[COUNTRY_LONG].gsub(/\"/, "") # sometimes names come with a "\" char
|
301
|
+
@countries[country] = long
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# sort and save to "countries.yml"
|
306
|
+
@countries = Hash[@countries.sort]
|
307
|
+
File.open(COUNTRIES_FN, "w") { |f| f.write @countries.to_yaml }
|
308
|
+
File.chmod(0666, COUNTRIES_FN) # force permissions to rw_rw_rw_ (issue #3)
|
309
|
+
else
|
310
|
+
# countries.yml exists, just read it
|
311
|
+
@countries = self.symbolize_keys(YAML::load_file(COUNTRIES_FN))
|
312
|
+
end
|
313
|
+
|
314
|
+
# Applies `countries-lookup.yml` if exists
|
315
|
+
lookup = self.get_countries_lookup()
|
316
|
+
if ! lookup.nil?
|
317
|
+
lookup.each do |key, value|
|
318
|
+
if value.nil? || self.blank?(value)
|
319
|
+
@countries.delete(key)
|
320
|
+
else
|
321
|
+
@countries[key] = value
|
322
|
+
end
|
323
|
+
end
|
324
|
+
@countries = @countries.sort.to_h # sort it alphabetically
|
325
|
+
end
|
326
|
+
|
327
|
+
# Return countries list
|
328
|
+
@countries
|
329
|
+
end
|
330
|
+
|
331
|
+
# get is a method to simplify the use of city-state
|
332
|
+
# get = countries, get(country) = states(country), get(country, state) = cities(state, country)
|
333
|
+
def self.get(country = nil, state = nil)
|
334
|
+
return self.countries if country.nil?
|
335
|
+
return self.states(country) if state.nil?
|
336
|
+
return self.cities(state, country)
|
337
|
+
end
|
338
|
+
|
339
|
+
# Emulates Rails' `blank?` method
|
340
|
+
def self.blank?(obj)
|
341
|
+
obj.respond_to?(:empty?) ? !!obj.empty? : !obj
|
342
|
+
end
|
343
|
+
|
344
|
+
# Emulates Rails' `present?` method
|
345
|
+
def self.present?(obj)
|
346
|
+
!self.blank?(obj)
|
347
|
+
end
|
348
|
+
|
349
|
+
# Emulates Rails' `symbolize_keys` method
|
350
|
+
def self.symbolize_keys(obj)
|
351
|
+
obj.transform_keys { |key| key.to_sym rescue key }
|
352
|
+
end
|
353
|
+
end
|