├── .travis.yml ├── lib ├── locationary │ ├── version.rb │ └── nearest_neighbour.rb └── locationary.rb ├── db └── geonames.bin ├── Rakefile ├── tmp └── .gitignore ├── tests ├── test_helper.rb ├── unit │ ├── nn_test.rb │ └── locationary_test.rb ├── integration │ ├── nn_test.rb │ └── locationary_lookup_test.rb └── performance │ ├── nn_test.rb │ └── locationary_test.rb ├── tasks ├── publish.rake ├── nearest_neighbour.rake ├── test.rake └── geonames.rake ├── Gemfile ├── .gitignore ├── README.md ├── Gemfile.lock ├── LICENSE.txt └── locationary.gemspec /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | 6 | -------------------------------------------------------------------------------- /lib/locationary/version.rb: -------------------------------------------------------------------------------- 1 | module Locationary 2 | VERSION = "0.0.3" 3 | end 4 | -------------------------------------------------------------------------------- /db/geonames.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orenmazor/locationary/master/db/geonames.bin -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | Dir.glob('tasks/*.rake').each { |r| import r } 4 | -------------------------------------------------------------------------------- /tmp/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /tests/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'minitest/pride' 3 | 4 | ENV['RACK_ENV'] = 'test' -------------------------------------------------------------------------------- /tasks/publish.rake: -------------------------------------------------------------------------------- 1 | desc 'publish this gem to rubygems' 2 | task :publish do 3 | system("gem build locationary.gemspec") 4 | system("gem push locationary-#{Locationary::VERSION}.gem") 5 | end -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in locationary.gemspec 4 | gemspec 5 | 6 | 7 | gem "fast_xor", :git => "git://github.com/CodeMonkeySteve/fast_xor.git", :ref => "85b79ec6d116f9680f23bd2c5c8c2c2039d477d8" 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | InstalledFiles 7 | _yardoc 8 | coverage 9 | doc/ 10 | lib/bundler/man 11 | pkg 12 | rdoc 13 | spec/reports 14 | test/tmp 15 | test/version_tmp 16 | tmp/allCountries.txt 17 | tmp/allCountries.zip 18 | db/kdtree.bin 19 | db/lookup.txt 20 | -------------------------------------------------------------------------------- /tasks/nearest_neighbour.rake: -------------------------------------------------------------------------------- 1 | require 'kdtree' 2 | require "./lib/locationary" 3 | 4 | namespace :nearest_neighbour do 5 | desc 'persist nearest neighbour structure' 6 | task :create do 7 | build_time = Benchmark.measure do 8 | Locationary.persist_nn_structure 9 | end 10 | puts "nearest-neighbour tree built in #{build_time.real} seconds" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /tests/unit/nn_test.rb: -------------------------------------------------------------------------------- 1 | require "./tests/test_helper" 2 | 3 | class NearestNeighbourTests < MiniTest::Unit::TestCase 4 | 5 | def test_nn_responds_to_nearest_neighbour 6 | assert Locationary.methods.include?(:nearest_neighbour) 7 | end 8 | 9 | def test_nn_does_load_data 10 | assert !Locationary.nn_data.nil? 11 | assert Locationary.nn_lookup.any? 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /tests/unit/locationary_test.rb: -------------------------------------------------------------------------------- 1 | require "./tests/test_helper" 2 | require "./lib/locationary" 3 | 4 | class LocationaryTests < MiniTest::Unit::TestCase 5 | def test_locationary_responds_to_find 6 | assert Locationary.methods.include?(:find) 7 | end 8 | 9 | def test_locationary_does_load_data 10 | assert Locationary.data.any? 11 | end 12 | 13 | def test_locationary_respondsto_find_by_methods 14 | props = [:postalcode, :country_code, :state, :province, :community] 15 | props.each { |prop| assert Locationary.methods.include?("find_by_#{prop}".to_sym) } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Locationary 2 | 3 | TODO: Write a gem description 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | gem 'locationary' 10 | 11 | And then execute: 12 | 13 | $ bundle 14 | 15 | Or install it yourself as: 16 | 17 | $ gem install locationary 18 | 19 | ## Usage 20 | 21 | TODO: Write usage instructions here 22 | 23 | ## Contributing 24 | 25 | 1. Fork it 26 | 2. Create your feature branch (`git checkout -b my-new-feature`) 27 | 3. Commit your changes (`git commit -am 'Add some feature'`) 28 | 4. Push to the branch (`git push origin my-new-feature`) 29 | 5. Create new Pull Request 30 | -------------------------------------------------------------------------------- /tasks/test.rake: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | task :default => ['test'] 4 | 5 | task :test => ['test:unit', 'test:integration'] 6 | 7 | namespace :test do 8 | desc "Run integration tests" 9 | Rake::TestTask.new(:unit) do |t| 10 | t.pattern = 'tests/unit/*_test.rb' 11 | t.libs << 'test' 12 | t.verbose = true 13 | end 14 | 15 | desc "Run integration tests" 16 | Rake::TestTask.new(:integration) do |t| 17 | t.pattern = 'tests/integration/*_test.rb' 18 | t.libs << 'lib:test' 19 | t.verbose = true 20 | end 21 | 22 | desc "Run performance tests" 23 | Rake::TestTask.new(:performance) do |t| 24 | t.pattern = 'tests/performance/*_test.rb' 25 | t.libs << 'lib:test' 26 | t.verbose = true 27 | end 28 | end -------------------------------------------------------------------------------- /tests/integration/nn_test.rb: -------------------------------------------------------------------------------- 1 | require "./tests/test_helper" 2 | 3 | class NearestNeighbourLookupTests < MiniTest::Unit::TestCase 4 | def test_finds_single_nearest_neighbour 5 | actual = "P6C3T9" 6 | assert_equal Locationary.nearest_neighbour(46.5333, -84.35)[0]["Postal Code"], actual 7 | end 8 | 9 | def test_finds_multiple_nearest_neighbours 10 | actual = ["K2P1J2", "K2P0C2", "K2P0B9"] 11 | 12 | results = Locationary.nearest_neighbour(45.4208, -75.69, num_matches: 3) 13 | assert_equal results.length, 3 14 | assert_equal results[0]["Postal Code"], actual[0] 15 | assert_equal results[1]["Postal Code"], actual[1] 16 | assert_equal results[2]["Postal Code"], actual[2] 17 | end 18 | 19 | def test_persists_nn_data_if_empty 20 | Locationary.clear_nn_data 21 | 22 | actual = "K2P1J2" 23 | results = Locationary.nearest_neighbour(45.4208, -75.69) 24 | assert_equal results[0]["Postal Code"], actual 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/CodeMonkeySteve/fast_xor.git 3 | revision: 85b79ec6d116f9680f23bd2c5c8c2c2039d477d8 4 | ref: 85b79ec6d116f9680f23bd2c5c8c2c2039d477d8 5 | specs: 6 | fast_xor (1.1.2) 7 | rake 8 | rake-compiler 9 | 10 | PATH 11 | remote: . 12 | specs: 13 | locationary (0.0.3) 14 | bundler (~> 1.3) 15 | fast_xor 16 | kdtree 17 | levenshtein-ffi 18 | minitest 19 | msgpack 20 | pry 21 | rake 22 | snappy 23 | zip 24 | 25 | GEM 26 | remote: https://rubygems.org/ 27 | specs: 28 | coderay (1.0.9) 29 | ffi (1.1.5) 30 | kdtree (0.3) 31 | levenshtein-ffi (1.0.3) 32 | ffi 33 | ffi (~> 1.1.5) 34 | method_source (0.8.2) 35 | minitest (5.0.6) 36 | msgpack (0.5.5) 37 | pry (0.9.12.2) 38 | coderay (~> 1.0.5) 39 | method_source (~> 0.8) 40 | slop (~> 3.4) 41 | rake (10.1.0) 42 | rake-compiler (0.9.1) 43 | rake 44 | slop (3.4.6) 45 | snappy (0.0.8) 46 | zip (2.0.2) 47 | 48 | PLATFORMS 49 | ruby 50 | 51 | DEPENDENCIES 52 | fast_xor! 53 | locationary! 54 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Oren Mazor 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. 23 | -------------------------------------------------------------------------------- /tests/performance/nn_test.rb: -------------------------------------------------------------------------------- 1 | require "./tests/test_helper" 2 | require "./lib/locationary" 3 | require "./lib/nn" 4 | require "benchmark" 5 | 6 | class NearestNeighbourPerformanceTests < MiniTest::Unit::TestCase 7 | def test_loading_nn_data_speed 8 | Locationary.clear_nn_data 9 | 10 | nn_loading_speed = Benchmark.measure do 11 | kd = Locationary.nn_data 12 | kd_lookup = Locationary.nn_lookup 13 | end 14 | assert nn_loading_speed.real < 1 15 | end 16 | 17 | def test_single_lookup_speed 18 | d = Locationary.data 19 | kd = Locationary.nn_data 20 | kd_data = Locationary.nn_lookup 21 | 22 | lookup_speed = Benchmark.measure do 23 | result = Locationary.nearest_neighbour(34.1, -118.2) 24 | end 25 | 26 | assert lookup_speed.real < 0.01 27 | end 28 | 29 | def test_multiple_lookup_speed 30 | d = Locationary.data 31 | kd = Locationary.nn_data 32 | kd_data = Locationary.nn_lookup 33 | 34 | lookup_speed = Benchmark.measure do 35 | results = Locationary.nearest_neighbour(45.42083333333334, -75.69, num_matches: 3) 36 | end 37 | 38 | assert lookup_speed.real < 0.01 39 | end 40 | end -------------------------------------------------------------------------------- /tests/integration/locationary_lookup_test.rb: -------------------------------------------------------------------------------- 1 | require "./tests/test_helper" 2 | require "./lib/locationary" 3 | 4 | class LookupTests < MiniTest::Unit::TestCase 5 | def setup 6 | @kanata = {"Postal Code"=>"K2K2K1", "Latitude"=>"45.3261190000", "Longitude"=>"-75.9106530000", "City"=>"Kanata", "Province"=>"Ontario", "Country" => "Canada"} 7 | end 8 | 9 | def test_strict_lookup_fails_quietly_on_wrong_data 10 | assert_equal nil, Locationary.find("foobar",{:strict => true}) 11 | end 12 | 13 | def test_strict_lookup_works_on_valid_data 14 | assert_equal @kanata, Locationary.find("K2K2K1",{:strict => true}) 15 | end 16 | 17 | def test_postalcode_convenience_method 18 | assert_equal @kanata, Locationary.find_by_postalcode("K2K2K1", {:strict => true}) 19 | end 20 | 21 | def test_postalcode_fuzzy_search 22 | assert_equal @kanata, Locationary.find_by_postalcode("K2K2Kl", {:strict => false}) 23 | end 24 | 25 | def test_fuzzy_search_ignores_case 26 | assert_equal @kanata, Locationary.find_by_postalcode("k2k2K1", {:strict => false}) 27 | end 28 | 29 | def test_fuzzy_search_finds_full_postal_code_despite_dyslexia 30 | assert_equal @kanata, Locationary.find_by_postalcode("j0hOP0", {:strict => false}) 31 | end 32 | end -------------------------------------------------------------------------------- /tests/performance/locationary_test.rb: -------------------------------------------------------------------------------- 1 | require "./tests/test_helper" 2 | require "./lib/locationary" 3 | require "benchmark" 4 | require "snappy" 5 | 6 | class LocationaryPerformanceTests < MiniTest::Unit::TestCase 7 | def test_loading_data_speed 8 | 9 | raw = nil 10 | reading_speed = Benchmark.measure do 11 | raw = File.read("#{Dir.pwd}/db/geonames_#{ENV['RACK_ENV']}.bin") 12 | end 13 | puts "reading: #{reading_speed}" 14 | assert reading_speed.real < 0.1 15 | 16 | encoded = nil 17 | inflating_speed = Benchmark.measure do 18 | encoded = Snappy.inflate(raw) 19 | end 20 | puts "inflating speed: #{inflating_speed}" 21 | assert inflating_speed.real < 0.3 22 | 23 | data = nil 24 | unpacking_speed = Benchmark.measure do 25 | data = MessagePack.unpack(encoded) 26 | end 27 | puts "unpacking speed: #{unpacking_speed}" 28 | assert unpacking_speed.real < 2 29 | 30 | end 31 | 32 | def test_strict_lookup_speed 33 | lookup_speed = Benchmark.measure do 34 | Locationary.find("K2K2K1") 35 | end 36 | 37 | puts "initial lookup took #{lookup_speed.real} seconds" 38 | 39 | lookup_speed = Benchmark.measure do 40 | Locationary.find("K2K2K1") 41 | end 42 | 43 | puts "warmed up lookup took #{lookup_speed.real} seconds" 44 | end 45 | end -------------------------------------------------------------------------------- /locationary.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'locationary/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "locationary" 8 | spec.version = Locationary::VERSION 9 | spec.authors = ["Oren Mazor"] 10 | spec.email = ["oren.mazor@gmail.com"] 11 | spec.description = "Ruby Gem to normalize and auto-correct location information" 12 | spec.summary = "Ruby Gem to normalize and auto-correct location information" 13 | spec.homepage = "" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_runtime_dependency "bundler", "~> 1.3" 22 | spec.add_runtime_dependency "rake" 23 | spec.add_runtime_dependency "msgpack" 24 | spec.add_runtime_dependency "minitest" 25 | spec.add_runtime_dependency "zip" 26 | spec.add_runtime_dependency "snappy" 27 | spec.add_runtime_dependency "pry" 28 | spec.add_runtime_dependency "levenshtein-ffi" 29 | spec.add_runtime_dependency "kdtree" 30 | spec.add_runtime_dependency "fast_xor" 31 | end 32 | -------------------------------------------------------------------------------- /lib/locationary.rb: -------------------------------------------------------------------------------- 1 | require "locationary/nearest_neighbour" 2 | require "locationary/version" 3 | require "msgpack" 4 | require "snappy" 5 | require "levenshtein" 6 | require "xor" 7 | 8 | module Locationary 9 | 10 | def Locationary.find(query, options = {:strict => true}) 11 | query.upcase 12 | 13 | result = Locationary.data[query] 14 | #if the user asked for a fuzzy lookup and we didn't find an exact match above 15 | if not options[:strict] and not result 16 | result = Locationary.fuzzy(query) 17 | end 18 | 19 | result 20 | end 21 | 22 | def Locationary.fuzzy(query) 23 | best_score = 9999999999 24 | best_hamming = 9999999999 25 | best_match = nil 26 | Locationary.data.keys.each do |key| 27 | new_score = Levenshtein.distance(key,query) 28 | if new_score < best_score 29 | new_hamming = 0 30 | key.dup.xor!(query).bytes.each { |b| new_hamming += b} 31 | if new_hamming < best_hamming 32 | best_score = new_score 33 | best_match = key 34 | end 35 | end 36 | end 37 | 38 | Locationary.data[best_match] 39 | end 40 | 41 | def Locationary.data 42 | @data ||= Locationary.load_data 43 | end 44 | 45 | private 46 | 47 | def Locationary.load_data 48 | raw = File.read("#{File.dirname(__FILE__)}/../db/geonames.bin") 49 | @data = MessagePack.unpack(Snappy.inflate(raw)) 50 | end 51 | 52 | PROPERTIES = { 53 | postalcode: "Postal Code", 54 | country_code: "Country Code", 55 | state: "Name 1", 56 | province: "Name 2", 57 | community: "Name 3" 58 | } 59 | 60 | PROPERTIES.each do |location_prop| 61 | class_eval <<-RUBY, __FILE__, __LINE__ +1 62 | def Locationary.find_by_#{location_prop[0].to_s}(val, options) 63 | Locationary.find(val, options) 64 | end 65 | RUBY 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/locationary/nearest_neighbour.rb: -------------------------------------------------------------------------------- 1 | require "kdtree" 2 | 3 | module Locationary 4 | 5 | def Locationary.nn_data 6 | @kd ||= Locationary.load_nn_data 7 | end 8 | 9 | def Locationary.clear_nn_data 10 | @kd = nil 11 | @kd_lookup = nil 12 | end 13 | 14 | def Locationary.nn_lookup 15 | @kd_lookup ||= Locationary.load_nn_lookup 16 | end 17 | 18 | def Locationary.nearest_neighbour(latitude, longitude, options = {}) 19 | num_matches = options[:num_matches] ||= 1 20 | 21 | results = [] 22 | Locationary.nn_data.nearestk(latitude, longitude, num_matches).each do |match| 23 | results << Locationary.data[Locationary.nn_lookup[match]] 24 | end 25 | results 26 | end 27 | 28 | def Locationary.persist_nn_structure 29 | points = [] 30 | lookup = [] 31 | i = 0 32 | 33 | Locationary.data.each do |location| 34 | lat = location[1]['Latitude'] 35 | lon = location[1]['Longitude'] 36 | if !lat.nil? and !lon.nil? 37 | points << [Float(location[1]['Latitude']), Float(location[1]['Longitude']), i] 38 | lookup << location[0] 39 | i += 1 40 | end 41 | end 42 | kd = Kdtree.new(points) 43 | 44 | File.open(Locationary.nn_data_location,"w") do |file| 45 | kd.persist(file) 46 | end 47 | 48 | File.open(Locationary.nn_lookup_location, "w") do |file| 49 | lookup.each { |l| file.write("#{l}\n") } 50 | end 51 | end 52 | 53 | private 54 | 55 | def Locationary.load_nn_lookup 56 | lookup = [] 57 | Locationary.validate_nn_presence 58 | 59 | File.open(Locationary.nn_lookup_location) do |f| 60 | f.each { |l| lookup << l.strip } 61 | end 62 | lookup 63 | end 64 | 65 | def Locationary.load_nn_data 66 | Locationary.validate_nn_presence 67 | kd = File.open(Locationary.nn_data_location) { |f| Kdtree.new(f) } 68 | end 69 | 70 | def Locationary.validate_nn_presence 71 | if !File.exists?(Locationary.nn_lookup_location) or !File.exists?(Locationary.nn_data_location) then 72 | Locationary.persist_nn_structure 73 | end 74 | end 75 | 76 | def Locationary.nn_data_location 77 | "#{Dir.pwd}/db/kdtree.bin" 78 | end 79 | 80 | def Locationary.nn_lookup_location 81 | "#{Dir.pwd}/db/lookup.txt" 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /tasks/geonames.rake: -------------------------------------------------------------------------------- 1 | require 'msgpack' 2 | require 'net/http' 3 | require 'csv' 4 | require 'zip/zip' 5 | require 'snappy' 6 | require 'benchmark' 7 | 8 | namespace :geonames do 9 | desc 'create database' 10 | task :create do 11 | target_environment = "#{ENV['RACK_ENV']}" 12 | db_path = "./db/geonames.bin" 13 | zipdatafile = "./tmp/allCountries.zip" 14 | rawdata = "./tmp/allCountries.txt" 15 | data_headers = ["Country Code","Postal Code","Place Name","Province","Province Shortcode","City","City Shortcode","Region","Region Shortcode","Latitude","Longitude","Accuracy"] 16 | canada_data_path = "./db/raw/canada.csv" 17 | 18 | result_headers = ["Postal Code", "City", "Province", "Country"] 19 | 20 | if File.exist?(db_path) 21 | File.delete(db_path) 22 | end 23 | 24 | begin 25 | download_time = Benchmark.measure do 26 | Net::HTTP.start("download.geonames.org") do |http| 27 | resp = http.get("/export/zip/allCountries.zip") 28 | open(zipdatafile, "wb") do |file| 29 | file.write(resp.body) 30 | end 31 | end 32 | end 33 | puts "downloaded file in #{download_time.real} seconds" 34 | end 35 | 36 | addresses = {} 37 | 38 | parse_time = Benchmark.measure do 39 | Zip::ZipFile.open(zipdatafile) do |zipfile| 40 | zipfile.each do |file| 41 | FileUtils.mkdir_p(File.dirname(rawdata)) 42 | zipfile.extract(file, rawdata) unless File.exist?(rawdata) 43 | data = File.read(rawdata) 44 | 45 | data.gsub!('"','') 46 | data.gsub!('\'','') 47 | 48 | CSV.parse(data, {:col_sep => "\t", :headers=>data_headers, :force_quotes => true}).each do |row| 49 | next unless "US" == row["Country Code"] 50 | 51 | addresses[row["Postal Code"].upcase] = row.to_hash.select {|k,v| result_headers.include?(k) } 52 | end 53 | end 54 | end 55 | 56 | puts " #{addresses.keys.count} addresses loaded from geonames" 57 | end 58 | puts "parsed data into address structure in #{parse_time.real} seconds" 59 | 60 | compress_time = Benchmark.measure do 61 | File.open(db_path,"w") do |file| 62 | file.write(Snappy.deflate(addresses.to_msgpack)) 63 | end 64 | end 65 | puts "compressed and written data store to disk in #{compress_time.real} seconds" 66 | end 67 | 68 | desc 'statistics' 69 | task :stats do 70 | db = Locationary.data 71 | results = {:country => {}} 72 | 73 | db.values.each do |location| 74 | results[:country][location[:Country]] += 1 75 | end 76 | 77 | puts results.inspect 78 | end 79 | end 80 | --------------------------------------------------------------------------------