├── Gemfile ├── spec_data ├── overlay │ ├── locale │ │ ├── de │ │ │ └── world.yml │ │ ├── en │ │ │ ├── world │ │ │ │ └── oc.yml │ │ │ └── world.yml │ │ └── zz │ │ │ ├── world │ │ │ └── oc.yml │ │ │ └── world.yml │ └── data │ │ └── world.yml ├── data │ ├── world │ │ ├── oc.yml │ │ └── oc │ │ │ └── ao.yml │ └── world.yml └── locale │ ├── en │ ├── world │ │ ├── oc.yml │ │ └── oc │ │ │ └── ao.yml │ └── world.yml │ └── de │ └── world.yml ├── lib ├── carmen │ ├── version.rb │ ├── world.rb │ ├── region_collection.rb │ ├── country.rb │ ├── utils.rb │ ├── querying.rb │ ├── region.rb │ └── i18n.rb └── carmen.rb ├── .gitignore ├── .travis.yml ├── spec ├── carmen │ ├── world_spec.rb │ ├── region_collection_spec.rb │ ├── utils_spec.rb │ ├── overlay_spec.rb │ ├── i18n_spec.rb │ ├── country_spec.rb │ └── region_spec.rb └── spec_helper.rb ├── Rakefile ├── Gemfile.lock ├── carmen.gemspec ├── script ├── convert_translations.rb └── update_data.rb ├── MIT-LICENSE ├── CHANGELOG.md └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /spec_data/overlay/locale/de/world.yml: -------------------------------------------------------------------------------- 1 | --- 2 | de: 3 | world: 4 | -------------------------------------------------------------------------------- /lib/carmen/version.rb: -------------------------------------------------------------------------------- 1 | module Carmen 2 | VERSION = '1.0.2' 3 | end 4 | -------------------------------------------------------------------------------- /spec_data/data/world/oc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - code: AO 3 | type: province 4 | -------------------------------------------------------------------------------- /spec_data/data/world/oc/ao.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - code: LO 3 | type: city 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | pkg 3 | tags 4 | tmp 5 | .DS_Store 6 | .bundle/* 7 | .rvmrc 8 | 9 | .idea/* 10 | -------------------------------------------------------------------------------- /spec_data/locale/en/world/oc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | world: 4 | oc: 5 | ao: 6 | name: Airstrip One 7 | -------------------------------------------------------------------------------- /spec_data/overlay/locale/en/world/oc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | world: 4 | oc: 5 | ao: 6 | name: Airstrip Uno 7 | -------------------------------------------------------------------------------- /spec_data/overlay/locale/zz/world/oc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | zz: 3 | world: 4 | oc: 5 | ao: 6 | name: Zairstrip Zuno 7 | -------------------------------------------------------------------------------- /spec_data/locale/en/world/oc/ao.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | world: 4 | oc: 5 | ao: 6 | lo: 7 | name: London 8 | -------------------------------------------------------------------------------- /spec_data/overlay/locale/zz/world.yml: -------------------------------------------------------------------------------- 1 | --- 2 | zz: 3 | world: 4 | es: 5 | official_name: The Zonderous Zountry of Zeastasia 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 3.2.0 3 | - ruby-head 4 | before_install: 5 | - gem install bundler 6 | matrix: 7 | allow_failures: 8 | - rvm: ruby-head -------------------------------------------------------------------------------- /spec_data/locale/de/world.yml: -------------------------------------------------------------------------------- 1 | --- 2 | de: 3 | world: 4 | eu: 5 | common_name: Eurasia 6 | name: Das großartige Staat von Eurasia 7 | official_name: Das großartige Staat von Eurasia 8 | -------------------------------------------------------------------------------- /spec_data/overlay/locale/en/world.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | world: 4 | es: 5 | official_name: The Wonderous Country of Eastasia 6 | se: 7 | common_name: Sealand 8 | name: Sealand 9 | official_name: The Principality of Sealand 10 | 11 | -------------------------------------------------------------------------------- /spec_data/data/world.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - alpha_2_code: OC 3 | alpha_3_code: OCE 4 | numeric_code: "001" 5 | type: country 6 | - alpha_2_code: EU 7 | alpha_3_code: EUR 8 | numeric_code: "002" 9 | type: country 10 | - alpha_2_code: ES 11 | alpha_3_code: EST 12 | numeric_code: "003" 13 | type: country 14 | -------------------------------------------------------------------------------- /spec/carmen/world_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Carmen::World do 4 | 5 | it 'is the World' do 6 | Carmen::World.instance.is_a?(Carmen::World).must_equal(true) 7 | end 8 | 9 | it 'has 3 subregions' do 10 | Carmen::World.instance.subregions.size.must_equal(3) 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /spec_data/overlay/data/world.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - alpha_2_code: SE 3 | alpha_3_code: SEA 4 | numeric_code: "004" 5 | common_name: Sealand 6 | name: Sealand 7 | official_name: The Principality of Sealand 8 | type: fort 9 | - alpha_2_code: EU 10 | _enabled: false 11 | - alpha_2_code: ES 12 | official_name: The Wonderous Country of Eastasia 13 | -------------------------------------------------------------------------------- /spec_data/locale/en/world.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | world: 4 | oc: 5 | common_name: Oceania 6 | name: Oceania 7 | official_name: The Superstate of Oceania 8 | eu: 9 | common_name: Eurasia 10 | name: Eurasia 11 | official_name: The Superstate of Eurasia 12 | es: 13 | common_name: Eastasia 14 | name: Eastasia 15 | official_name: The Superstate of Eastasia 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | require 'rake/testtask' 5 | Rake::TestTask.new(:spec) do |test| 6 | test.libs << 'lib' << 'spec' 7 | test.pattern = 'spec/**/*_spec.rb' 8 | test.verbose = true 9 | end 10 | 11 | task :default => :spec 12 | 13 | desc "Start a console with this version of Carmen loaded" 14 | task :console do 15 | require 'bundler/setup' 16 | require 'carmen' 17 | require 'irb' 18 | ARGV.clear 19 | IRB.start 20 | end 21 | -------------------------------------------------------------------------------- /lib/carmen/world.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | require 'carmen/region' 4 | 5 | module Carmen 6 | class World < Region 7 | include Singleton 8 | 9 | def type; 'world'; end 10 | def name; 'Earth'; end 11 | 12 | def subregion_data_path 13 | 'world.yml' 14 | end 15 | 16 | def subregion_class 17 | Country 18 | end 19 | 20 | def path 21 | 'world' 22 | end 23 | 24 | def inspect 25 | "<##{self.class}>" 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/carmen/region_collection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Carmen::RegionCollection do 4 | 5 | before do 6 | @collection = Carmen::RegionCollection.new([ 7 | Carmen::Region.new('type' => 'custom_type1', 'code' => 'AA'), 8 | Carmen::Region.new('type' => 'custom_type2', 'code' => 'BB') 9 | ]) 10 | end 11 | 12 | it 'provides an API for filtering regions by type' do 13 | @collection.typed('custom_type1').map(&:code).must_equal ['AA'] 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | carmen (1.0.2) 5 | activesupport (>= 3.0.0) 6 | 7 | GEM 8 | remote: http://rubygems.org/ 9 | specs: 10 | activesupport (7.0.7.2) 11 | concurrent-ruby (~> 1.0, >= 1.0.2) 12 | i18n (>= 1.6, < 2) 13 | minitest (>= 5.1) 14 | tzinfo (~> 2.0) 15 | concurrent-ruby (1.2.2) 16 | i18n (1.14.1) 17 | concurrent-ruby (~> 1.0) 18 | minitest (5.19.0) 19 | rake (13.0.6) 20 | tzinfo (2.0.6) 21 | concurrent-ruby (~> 1.0) 22 | 23 | PLATFORMS 24 | x86_64-darwin-22 25 | x86_64-linux 26 | 27 | DEPENDENCIES 28 | carmen! 29 | i18n 30 | rake 31 | 32 | BUNDLED WITH 33 | 2.4.4 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | 3 | require 'bundler/setup' 4 | require 'carmen' 5 | 6 | def setup_carmen_test_data_path 7 | Carmen.clear_data_paths 8 | Carmen.append_data_path(carmen_spec_data_path) 9 | end 10 | 11 | def setup_carmen_test_i18n_backend 12 | Carmen.i18n_backend = Carmen::I18n::Simple.new(carmen_spec_locale_path) 13 | end 14 | 15 | def carmen_spec_data_path 16 | Carmen.root_path + 'spec_data/data' 17 | end 18 | 19 | def carmen_spec_locale_path 20 | Carmen.root_path + 'spec_data/locale' 21 | end 22 | 23 | def carmen_spec_overlay_locale_path 24 | Carmen.root_path + 'spec_data/overlay/locale' 25 | end 26 | 27 | def carmen_spec_overlay_data_path 28 | Carmen.root_path + 'spec_data/overlay/data' 29 | end 30 | 31 | setup_carmen_test_data_path 32 | setup_carmen_test_i18n_backend 33 | 34 | -------------------------------------------------------------------------------- /carmen.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) 4 | require "carmen/version" 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{carmen} 8 | s.summary = %q{A collection of geographic region data for Ruby} 9 | s.description = %q{Includes data from the Debian iso-data project.} 10 | s.version = Carmen::VERSION 11 | s.authors = ["Jim Benton"] 12 | s.email = %q{jim@autonomousmachine.com} 13 | s.homepage = %q{http://github.com/jim/carmen} 14 | 15 | s.required_rubygems_version = '>= 1.3.6' 16 | s.require_paths = ["lib"] 17 | s.files = Dir.glob("{lib,iso_data,locale}/**/*") + %w(MIT-LICENSE README.md CHANGELOG.md) 18 | 19 | s.add_development_dependency('rake') 20 | s.add_development_dependency('i18n') 21 | s.add_dependency('activesupport', '>= 3.0.0') 22 | end 23 | -------------------------------------------------------------------------------- /spec/carmen/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Utils.merge_arrays_by_keys' do 4 | 5 | it 'merges arrays of hashes using a key' do 6 | first = [ 7 | { 'code' => 'AA', 'meta' => 'original' }, 8 | { 'code' => 'BB', 'meta' => 'original' } 9 | ] 10 | 11 | second = [ 12 | { 'code' => 'BB', 'meta' => 'modified' }, 13 | { 'code' => 'CC', 'meta' => 'new' } 14 | ] 15 | 16 | expected = [ 17 | { 'code' => 'AA', 'meta' => 'original' }, 18 | { 'code' => 'BB', 'meta' => 'modified' }, 19 | { 'code' => 'CC', 'meta' => 'new' } 20 | ] 21 | 22 | merged = Carmen::Utils.merge_arrays_by_keys([first, second], ['code']) 23 | 24 | merged.must_equal(expected) 25 | end 26 | end 27 | 28 | describe 'Utils.deep_hash_merge' do 29 | 30 | it 'merges hashes' do 31 | first = { 'a' => 'old', 'b' => 'old', 'c' => 'old' } 32 | second = { 'a' => nil, 'b' => 'new', 'd' => 'new' } 33 | 34 | expected = { 'a' => 'old', 'b' => 'new', 'c' => 'old', 'd' => 'new' } 35 | 36 | Carmen::Utils.deep_hash_merge([first, second]).must_equal(expected) 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /lib/carmen/region_collection.rb: -------------------------------------------------------------------------------- 1 | require 'carmen/querying' 2 | 3 | module Carmen 4 | # RegionCollection is responsible for holding the subregions for a 5 | # region and also provides an interface to query said subregions. 6 | # 7 | # Example: 8 | # 9 | # states = Carmen::Country.coded('US').subregions 10 | # => # 11 | # states.size 12 | # => 5 13 | # states.named('Illinois') 14 | # => # 15 | # 16 | class RegionCollection < Array 17 | include Querying 18 | 19 | # Filters the regions in this collection by type. 20 | # 21 | # type - The String type to filter by 22 | # 23 | # Returns a region collection containing all the regions with the supplied 24 | # type. 25 | def typed(type) 26 | downcased_type = type.downcase 27 | results = select{ |r| r.type == downcased_type } 28 | Carmen::RegionCollection.new(results) 29 | end 30 | 31 | private 32 | 33 | def query_collection 34 | self 35 | end 36 | 37 | def attribute_to_search_for_code(code) 38 | :code 39 | end 40 | 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/carmen/overlay_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Data overlaying" do 4 | 5 | before do 6 | Carmen.append_data_path(carmen_spec_overlay_data_path) 7 | end 8 | 9 | after do 10 | setup_carmen_test_data_path 11 | end 12 | 13 | it 'finds elements that exist only in overlay files' do 14 | sealand = Carmen::Country.coded('SE') 15 | sealand.instance_of?(Carmen::Country).must_equal true 16 | sealand.type.must_equal('fort') 17 | end 18 | 19 | it 'still finds elements that exist only in gem files' do 20 | oceania = Carmen::Country.coded('OC') 21 | oceania.instance_of?(Carmen::Country).must_equal true 22 | oceania.type.must_equal('country') 23 | end 24 | 25 | it 'still finds subregions that exist only in gem files' do 26 | oceania = Carmen::Country.coded('OC') 27 | oceania.subregions?.must_equal true 28 | oceania.subregions.named("Airstrip One").type.must_equal("province") 29 | end 30 | 31 | it 'removes elements that have _enabled set to false' do 32 | Carmen::World.instance.subregions.size.must_equal(3) 33 | Carmen::Country.named('Eurasia').must_equal nil 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /script/convert_translations.rb: -------------------------------------------------------------------------------- 1 | # DO NOT USE 2 | require 'yaml' 3 | require 'pathname' 4 | require 'fileutils' 5 | 6 | YAML::ENGINE.yamler = 'psych' 7 | 8 | ROOT = Pathname.new(__FILE__ + '/../..').expand_path 9 | DEPRECATED_DATA_PATH = ROOT + 'deprecated_data' 10 | 11 | def convert_country_files 12 | Dir[DEPRECATED_DATA_PATH + 'countries/*.yml'].each do |file| 13 | puts "Converting #{file}" 14 | 15 | countries = YAML.load_file(file) 16 | locale = Pathname.new(file).basename('.yml').to_s 17 | 18 | sorted = countries.sort_by {|(name, code)| code} 19 | converted = sorted.inject({}) do |hash, (name, code)| 20 | hash[code.downcase]= { 'common_name' => nil, 21 | 'name' => name, 22 | 'official_name' => nil } 23 | hash 24 | end 25 | 26 | wrapped = { 27 | locale => { 28 | 'world' => converted 29 | } 30 | } 31 | 32 | # Make the locale's directory 33 | locale_path = ROOT + "locale/#{locale}" 34 | FileUtils.mkdir_p(locale_path) 35 | 36 | File.open(locale_path + "world.yml", 'w') do |f| 37 | YAML.dump(wrapped, f) 38 | end 39 | 40 | end 41 | end 42 | 43 | convert_country_files 44 | -------------------------------------------------------------------------------- /lib/carmen/country.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | require 'carmen/world' 4 | require 'carmen/region' 5 | require 'carmen/querying' 6 | 7 | module Carmen 8 | class Country < Region 9 | extend Querying 10 | extend SingleForwardable 11 | 12 | attr_reader :alpha_2_code 13 | attr_reader :alpha_3_code 14 | attr_reader :numeric_code 15 | attr_reader :uuid 16 | 17 | def initialize(data={}, parent=nil) 18 | @alpha_2_code = data['alpha_2_code'] 19 | @alpha_3_code = data['alpha_3_code'] 20 | @numeric_code = data['numeric_code'] 21 | @uuid = data['uuid'] 22 | super 23 | end 24 | 25 | def common_name 26 | Carmen.i18n_backend.translate(path('common_name')) 27 | end 28 | 29 | def official_name 30 | Carmen.i18n_backend.translate(path('official_name')) 31 | end 32 | 33 | def self.all 34 | World.instance.subregions 35 | end 36 | 37 | def self.query_collection 38 | all 39 | end 40 | 41 | def inspect 42 | %(<##{self.class} name="#{name}">) 43 | end 44 | 45 | def code 46 | alpha_2_code 47 | end 48 | 49 | private 50 | 51 | def self.attribute_to_search_for_code(code) 52 | code.to_s.size == 2 ? :alpha_2_code : :alpha_3_code 53 | end 54 | 55 | def subregion_directory 56 | alpha_2_code.downcase 57 | end 58 | 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Jim Benton 2 | 3 | View helpers based on code (c) 2008 Michael Koziarski (http://github.com/rails/country_select) 4 | 5 | Contains data from the iso-codes Debian project, released under the LGPL: 6 | 7 | Copyright (C) 2004-2006 Alastair McKinstry 8 | Copyright (C) 2004, 2007 Christian Perrier 9 | Copyright (C) 2005-2007 Tobias Quathamer 10 | Copyright (C) 2007, 2009 LI Daobing 11 | Copyright (C) 2007-2010 Alexis Darrasse 12 | 13 | Source: 14 | 15 | More info: http://pkg-isocodes.alioth.debian.org/ 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining 18 | a copy of this software and associated documentation files (the 19 | "Software"), to deal in the Software without restriction, including 20 | without limitation the rights to use, copy, modify, merge, publish, 21 | distribute, sublicense, and/or sell copies of the Software, and to 22 | permit persons to whom the Software is furnished to do so, subject to 23 | the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be 26 | included in all copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 29 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 30 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 31 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 32 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 33 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 34 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 35 | -------------------------------------------------------------------------------- /lib/carmen/utils.rb: -------------------------------------------------------------------------------- 1 | module Carmen 2 | module Utils 3 | # Merge an array of hashes deeply. 4 | # 5 | # When a conflict occurs: 6 | # - if both the old value and the new value respond_to? :merge, they 7 | # are recursively passed to deep_merge_hash and the result used. 8 | # - if either doesn't respond_to? :merge, then the new value is used if 9 | # it is not nil. If the new value is nil, the old value is used. 10 | # 11 | # Returns a merged hash. 12 | def self.deep_hash_merge(hashes) 13 | return hashes.first if hashes.size == 1 14 | 15 | hashes.inject { |acc, hash| 16 | acc.merge(hash) { |key, old_value, new_value| 17 | if old_value.respond_to?(:merge) && new_value.respond_to?(:merge) 18 | deep_hash_merge([old_value, new_value]) 19 | else 20 | new_value || old_value 21 | end 22 | } 23 | } 24 | end 25 | 26 | # Merge arrays of hashes using the specified keys. 27 | # 28 | # If two hashes have the same value for a key, they are merged together. 29 | # Otherwise, a new hash is appended to the array. 30 | # 31 | # Matching arrays uses the keys in the order they are provided. 32 | # 33 | # Returns a single merges array of hashes. 34 | def self.merge_arrays_by_keys(arrays, keys) 35 | arrays.inject do |aggregate, array| 36 | 37 | array.each do |new_hash| 38 | # Find the matching element in the agregate array 39 | existing = aggregate.find do |hash| 40 | keys.any? {|key| hash[key] && hash[key] == new_hash[key] } 41 | end 42 | 43 | # Merge the new hash to an existing one, or append it if new 44 | if existing 45 | existing.merge!(new_hash) 46 | else 47 | aggregate << new_hash 48 | end 49 | end 50 | 51 | aggregate 52 | end 53 | 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/carmen/querying.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/string' 2 | 3 | module Carmen 4 | module Querying 5 | # Find a region by code. 6 | # 7 | # code - The String code to search for 8 | # 9 | # Returns a region with the supplied code, or nil if none is found. 10 | def coded(code) 11 | return nil if code.nil? 12 | attribute = attribute_to_search_for_code(code) 13 | if attribute.nil? 14 | fail "could not find an attribute to search for code '#{code}'" 15 | end 16 | code = code.downcase # Codes are all ASCII 17 | query_collection.find do |region| 18 | region.send(attribute).downcase == code 19 | end 20 | end 21 | 22 | # Find a region by name. 23 | # 24 | # name - The String name to search for. 25 | # options - The Hash options used to modify the search (default:{}): 26 | # :fuzzy - Whether to use fuzzy matching when finding a 27 | # matching name (optional, default: false) 28 | # :case - Whether or not the match is case-sensitive 29 | # (optional, default: false) 30 | # 31 | # Returns a region with the supplied name, or nil if none if found. 32 | def named(name, options={}) 33 | case_fold = !options[:case] && name.respond_to?(:each_codepoint) 34 | # These only need to be built once 35 | name = case_fold ? normalise_name(name) : name 36 | # For now, "fuzzy" just means substring, optionally case-insensitive (the second argument looks for nil, not falseness) 37 | regexp = options[:fuzzy] ? Regexp.new(name, options[:case] ? nil : true) : nil 38 | 39 | query_collection.find do |region| 40 | found_literal = name === (case_fold && region.name ? normalise_name(region.name) : region.name) 41 | found_literal || options[:fuzzy] && regexp === region.name 42 | end 43 | end 44 | 45 | private 46 | 47 | def normalise_name(name) 48 | name.mb_chars.downcase.unicode_normalize(:nfkc) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/carmen.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'pathname' 3 | 4 | lib_path = File.expand_path('../../lib', __FILE__) 5 | $LOAD_PATH.unshift(lib_path) 6 | 7 | require 'carmen/country' 8 | require 'carmen/i18n' 9 | require 'carmen/version' 10 | 11 | module Carmen 12 | class << self 13 | 14 | attr_accessor :data_paths, :i18n_backend 15 | 16 | # Public: Return the current array of locations where data files are stored. 17 | # 18 | # Data in entries that appear later in the array takes precedence. 19 | # 20 | # Each path should follow the following structure: 21 | # |- world.yml (all countries) 22 | # \- regions (directory for subregions, named by code) 23 | # |- be.yml (subregion file for a country) 24 | # 25 | # Defaults to only the the `iso_data` directory within the Carmen directory. 26 | def data_paths 27 | @data_paths 28 | end 29 | 30 | # Public: Set the array of paths for Carmen to search for data files. 31 | def data_paths=(paths) 32 | @data_paths = paths 33 | end 34 | 35 | # Public: return the current I18n backend. 36 | # 37 | # Defaults to an instance of Carmen::I18n::Simple. 38 | def i18n_backend 39 | @i18n_backend 40 | end 41 | 42 | # Public: set an object to use as the I18n backend. 43 | # 44 | # Ths suppiled object must respond to t(key). 45 | def i18n_backend=(backend) 46 | @i18n_backend = backend 47 | end 48 | 49 | # Public: the Carmen library's root directory. 50 | # 51 | # Provides a way to find the built-in data and locale files. 52 | attr_accessor :root_path 53 | 54 | # Public: Append an additional data path. 55 | # path - The String path to the data directory. 56 | def append_data_path(path) 57 | World.instance.reset! 58 | self.data_paths << Pathname.new(path) 59 | end 60 | 61 | # Public: Clear the data_paths array. 62 | def clear_data_paths 63 | World.instance.reset! 64 | self.data_paths = [] 65 | end 66 | 67 | # Public: Reset the data_paths array to the defaults. 68 | def reset_data_paths 69 | clear_data_paths 70 | # append_data_path(root_path + 'iso_data/base') 71 | # append_data_path(root_path + 'iso_data/overlay') 72 | end 73 | 74 | # Public: Reset the i18n_backend to a default backend. 75 | def reset_i18n_backend 76 | # base_locale_path = root_path + 'locale/base' 77 | # override_locale_path = root_path + 'locale/overlay' 78 | self.i18n_backend = Carmen::I18n::Simple.new() 79 | end 80 | end 81 | 82 | self.root_path = Pathname.new(__FILE__) + '../..' 83 | 84 | self.reset_data_paths 85 | self.reset_i18n_backend 86 | 87 | end 88 | -------------------------------------------------------------------------------- /spec/carmen/i18n_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'spec_helper' 4 | 5 | describe 'Carmen I18n defaults' do 6 | 7 | it "sets an instance of I18n::Simple as the default backend" do 8 | backend = Carmen.i18n_backend 9 | 10 | backend.instance_of?(Carmen::I18n::Simple).must_equal true 11 | end 12 | 13 | end 14 | 15 | describe "I18n::Simple" do 16 | 17 | before do 18 | path = carmen_spec_locale_path 19 | @i18n = Carmen::I18n::Simple.new(path) 20 | end 21 | 22 | it 'knows which locales are available' do 23 | @i18n.available_locales.must_equal ['de', 'en'] 24 | end 25 | 26 | it "loads and merges yaml files" do 27 | @i18n.t('world.oc.name').must_equal 'Oceania' 28 | @i18n.t('world.oc.ao.name').must_equal 'Airstrip One' 29 | @i18n.t('world.oc.ao.lo.name').must_equal 'London' 30 | end 31 | 32 | describe "overlaying additional locale paths" do 33 | 34 | before do 35 | @i18n.append_locale_path(carmen_spec_overlay_locale_path) 36 | end 37 | 38 | after do 39 | @i18n.locale_paths.pop 40 | @i18n.reset! 41 | end 42 | 43 | it 'can override the names of countries' do 44 | @i18n.t('world.es.official_name').must_equal('The Wonderous Country of Eastasia') 45 | end 46 | 47 | it 'can override the names of subregions' do 48 | @i18n.t('world.oc.ao.name').must_equal('Airstrip Uno') 49 | end 50 | end 51 | 52 | describe 'using a non-default locale' do 53 | before do 54 | @i18n.append_locale_path(carmen_spec_overlay_locale_path) 55 | @i18n.locale = 'zz' 56 | end 57 | 58 | after do 59 | @i18n.locale = Carmen::I18n::Simple::DEFAULT_LOCALE 60 | @i18n.reset! 61 | end 62 | 63 | it 'retains existing locales' do 64 | @i18n.available_locales.must_equal ['de', 'en', 'zz'] 65 | end 66 | 67 | it 'stores the current locale' do 68 | @i18n.locale.must_equal 'zz' 69 | end 70 | 71 | it 'can override the names of countries' do 72 | @i18n.t('world.es.official_name').must_equal('The Zonderous Zountry of Zeastasia') 73 | end 74 | 75 | it 'can override the names of subregions' do 76 | @i18n.t('world.oc.ao.name').must_equal('Zairstrip Zuno') 77 | end 78 | 79 | it 'falls back when a a locale is missing a value' do 80 | @i18n.t('world.eu.official_name').must_equal('The Superstate of Eurasia') 81 | end 82 | end 83 | 84 | describe 'overlaying empty files onto a locale' do 85 | before do 86 | @i18n.append_locale_path(carmen_spec_overlay_locale_path) 87 | @i18n.locale = 'de' 88 | end 89 | 90 | after do 91 | @i18n.locale = Carmen::I18n::Simple::DEFAULT_LOCALE 92 | @i18n.reset! 93 | end 94 | 95 | it 'still has access to the base locale data' do 96 | @i18n.t('world.eu.official_name').must_equal('Das großartige Staat von Eurasia') 97 | end 98 | 99 | end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /spec/carmen/country_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Carmen::Country do 4 | 5 | describe "all" do 6 | before do 7 | @countries = countries = Carmen::Country.all 8 | end 9 | 10 | it "provides access to all countries" do 11 | @countries.size.must_equal 3 12 | end 13 | 14 | it "denies modification of countries" do 15 | assert_raises RuntimeError do 16 | @countries.clear 17 | end 18 | end 19 | end 20 | 21 | describe "API for finding countries by name" do 22 | it "provides exact matching" do 23 | eastasia = Carmen::Country.named('Eastasia') 24 | eastasia.instance_of?(Carmen::Country).must_equal true 25 | end 26 | 27 | it "provides case-insensitive searches by default" do 28 | eurasia = Carmen::Country.named('eUrAsIa') 29 | eurasia.instance_of?(Carmen::Country).must_equal true 30 | eurasia.name.must_equal 'Eurasia' 31 | end 32 | 33 | it "provides case-sensitive searches optionally" do 34 | oceania = Carmen::Country.named('oCeAnIa', :case => true) 35 | oceania.must_equal nil 36 | oceania = Carmen::Country.named('Oceania', :case => true) 37 | oceania.instance_of?(Carmen::Country).must_equal true 38 | oceania.name.must_equal 'Oceania' 39 | end 40 | 41 | it "provides fuzzy (substring) matching optionally" do 42 | eastasia = Carmen::Country.named('East', :fuzzy => true) 43 | eastasia.instance_of?(Carmen::Country).must_equal true 44 | eastasia.name.must_equal 'Eastasia' 45 | end 46 | 47 | end 48 | 49 | it "provides an API for finding countries by code" do 50 | eurasia = Carmen::Country.coded('EU') 51 | eurasia.instance_of?(Carmen::Country).must_equal true 52 | end 53 | 54 | describe "basic attributes" do 55 | before do 56 | @oceania = Carmen::Country.coded('OC') 57 | end 58 | 59 | it "is of type :country" do 60 | @oceania.type.must_equal 'country' 61 | end 62 | 63 | it "has a name" do 64 | @oceania.name.must_equal 'Oceania' 65 | end 66 | 67 | it "has an official name" do 68 | @oceania.official_name.must_equal 'The Superstate of Oceania' 69 | end 70 | it "has a common name" do 71 | @oceania.common_name.must_equal 'Oceania' 72 | end 73 | 74 | it "has a 2 character code" do 75 | @oceania.alpha_2_code.must_equal 'OC' 76 | end 77 | 78 | it "has a 3 character code" do 79 | @oceania.alpha_3_code.must_equal 'OCE' 80 | end 81 | 82 | it "has code as an alias to alpha_2_code" do 83 | @oceania.code.must_equal 'OC' 84 | end 85 | 86 | it "has a numeric code" do 87 | @oceania.numeric_code.must_equal '001' 88 | end 89 | 90 | it "has the world as a parent" do 91 | @oceania.parent.must_equal Carmen::World.instance 92 | end 93 | 94 | it 'has a reasonable inspect value' do 95 | @oceania.inspect.must_equal '<#Carmen::Country name="Oceania">' 96 | end 97 | end 98 | 99 | 100 | end 101 | -------------------------------------------------------------------------------- /lib/carmen/region.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | require 'carmen/region_collection' 4 | require 'carmen/utils' 5 | 6 | module Carmen 7 | class Region 8 | 9 | attr_reader :type 10 | attr_reader :code 11 | attr_reader :parent 12 | 13 | def initialize(data={}, parent=nil) 14 | @type = data['type'] 15 | @code = data['code'] 16 | @parent = parent 17 | end 18 | 19 | def name 20 | Carmen.i18n_backend.translate(path('name')) 21 | end 22 | 23 | def subregions 24 | @subregions ||= load_subregions.freeze 25 | end 26 | 27 | def subregions? 28 | !subregions.empty? 29 | end 30 | 31 | def subregion_data_path 32 | @parent.subregion_data_path.sub('.yml', "/#{subregion_directory}.yml") 33 | end 34 | 35 | def subregion_class 36 | Region 37 | end 38 | 39 | # Return a path string for this region. Useful for use with I18n. 40 | # 41 | # Returns a string in the format "world.$PARENT_CODE.$REGION_CODE", such as 42 | # "world.us.il". The number of segments is the depth of the region plus one. 43 | def path(suffix=nil) 44 | base = "#{parent.path}.#{subregion_directory}" 45 | base << ".#{suffix.to_s}" if suffix 46 | base 47 | end 48 | 49 | def inspect 50 | "<##{self.class} name=\"#{name}\" type=\"#{type}\">" 51 | end 52 | 53 | def to_s 54 | name 55 | end 56 | 57 | # Clears the subregion cache 58 | def reset! 59 | @subregions = nil 60 | end 61 | 62 | def <=>(other) 63 | name <=> other.name 64 | end 65 | 66 | private 67 | 68 | def subregion_directory 69 | code.downcase 70 | end 71 | 72 | def load_subregions 73 | if Carmen.data_paths.any? {|path| (path + subregion_data_path).exist? } 74 | load_subregions_from_path(subregion_data_path, self) 75 | else 76 | RegionCollection.new([]) 77 | end 78 | end 79 | 80 | def load_subregions_from_path(path, parent=nil) 81 | regions = load_data_at_path(path).collect do |data| 82 | subregion_class.new(data, parent) 83 | end 84 | RegionCollection.new(regions) 85 | end 86 | 87 | # Load the data for a path. 88 | # 89 | # The resulting data will be the result of loading the file from the data_path 90 | # and overlaying matching data (if it exists) from the overlay_path. 91 | def load_data_at_path(path) 92 | data_sets = Carmen.data_paths.map do |data_path| 93 | if File.exist?(data_path + path) 94 | YAML.load_file(data_path + path) 95 | else 96 | [] 97 | end 98 | end 99 | flatten_data(data_sets) 100 | end 101 | 102 | # Merge multiple arrays of hashes together 103 | # 104 | # Use either 'code' or 'alpha_2_code' to match elements between the sets. 105 | # 106 | # Returns a single merged array of hashes. 107 | def flatten_data(arrays) 108 | keys = %w(code alpha_2_code) 109 | Utils.merge_arrays_by_keys(arrays, keys).reject do |hash| 110 | hash['_enabled'] == false 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/carmen/i18n.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'carmen/utils' 3 | 4 | module Carmen 5 | module I18n 6 | 7 | # A simple object to handle I18n translation in simple situations. 8 | class Simple 9 | 10 | DEFAULT_LOCALE = 'en' 11 | 12 | attr_accessor :cache 13 | attr_reader :fallback_locale 14 | attr_reader :locale_paths 15 | 16 | def initialize(*initial_locale_paths) 17 | self.locale = DEFAULT_LOCALE 18 | @fallback_locale = DEFAULT_LOCALE 19 | @locale_paths = [] 20 | initial_locale_paths.each do |path| 21 | append_locale_path(path) 22 | end 23 | end 24 | 25 | def append_locale_path(path) 26 | reset! 27 | @locale_paths << Pathname.new(path) 28 | end 29 | 30 | # Set a new locale 31 | # 32 | # Calling this method will clear the cache. 33 | def locale=(locale) 34 | Thread.current[:carmen_locale] = locale.to_s 35 | end 36 | 37 | def locale 38 | Thread.current[:carmen_locale] 39 | end 40 | 41 | # Retrieve a translation for a key in the following format: 'a.b.c' 42 | # 43 | # This will attempt to find the key in the current locale, and if nothing 44 | # is found, a value found in the fallback locale will be used instead. 45 | def translate(key) 46 | read(key.to_s) 47 | end 48 | 49 | alias :t :translate 50 | 51 | # Clear the cache. Should be called after appending a new locale path 52 | # manually (in case lookups have already occurred.) 53 | # 54 | # When adding a locale path, it's best to use #append_locale_path, which 55 | # resets the cache automatically. 56 | def reset! 57 | @cache = nil 58 | end 59 | 60 | def inspect 61 | "<##{self.class} locale=#{self.locale}>" 62 | end 63 | 64 | def available_locales 65 | load_cache_if_needed 66 | @cache.keys.sort 67 | end 68 | 69 | private 70 | 71 | def read(key) 72 | load_cache_if_needed 73 | translated = read_from_hash(key, @cache[self.locale]) 74 | translated ||= read_from_hash(key, @cache[@fallback_locale]) if self.locale != @fallback_locale 75 | translated 76 | end 77 | 78 | def read_from_hash(key, source_hash) 79 | key.split('.').inject(source_hash) { |hash, key| 80 | hash[key] unless hash.nil? 81 | } 82 | end 83 | 84 | # Load all files located in @locale_paths, merge them, and store the result 85 | # in @cache. 86 | def load_cache_if_needed 87 | return unless @cache.nil? 88 | hashes = load_hashes_for_paths(@locale_paths) 89 | @cache = Utils.deep_hash_merge(hashes) 90 | end 91 | 92 | def load_hashes_for_paths(paths) 93 | paths.collect { |path| 94 | if !File.exist?(path) 95 | fail "Path #{path} not found when loading locale files" 96 | end 97 | Dir[path + '**/*.yml'].map { |file_path| 98 | YAML.load_file(file_path) 99 | } 100 | }.flatten 101 | end 102 | 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /script/update_data.rb: -------------------------------------------------------------------------------- 1 | # DO NOT USE 2 | require 'nokogiri' 3 | require 'yaml' 4 | require 'pathname' 5 | 6 | YAML::ENGINE.yamler = 'psych' 7 | 8 | begin 9 | require 'ftools' 10 | rescue LoadError 11 | require 'fileutils' # ftools is now fileutils in Ruby 1.9 12 | end 13 | 14 | def write_file(data, path) 15 | FileUtils.mkdir_p(File.dirname(path)) 16 | File.open(path + '.yml', 'w') { |f| f.write data.to_yaml } 17 | end 18 | 19 | def write_data_to_path_as_yaml(data, path) 20 | data_keys = %w{alpha_2_code alpha_3_code numeric_code type} 21 | locale_keys = %w{common_name name official_name} 22 | 23 | locale_data = {} 24 | data.each do |element| 25 | locale = {} 26 | locale_keys.each do |key| 27 | locale[key] = element.delete(key) if element.key?(key) 28 | end 29 | parent_key = element['alpha_2_code'] || element['code'] 30 | locale_data[parent_key.downcase] = locale 31 | end 32 | 33 | path_segments = "en/#{path}".split('/').reverse 34 | wrapped_locale_data = path_segments.inject(locale_data) { |hash, path| 35 | { path => hash } 36 | } 37 | 38 | write_file(data, 'iso_data/base/' + path) 39 | write_file(wrapped_locale_data, 'locale/base/en/' + path) 40 | end 41 | 42 | def write_regions_to_path_as_yaml(regions_data, path) 43 | regions_data.each do |subregion_data| 44 | subregions = subregion_data.delete('subregions') 45 | if subregions 46 | subregion_path = path + "/#{subregion_data['code'].downcase}" 47 | write_regions_to_path_as_yaml(subregions, subregion_path) 48 | end 49 | end 50 | write_data_to_path_as_yaml(regions_data, path) 51 | end 52 | 53 | 54 | puts "Downloading data" 55 | 56 | data_path = Pathname.new(File.expand_path('../../iso_data/base', __FILE__)) 57 | tmp_path = data_path + 'tmp' 58 | 59 | FileUtils.mkdir_p(tmp_path) 60 | 61 | files = { 62 | 'iso_3166.xml' => 'http://anonscm.debian.org/gitweb/?p=iso-codes/iso-codes.git;a=blob_plain;f=iso_3166/iso_3166.xml;hb=HEAD', 63 | 'iso_3166_2.xml' => 'http://anonscm.debian.org/gitweb/?p=iso-codes/iso-codes.git;a=blob_plain;f=iso_3166_2/iso_3166_2.xml;hb=HEAD' } 64 | 65 | files.each_pair do |file, url| 66 | `cd #{tmp_path.to_s} && curl -o #{file} "#{url}"` 67 | end 68 | 69 | # countries 70 | puts "Importing countries" 71 | 72 | country_data_path = tmp_path + 'iso_3166.xml' 73 | file = File.open(country_data_path) 74 | doc = Nokogiri::XML(file) 75 | file.close 76 | 77 | countries = [] 78 | doc.xpath('//iso_3166_entry').each do |country| 79 | print '.' 80 | countries << { 81 | 'alpha_2_code' => country['alpha_2_code'], 82 | 'alpha_3_code' => country['alpha_3_code'], 83 | 'numeric_code' => country['numeric_code'], 84 | 'common_name' => country['common_name'], 85 | 'name' => country['name'], 86 | 'official_name' => country['official_name'], 87 | 'type' => 'country' 88 | } 89 | end 90 | 91 | puts 92 | 93 | sorted_countries = countries.sort_by {|e| e['alpha_2_code'] } 94 | write_data_to_path_as_yaml(sorted_countries, 'world') 95 | 96 | # regions 97 | puts "Importing regions" 98 | 99 | region_data_path = tmp_path + 'iso_3166_2.xml' 100 | file = File.open(region_data_path) 101 | doc = Nokogiri::XML(file) 102 | file.close 103 | 104 | warnings = [] 105 | 106 | doc.css('iso_3166_country').each do |country| 107 | code = country['code'].downcase 108 | regions = [] 109 | country.css('iso_3166_subset').each do |subset| 110 | 111 | type = subset['type'].downcase 112 | subregions = subset.css('iso_3166_2_entry').map do |subregion| 113 | data = { 114 | 'code' => subregion['code'].gsub(%r{^#{country['code']}-}, ''), 115 | 'name' => subregion['name'], 116 | 'type' => type 117 | } 118 | 119 | if subregion['parent'] 120 | parent = regions.find do |r| 121 | parent_code = r['code'] 122 | parent_code = parent_code.split(/-| /)[1] if parent_code =~ /-| / 123 | parent_code == subregion['parent'] 124 | end 125 | if parent 126 | parent['subregions'] ||= [] 127 | parent['subregions'] << data 128 | else 129 | warnings << "warning, did not find parent '#{subregion['parent']}'" 130 | warnings << subregion 131 | warnings << regions 132 | warnings << '' 133 | end 134 | else 135 | regions << data 136 | end 137 | end 138 | 139 | end 140 | 141 | sorted_regions = regions.sort_by {|e| e['code'] } 142 | write_regions_to_path_as_yaml(sorted_regions, "world/#{code}") 143 | print '.' 144 | end 145 | 146 | puts 147 | 148 | unless warnings.empty? 149 | puts warnings.join("\n") 150 | end 151 | 152 | FileUtils.rm_rf(tmp_path) 153 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.0.3 (Pending Release) 2 | * Add region search by type with Carmen::RegionCollection#typed (alessandro1997) 3 | * Freeze regions arrays (j15e) 4 | * Add Rangpur bibha, Bangladesh (ecbypi) 5 | * Add Telegana, India (ecbypi) 6 | 7 | ### 1.0.2 8 | * Replace use of UnicodeUtils with ActiveSupport (eikes) 9 | * Update data from upstream sources. 10 | * Fix spelling errors for French subregions (hugolantaume) 11 | * Fix spelling errors for Spanish subregions (nudzg) 12 | * Added missing nl translations for bq, cw, ss and sx (brtdv) 13 | * Moved translations into locale/overlay from locale/base. Base is only for data from iso_codes. 14 | * Changed the official name of Taiwan to Republic of China. 15 | * Fixed the name of Vietnam. 16 | * Add local files for Bangla language (tauhidul35) 17 | 18 | ### 1.0.1 19 | * Avoid raising an exception when calling Querying#coded with a nil code 20 | * Fix a bug where adding additional data paths caused an error when looking up localized names in the base locale data (seangaffney) 21 | * Add Country#numeric_code (stevenharman) 22 | * Fix the name of Lima (goddamnhippie) 23 | * Add south Sudan Swedish translation (barsoom) 24 | * Add Russian translations of Russian Federation (Envek) 25 | * Fix a regression in the localization of Taiwan from the 1.0 rewrite. 26 | * Fix a bug where empty locale files would prevent access to the base data. 27 | * Add a way to ship overlayed data sets with Carmen to allow for differences from the upstream data source. 28 | * Remove Puerto Rico from the list of countries as it is a subregion. 29 | * Restore the naming of Taiwan after a regression to an outdated name. 30 | * Added La Rioja to the list of subregions of Argentina (njacobs1) 31 | * Added APO states to US subregions. 32 | 33 | ### 1.0.0 (April 20, 2013) 34 | * Updated version numbering and pushed 1.0.0pre to v1.0.0. 35 | * Merged in updates to German locations, via a patch from @leifg 36 | 37 | ### 1.0.0pre 38 | * Complete rewrite. New data source and API. Extracting Rails view 39 | helpers into seperate gem. 40 | 41 | ### 0.2.12 42 | * Republish the gem with Ruby 1.8.7. 43 | 44 | ### 0.2.11 45 | * Remove Jeweler and release new version. 46 | 47 | ### 0.2.10 48 | * Generate the gem with Ruby 1.8.7 to try to fix YAML library 49 | incompatibilities. 50 | 51 | ### 0.2.9 52 | * Preserve order of priority_countries in country_select (castiglione) 53 | * Add Finnish localization (marjakapyaho) 54 | * Update a few contru names to match ISO naming (belt) 55 | * Fall back to default locale if selected locale is missing (twinge) 56 | * Added Russian country translations (grlm) 57 | * Added South Sudan as a country (edshadi) 58 | * Renamed Libyan Arab Jamahiriya to Libya (mdimas) 59 | * Fixed an issue where trying find a country for a blank string would 60 | match everything (smathieu) 61 | * Added Italian country names (Arkham) 62 | * Add Polish, slovak and czech translations (Pajk) 63 | * Various corrections to country names (wolframarnold) 64 | * Add Chinese counties (liwh) 65 | * Add Dutch province names (ariejan) 66 | * Add Saint Barthelemy (BL) and Saint Martin (French Part) (MF) (nengxu) 67 | * Add Japanese countries localization (bonsaiben) 68 | * Prevent Carmen::state_name('NO','NO') from crashing (mhourahine) 69 | * Change "Taiwan, Province of China" to "Taiwan" (camilleroux) 70 | * Add spanish translation for countries (federomero) 71 | 72 | ### 0.2.8 73 | * Use a shorter name for US Armed Forces States (cgs) 74 | * Added Gujarat to the list of states in India (swaroopch) 75 | * Added American Samoa to the list of US States 76 | * Added Dutch country translations (Arie) 77 | * Added Kosovo to German Translation (Christopher Thorpe) 78 | * Added the ability to list countries at the top of the list (jjthrash) 79 | * Added country names in Hindi (sukeerthiadiga) 80 | 81 | ### 0.2.7 82 | * Fix a gemspec disaster. 83 | 84 | ### 0.2.6 (pulled) 85 | * Suppress a deprecation warning in Rails 3 (anupamc) 86 | * Remove init.rb altogether and use requires under Rails 87 | * Added Indian states and union territories (orthodoc) 88 | 89 | ### 0.2.5 90 | * Data corrections (mikepinde) 91 | 92 | ### 0.2.4 93 | * Fixed autoloading under Rails 3 94 | 95 | ### 0.2.2 96 | * Added state and country exclusion (kalafut) 97 | 98 | ### 0.2.1 99 | * Added regions for New Zealand (yehezkielbs) 100 | 101 | ### 0.2.0 102 | * Merge in Maximilian Schulz's locale fork, refactor internals to better support locales, and update documentation. 103 | * Remove Carmen::STATES and Carmen::COUNTRIES constants in favor of module instance variables and proper accessors. 104 | * Add a test_helper and remove dependency on RubyGems. 105 | 106 | ### 0.1.3 107 | * DEPRECATE Carmen::COUNTRIES in favor of Carmen.countries 108 | -------------------------------------------------------------------------------- /spec/carmen/region_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require 'spec_helper' 4 | 5 | describe Carmen::Region do 6 | 7 | describe 'basic methods' do 8 | before do 9 | @airstrip_one = Carmen::Country.coded('OC').subregions.first 10 | end 11 | 12 | it 'has a reasonable inspect value' do 13 | @airstrip_one.inspect.must_equal '<#Carmen::Region name="Airstrip One" type="province">' 14 | end 15 | 16 | it 'has a reasonable explicit string conversion' do 17 | "#{@airstrip_one}".must_equal 'Airstrip One' 18 | end 19 | 20 | it "has the correct subregion path" do 21 | @airstrip_one.subregion_data_path.must_equal "world/oc/ao.yml" 22 | end 23 | 24 | it "knows if it has subregions" do 25 | @airstrip_one.subregions?.must_equal true 26 | end 27 | 28 | it "has a path" do 29 | @airstrip_one.path.must_equal 'world.oc.ao' 30 | end 31 | 32 | describe "subregions" do 33 | it "is frozen" do 34 | subregions = Carmen::Country.coded('OC').subregions 35 | 36 | assert_raises RuntimeError do 37 | subregions.clear 38 | end 39 | end 40 | end 41 | 42 | describe "subregion" do 43 | before do 44 | @london = @airstrip_one.subregions.first 45 | end 46 | 47 | it "has a name" do 48 | @london.name.must_equal "London" 49 | end 50 | 51 | it "has a code" do 52 | @london.code.must_equal 'LO' 53 | end 54 | 55 | it "has a type" do 56 | @london.type.must_equal 'city' 57 | end 58 | 59 | it "has a parent" do 60 | @london.parent.must_equal @airstrip_one 61 | end 62 | end 63 | end 64 | 65 | describe "querying" do 66 | before do 67 | @world = Carmen::World.instance 68 | end 69 | 70 | it 'can find subregions by exact name' do 71 | eastasia = @world.subregions.named('Eastasia') 72 | eastasia.name.must_equal('Eastasia') 73 | end 74 | 75 | it "can find subregions by case-insensitive search by default" do 76 | eurasia = @world.subregions.named('eUrAsIa') 77 | eurasia.instance_of?(Carmen::Country).must_equal true 78 | eurasia.name.must_equal 'Eurasia' 79 | end 80 | 81 | it "can find subregions optionally case-sensitively" do 82 | oceania = @world.subregions.named('oCeAnIa', :case => true) 83 | oceania.must_equal nil 84 | oceania = @world.subregions.named('Oceania', :case => true) 85 | oceania.instance_of?(Carmen::Country).must_equal true 86 | oceania.name.must_equal 'Oceania' 87 | end 88 | 89 | it "can find subregions with fuzzy (substring) matching optionally" do 90 | eastasia = @world.subregions.named('East', :fuzzy => true) 91 | eastasia.instance_of?(Carmen::Country).must_equal true 92 | eastasia.name.must_equal 'Eastasia' 93 | end 94 | 95 | it 'can find subregions by name using a regex' do 96 | eastasia = @world.subregions.named(/Eastasia/) 97 | eastasia.name.must_equal('Eastasia') 98 | end 99 | 100 | it 'can find subregions by name using a case-insensitive regex' do 101 | eastasia = @world.subregions.named(/eastasia/i) 102 | eastasia.name.must_equal('Eastasia') 103 | end 104 | 105 | it 'handles querying for a nil code safely' do 106 | @world.subregions.coded(nil).must_equal nil 107 | end 108 | 109 | it 'handles querying for a nil name safely' do 110 | @world.subregions.named(nil).must_equal nil 111 | end 112 | 113 | describe 'unicode character handling' do 114 | before do 115 | Carmen.i18n_backend.locale = :de 116 | end 117 | 118 | after do 119 | Carmen.i18n_backend.locale = :en 120 | end 121 | 122 | it 'can find a country using unicode characters' do 123 | large = @world.subregions.named('Das großartige Staat von Eurasia') 124 | large.instance_of?(Carmen::Country).must_equal true 125 | large.name.must_equal('Das großartige Staat von Eurasia') 126 | end 127 | 128 | it 'can find a country using unicode characters' do 129 | large = @world.subregions.named('gross', :fuzzy => true) 130 | large.instance_of?(Carmen::Country).must_equal true 131 | large.name.must_equal('Das großartige Staat von Eurasia') 132 | end 133 | 134 | end 135 | end 136 | 137 | class SortTestRegion < Carmen::Region 138 | def initialize(data={}, parent=nil) 139 | super 140 | @name = data['name'] 141 | end 142 | def name 143 | @name 144 | end 145 | end 146 | 147 | describe "Sorting" do 148 | it 'does a comparison' do 149 | germany = SortTestRegion.new('name' => 'Germany') 150 | guatemala = SortTestRegion.new('name' => 'Guatemala') 151 | (germany <=> guatemala).must_equal -1 152 | (guatemala <=> germany).must_equal 1 153 | (germany <=> germany).must_equal 0 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Carmen 2 | 3 | > A repository of geographic regions for Ruby 4 | 5 | [![Build Status](https://secure.travis-ci.org/jim/carmen.png?branch=master)](http://travis-ci.org/jim/carmen) 6 | 7 | Carmen features the following: 8 | 9 | * Clean API 10 | * Complete countries & regions data from the iso-codes Debian package 11 | * A sane approach to internationalization 12 | 13 | ## Ruby on Rails 14 | 15 | If you are using Carmen with Rails, you should check out the [carmen-rails](http://github.com/jim/carmen-rails) library. 16 | 17 | # How to Use Carmen 18 | 19 | Carmen is designed to make it easy to access Country and region data. 20 | You can query for a country by name or code: 21 | 22 | require 'carmen' 23 | include Carmen 24 | 25 | us = Country.named('United States') 26 | => <#Carmen::Country name="United States"> 27 | 28 | A Country object has some attributes that may be useful: 29 | 30 | us.alpha_2_code 31 | => 'US' 32 | 33 | us.alpha_3_code 34 | => 'USA' 35 | 36 | us.code # alias for alpha_2_code 37 | => 'US' 38 | 39 | us.official_name 40 | => "United States of America" 41 | 42 | A `Country` (and its subregions) can contain subregions. In the US these are states, but other countries have other types of regions: 43 | 44 | us.subregions? 45 | => true 46 | 47 | us.subregions.first 48 | => <#Carmen::Region name="Alabama" type="state"> 49 | 50 | `Country#subregions` returns a `RegionCollection`, which can be queried 51 | similarly to a `Country` to find, for instance, a specific state: 52 | 53 | illinois = us.subregions.coded('IL') 54 | => <#Carmen::Region "Illinois"> 55 | 56 | You can also find all subregions with a specific type: 57 | 58 | states = us.subregions.typed('state') 59 | => [<#Carmen::Region name="Alaska" type="state">, <#Carmen::Region name="Alabama" type="state">, ...] 60 | 61 | Subregions support a smaller set of attributes than countries: 62 | 63 | illinois.name 64 | => "Illinois" 65 | 66 | illinois.code 67 | => "IL" 68 | 69 | illinois.type 70 | => "state" 71 | 72 | Some subregions may contain additional subregions. An example of this is Spain: 73 | 74 | spain = Country.named('Spain') 75 | andalucia = spain.subregions.first 76 | => <#Carmen::Region name="Andalucía" type="autonomous community"> 77 | 78 | andalucia.subregions? 79 | => true 80 | 81 | andalucia.subregions.first 82 | => <#Carmen::Region name="Almería" type="province"> 83 | 84 | ## How Carmen organizes data 85 | 86 | In order to facilitate support for I18n, Carmen stores the structure of regions 87 | separately from the strings that represent a region's names. The default data 88 | that ships with Carmen is in the iso_data and locale directories, 89 | respectively. 90 | 91 | ## Overriding structural data 92 | 93 | You might want to tweak the data that Carmen provides for a variety of reasons. Carmen 94 | maintains an array of paths to load data from in: `Carmen.data_paths`. The structure of 95 | files in each of these paths should mirror those in the `iso_data` path Carmen ships with. 96 | 97 | To add a new country to the system, you would create a directory (let's use `my_data` as an example), 98 | and create a `world.yml` file inside it. Then add the path to Carman: 99 | 100 | Carmen.append_data_path File.expand_path('../my_data', __FILE__) 101 | 102 | Elements within the data files are identified using their `code` values (or, in the case of countries, `alpha_2_code`). Create a new block for the country you wish to add inside `my_data/world.yml`: 103 | 104 | --- 105 | - alpha_2_code: ZZ 106 | alpha_3_code: ZZZ 107 | numeric_code: "999" 108 | type: country 109 | 110 | Now, modify the fields you wish to change, and delete the others. Be sure to specify `alpha_2_code` for countries and `code` for subregions, as those values are used internally by Carmen to match your customized data with the corresponding data in the default dataset. 111 | 112 | Now, Carmen will reflect your personal view of the world: 113 | 114 | Carmen::Country.coded('ZZ').type 115 | => "country" 116 | 117 | You will also want to create a localization file with the names for the new 118 | region. See the section 'Customizing an existing locale', below. 119 | 120 | ### Modifying existing elements 121 | 122 | Existing regions can be modified by copying their existing data block into 123 | a new file at the correct overlay path, and modifying the values as desired. 124 | 125 | ### Disabling elements 126 | 127 | It is also possible to remove an element from the dataset by setting its `_enabled` value to [anything YAML considers false](http://yaml.org/type/bool.html), such as 'false' or 'no': 128 | 129 | - alpha_2_code: EU 130 | _enabled: false 131 | 132 | This will cause Carmen to not return that element from any query: 133 | 134 | Carmen::Country.coded('EU') 135 | => nil 136 | 137 | ## Localization 138 | 139 | Carmen ships with very simple I18n support. You can tell Carmen to use your own 140 | I18n backend: 141 | 142 | Carmen.i18n_backend = YourI18nBackend.new 143 | 144 | The object used as a backend must respond to `t` with a single argument (the 145 | key being looked up). This key will look something like `world.us.il.name`. 146 | 147 | ## Setting the locale 148 | 149 | If you use the built in I18n support, you can set the locale: 150 | 151 | Carmen.i18n_backend.locale = :es 152 | 153 | Each region is assigned 154 | a localization key based on the formula world.PARENT\_CODE.CODE. The 155 | key used for the United States is `world.us`. 156 | 157 | ## Customizing an existing locale 158 | 159 | The library ships with a set of YAML files that contain localizations of many 160 | country names (and some states). If you want to override any of these values, 161 | create a YAML file that contains a nested hash structure, where each segment of 162 | the key is a hash: 163 | 164 | en: 165 | world: 166 | us: 167 | official_name: These Crazy States 168 | 169 | This file can live anywhere, but it is recommended that it be stored in 170 | a structure similar to the one Carmen uses for its locale storage. 171 | 172 | To tell Carmen to load this file, add the directory it is contained in to the 173 | set of locale paths used by the backend: 174 | 175 | Carmen.i18n_backend.append_locale_path('/path/to/your/locale/files') 176 | 177 | If you are using your own backend, then follow the steps necessary to have it 178 | load your additional files instead. 179 | 180 | 181 | ## Contributing to Carmen 182 | 183 | Please read [Contributing Data](https://github.com/jim/carmen/wiki/Contributing-Data) before making any changes to the project's data. It will save you (and me) a bunch of time! 184 | 185 | ## Extensions 186 | 187 | [Jacob Morris](https://github.com/jacobsimeon) has created [a plugin for Carmen that adds support for demonyms](https://github.com/jacobsimeon/carmen-demonyms). 188 | 189 | [Cyle Hunter](https://github.com/nozpheratu) has created [a plugin that adds ISO 4217 currency names to Carmen::Country](https://github.com/nozpheratu/carmen-iso-4217). 190 | --------------------------------------------------------------------------------