module CS

Constants

CITY
COUNTRIES_FN
COUNTRY
COUNTRY_LONG
DEFAULT_CITIES_LOOKUP_FN
DEFAULT_COUNTRIES_LOOKUP_FN
DEFAULT_STATES_LOOKUP_FN
FILES_FOLDER

CS constants

ID

constants: CVS position

MAXMIND_DB_FN
STATE
STATE_LONG
VERSION

Public Class Methods

blank?(obj) click to toggle source

Emulates Rails’ ‘blank?` method

# File lib/city-state.rb, line 337
def self.blank?(obj)
  obj.respond_to?(:empty?) ? !!obj.empty? : !obj
end
cities(state, country = nil) click to toggle source
# File lib/city-state.rb, line 159
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.exist? 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?
      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
countries() click to toggle source

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

# File lib/city-state.rb, line 286
def self.countries
  if ! File.exist? COUNTRIES_FN
    # countries.yml doesn't exists, extract from MAXMIND_DB
    update_maxmind unless File.exist? 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_country() click to toggle source
# File lib/city-state.rb, line 135
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) click to toggle source
# File lib/city-state.rb, line 155
def self.current_country=(country)
  @current_country = country.to_s.upcase.to_sym
end
get(country = nil, state = nil) click to toggle source

get is a method to simplify the use of city-state get = countries, get(country) = states(country), get(country, state) = cities(state, country)

# File lib/city-state.rb, line 330
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) click to toggle source
# File lib/city-state.rb, line 213
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.exist?(@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_lookup() click to toggle source
# File lib/city-state.rb, line 241
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.exist?(@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) click to toggle source
# File lib/city-state.rb, line 227
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.exist?(@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) click to toggle source
# File lib/city-state.rb, line 77
def self.install(country)
  # get CSV if doesn't exists
  update_maxmind unless File.exist? 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!(/\"/, "") # 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 { |k, v| cities[k].sort! }

  # 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) click to toggle source

Emulates Rails’ ‘present?` method

# File lib/city-state.rb, line 342
def self.present?(obj)
  !self.blank?(obj)
end
set_cities_lookup_file(filename) click to toggle source
# File lib/city-state.rb, line 198
def self.set_cities_lookup_file(filename)
  @cities_lookup_fn = filename
  @cities_lookup    = nil
end
set_countries_lookup_file(filename) click to toggle source
# File lib/city-state.rb, line 208
def self.set_countries_lookup_file(filename)
  @countries_lookup_fn = filename
  @countries_lookup    = nil
end
set_license_key(license_key) click to toggle source
# File lib/city-state.rb, line 31
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) click to toggle source
# File lib/city-state.rb, line 27
def self.set_maxmind_zip_url(maxmind_zip_url)
  @maxmind_zip_url = maxmind_zip_url
end
set_states_lookup_file(filename) click to toggle source
# File lib/city-state.rb, line 203
def self.set_states_lookup_file(filename)
  @states_lookup_fn = filename
  @states_lookup    = nil
end
states(country) click to toggle source
# File lib/city-state.rb, line 253
def self.states(country)
  # Bugfix: https://github.com/loureirorg/city-state/issues/24
  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.exist? 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) click to toggle source

Emulates Rails’ ‘symbolize_keys` method

# File lib/city-state.rb, line 347
def self.symbolize_keys(obj)
  obj.transform_keys { |key| key.to_sym rescue key }
end
update() click to toggle source
# File lib/city-state.rb, line 59
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_maxmind() click to toggle source
# File lib/city-state.rb, line 37
def self.update_maxmind
  require "open-uri"
  require "zip"

  # get zipped file
  return false if !@maxmind_zip_url
  f_zipped = URI.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