Module: CS

Defined in:
lib/cs.rb

Constant Summary collapse

FILES_FOLDER =

CS constants

File.expand_path('../db', __FILE__)
MAXMIND_DB_FN =
File.join(FILES_FOLDER, "GeoLite2-City-Locations-en.csv")
COUNTRIES_FN =
File.join(FILES_FOLDER, "countries.yml")
DEFAULT_CITIES_LOOKUP_FN =
'db/cities-lookup.yml'
DEFAULT_STATES_LOOKUP_FN =
'db/states-lookup.yml'
DEFAULT_COUNTRIES_LOOKUP_FN =
'db/countries-lookup.yml'
ID =

constants: CVS position

0
COUNTRY =
4
COUNTRY_LONG =
5
STATE =
6
STATE_LONG =
7
CITY =
10

Class Method Summary collapse

Class Method Details

.blank?(obj) ⇒ Boolean

Emulates Rails’ ‘blank?` method

Returns:

  • (Boolean)


336
337
338
# File 'lib/cs.rb', line 336

def self.blank?(obj)
  obj.respond_to?(:empty?) ? !!obj.empty? : !obj
end

.cities(state, country = nil) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/cs.rb', line 160

def self.cities(state, country = nil)
  self.current_country = country if self.present?(country) # set as current_country
  country = self.current_country
  state = state.to_s.upcase.to_sym

  # load the country file
  if self.blank?(@cities[country])
    cities_fn = File.join(FILES_FOLDER, "cities.#{country.to_s.downcase}")
    self.install(country) if ! File.exists? cities_fn
    @cities[country] = self.symbolize_keys(YAML::load_file(cities_fn))

    # Remove duplicated cities
    @cities[country].each do |key, value|
      @cities[country][key] = value.uniq || []
    end

    # Process lookup table
    lookup = get_cities_lookup(country, state)
    if ! lookup.nil?
      @cities[country][state] = [] if @cities[country][state].nil?
      lookup.each do |old_value, new_value|
        if new_value.nil? || self.blank?(new_value)
          @cities[country][state].delete(old_value)
        else
          index = @cities[country][state].index(old_value)
          if index.nil?
            @cities[country][state] << new_value
          else
            @cities[country][state][index] = new_value
          end
        end
      end
      @cities[country][state] = @cities[country][state].sort # sort it alphabetically
    end
  end

  # Return list
  @cities[country][state]
end

.countriesObject

list of all countries of the world (countries.yml)



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/cs.rb', line 286

def self.countries
  if ! File.exists? COUNTRIES_FN
    # countries.yml doesn't exists, extract from MAXMIND_DB
    update_maxmind unless File.exists? MAXMIND_DB_FN

    # reads CSV line by line
    File.foreach(MAXMIND_DB_FN) do |line|
      rec = line.split(",")
      next if self.blank?(rec[COUNTRY]) || self.blank?(rec[COUNTRY_LONG]) # jump empty records
      country = rec[COUNTRY].to_s.upcase.to_sym # normalize to something like :US, :BR
      if self.blank?(@countries[country])
        long = rec[COUNTRY_LONG].gsub(/\"/, "") # sometimes names come with a "\" char
        @countries[country] = long
      end
    end

    # sort and save to "countries.yml"
    @countries = Hash[@countries.sort]
    File.open(COUNTRIES_FN, "w") { |f| f.write @countries.to_yaml }
    File.chmod(0666, COUNTRIES_FN) # force permissions to rw_rw_rw_ (issue #3)
  else
    # countries.yml exists, just read it
    @countries = self.symbolize_keys(YAML::load_file(COUNTRIES_FN))
  end

  # Applies `countries-lookup.yml` if exists
  lookup = self.get_countries_lookup()
  if ! lookup.nil?
    lookup.each do |key, value|
      if value.nil? || self.blank?(value)
        @countries.delete(key)
      else
        @countries[key] = value
      end
    end
    @countries = @countries.sort.to_h # sort it alphabetically
  end

  # Return countries list
  @countries
end

.current_countryObject



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/cs.rb', line 136

def self.current_country
  return @current_country if self.present?(@current_country)

  # we don't have used this method yet: discover by the file extension
  fn = Dir[File.join(FILES_FOLDER, "cities.*")].last
  @current_country = self.blank?(fn) ? nil : fn.split(".").last

  # there's no files: we'll install and use :US
  if self.blank?(@current_country)
    @current_country = :US
    self.install(@current_country)

  # we find a file: normalize the extension to something like :US
  else
    @current_country = @current_country.to_s.upcase.to_sym
  end

  @current_country
end

.current_country=(country) ⇒ Object



156
157
158
# File 'lib/cs.rb', line 156

def self.current_country=(country)
  @current_country = country.to_s.upcase.to_sym
end

.get(country = nil, state = nil) ⇒ Object

get = countries, get(country) = states(country), get(country, state) = cities(state, country)



329
330
331
332
333
# File 'lib/cs.rb', line 329

def self.get(country = nil, state = nil)
  return self.countries if country.nil?
  return self.states(country) if state.nil?
  return self.cities(state, country)
end

.get_cities_lookup(country, state) ⇒ Object



215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/cs.rb', line 215

def self.get_cities_lookup(country, state)
  # lookup file not loaded
  if @cities_lookup.nil?
    @cities_lookup_fn  = DEFAULT_CITIES_LOOKUP_FN if @cities_lookup_fn.nil?
    @cities_lookup_fn  = File.expand_path(@cities_lookup_fn)
    return nil if ! File.exists?(@cities_lookup_fn)
    @cities_lookup = self.symbolize_keys(YAML::load_file(@cities_lookup_fn)) # force countries to be symbols
    @cities_lookup.each { |key, value| @cities_lookup[key] = self.symbolize_keys(value) } # force states to be symbols
  end

  return nil if ! @cities_lookup.key?(country) || ! @cities_lookup[country].key?(state)
  @cities_lookup[country][state]
end

.get_countries_lookupObject



243
244
245
246
247
248
249
250
251
252
253
# File 'lib/cs.rb', line 243

def self.get_countries_lookup
  # lookup file not loaded
  if @countries_lookup.nil?
    @countries_lookup_fn  = DEFAULT_COUNTRIES_LOOKUP_FN if @countries_lookup_fn.nil?
    @countries_lookup_fn  = File.expand_path(@countries_lookup_fn)
    return nil if ! File.exists?(@countries_lookup_fn)
    @countries_lookup = self.symbolize_keys(YAML::load_file(@countries_lookup_fn)) # force countries to be symbols
  end

  @countries_lookup
end

.get_states_lookup(country) ⇒ Object



229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/cs.rb', line 229

def self.get_states_lookup(country)
  # lookup file not loaded
  if @states_lookup.nil?
    @states_lookup_fn  = DEFAULT_STATES_LOOKUP_FN if @states_lookup_fn.nil?
    @states_lookup_fn  = File.expand_path(@states_lookup_fn)
    return nil if ! File.exists?(@states_lookup_fn)
    @states_lookup = self.symbolize_keys(YAML::load_file(@states_lookup_fn)) # force countries to be symbols
    @states_lookup.each { |key, value| @states_lookup[key] = self.symbolize_keys(value) } # force states to be symbols
  end

  return nil if ! @states_lookup.key?(country)
  @states_lookup[country]
end

.install(country) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/cs.rb', line 75

def self.install(country)
  # get CSV if doesn't exists
  update_maxmind unless File.exists? MAXMIND_DB_FN

  # normalize "country"
  country = country.to_s.upcase

  # some state codes are empty: we'll use "states-replace" in these cases
  states_replace_fn = File.join(FILES_FOLDER, "states-replace.yml")
  states_replace = self.symbolize_keys(YAML::load_file(states_replace_fn))
  states_replace = states_replace[country.to_sym] || {} # we need just this country
  states_replace_inv = states_replace.invert # invert key with value, to ease the search

  # read CSV line by line
  cities = {}
  states = {}
  File.foreach(MAXMIND_DB_FN) do |line|
    rec = line.split(",")
    next if rec[COUNTRY] != country
    next if (self.blank?(rec[STATE]) && self.blank?(rec[STATE_LONG])) && self.blank?(rec[CITY])

    # some state codes are empty: we'll use "states-replace" in these cases
    rec[STATE] = states_replace_inv[rec[STATE_LONG]] if self.blank?(rec[STATE])
    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

    # some long names are empty: we'll use "states-replace" to get the code
    rec[STATE_LONG] = states_replace[rec[STATE]] if self.blank?(rec[STATE_LONG])

    # normalize
    rec[STATE] = rec[STATE].to_sym
    rec[CITY].gsub!(/\"/, "") # sometimes names come with a "\" char
    rec[STATE_LONG].gsub!(/\"/, "") if !self.blank?(rec[STATE_LONG])# sometimes names come with a "\" char

    # cities list: {TX: ["Texas City", "Another", "Another 2"]}
    cities.merge!({rec[STATE] => []}) if ! states.has_key?(rec[STATE])
    cities[rec[STATE]] << rec[CITY]

    # states list: {TX: "Texas", CA: "California"}
    if ! states.has_key?(rec[STATE])
      state = {rec[STATE] => rec[STATE_LONG]}
      states.merge!(state)
    end
  end

  # sort
  cities = Hash[cities.sort]
  states = Hash[states.sort]
  cities.each do |k, v| 
    v.reject! {|i| i.empty? }
    cities[k].sort!
  end

  # save to states.us and cities.us
  states_fn = File.join(FILES_FOLDER, "states.#{country.downcase}")
  cities_fn = File.join(FILES_FOLDER, "cities.#{country.downcase}")
  File.open(states_fn, "w") { |f| f.write states.to_yaml }
  File.open(cities_fn, "w") { |f| f.write cities.to_yaml }
  File.chmod(0666, states_fn, cities_fn) # force permissions to rw_rw_rw_ (issue #3)
  true
end

.present?(obj) ⇒ Boolean

Emulates Rails’ ‘present?` method

Returns:

  • (Boolean)


341
342
343
# File 'lib/cs.rb', line 341

def self.present?(obj)
  !self.blank?(obj)
end

.set_cities_lookup_file(filename) ⇒ Object



200
201
202
203
# File 'lib/cs.rb', line 200

def self.set_cities_lookup_file(filename)
  @cities_lookup_fn = filename
  @cities_lookup    = nil
end

.set_countries_lookup_file(filename) ⇒ Object



210
211
212
213
# File 'lib/cs.rb', line 210

def self.set_countries_lookup_file(filename)
  @countries_lookup_fn = filename
  @countries_lookup    = nil
end

.set_license_key(license_key) ⇒ Object



29
30
31
32
33
# File 'lib/cs.rb', line 29

def self.set_license_key(license_key)
  url = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City-CSV&license_key=#{license_key}&suffix=zip"
  @license_key = license_key
  self.set_maxmind_zip_url(url)
end

.set_maxmind_zip_url(maxmind_zip_url) ⇒ Object



25
26
27
# File 'lib/cs.rb', line 25

def self.set_maxmind_zip_url(maxmind_zip_url)
  @maxmind_zip_url = maxmind_zip_url
end

.set_states_lookup_file(filename) ⇒ Object



205
206
207
208
# File 'lib/cs.rb', line 205

def self.set_states_lookup_file(filename)
  @states_lookup_fn = filename
  @states_lookup    = nil
end

.states(country) ⇒ Object



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/cs.rb', line 255

def self.states(country)
  return {} if country.nil?

  # Set it as current_country
  self.current_country = country # set as current_country
  country = self.current_country # normalized

  # Load the country file
  if self.blank?(@states[country])
    states_fn = File.join(FILES_FOLDER, "states.#{country.to_s.downcase}")
    self.install(country) if ! File.exists? states_fn
    @states[country] = self.symbolize_keys(YAML::load_file(states_fn))
    # Process lookup table
    lookup = get_states_lookup(country)
    if ! lookup.nil?
      lookup.each do |key, value|
        if value.nil? || self.blank?(value)
          @states[country].delete(key)
        else
          @states[country][key] = value
        end
      end
      @states[country] = @states[country].sort.to_h # sort it alphabetically
    end
  end

  # Return list
  @states[country] || {}
end

.symbolize_keys(obj) ⇒ Object

Emulates Rails’ ‘symbolize_keys` method



346
347
348
# File 'lib/cs.rb', line 346

def self.symbolize_keys(obj)
  obj.transform_keys { |key| key.to_sym rescue key }
end

.updateObject



57
58
59
60
61
62
63
64
65
# File 'lib/cs.rb', line 57

def self.update
  self.update_maxmind # update via internet
  Dir[File.join(FILES_FOLDER, "states.*")].each do |state_fn|
    self.install(state_fn.split(".").last.upcase.to_sym) # reinstall country
  end
  @countries, @states, @cities = [{}, {}, {}] # invalidades cache
  File.delete COUNTRIES_FN # force countries.yml to be generated at next call of CS.countries
  true
end

.update_maxmindObject



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/cs.rb', line 35

def self.update_maxmind
  require "open-uri"
  require "zip"

  # get zipped file
  return false if !@maxmind_zip_url
  f_zipped = open(@maxmind_zip_url)

  # unzip file:
  # recursively searches for "GeoLite2-City-Locations-en"
  Zip::File.open(f_zipped) do |zip_file|
    zip_file.each do |entry|
      if self.present?(entry.name["GeoLite2-City-Locations-en"])
        fn = entry.name.split("/").last
        entry.extract(File.join(FILES_FOLDER, fn)) { true } # { true } is to overwrite
        break
      end
    end
  end
  true
end