globalphone 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Sam Stephenson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,163 @@
1
+ # GlobalPhone
2
+
3
+ GlobalPhone parses, validates, and formats local and international phone numbers according to the [E.164 standard](http://en.wikipedia.org/wiki/E.164).
4
+
5
+ **Store and display phone numbers in your app.** Accept phone number input in national or international format. Convert phone numbers to international strings (`+13125551212`) for storage and retrieval. Present numbers in national format (`(312) 555-1212`) in your UI.
6
+
7
+ **Designed with the future in mind.** GlobalPhone uses format specifications from Google's open-source [libphonenumber](http://code.google.com/p/libphonenumber/) database. No need to upgrade the library when a new phone format is introduced—just generate a new copy of the database and check it into your app.
8
+
9
+ **Pure Ruby. No dependencies.** GlobalPhone is designed for Ruby 1.9.3 and up. (Works in 1.8.7, too—just bring your own `json` gem.)
10
+
11
+ ## Installation
12
+
13
+ 1. Add the `global_phone` gem to your app. For example, using Bundler:
14
+
15
+ $ echo "gem 'global_phone'" >> Gemfile
16
+ $ bundle install
17
+
18
+ 2. Use `global_phone_dbgen` to convert Google's libphonenumber `PhoneNumberMetaData.xml` file into a JSON database for GlobalPhone.
19
+
20
+ $ gem install global_phone_dbgen
21
+ $ global_phone_dbgen > db/global_phone.json
22
+
23
+ 3. Tell GlobalPhone where to find the database. For example, in a Rails app, create an initializer in `config/initializers/global_phone.rb`:
24
+
25
+ ```ruby
26
+ require 'global_phone'
27
+ GlobalPhone.db_path = Rails.root.join('db/global_phone.json')
28
+ ```
29
+
30
+ ## Examples
31
+
32
+ Parse an international number string into a `GlobalPhone::Number` object:
33
+
34
+ ```ruby
35
+ number = GlobalPhone.parse('+1-312-555-1212')
36
+ # => #<GlobalPhone::Number territory=#<GlobalPhone::Territory country_code=1 name=US> national_string="3125551212">
37
+ ```
38
+
39
+ Query the country code and likely territory name of the number:
40
+
41
+ ```ruby
42
+ number.country_code
43
+ # => "1"
44
+
45
+ number.territory.name
46
+ # => "US"
47
+ ```
48
+
49
+ Present the number in national and international formats:
50
+
51
+ ```ruby
52
+ number.national_format
53
+ # => "(312) 555-1212"
54
+
55
+ number.international_format
56
+ # => "+1 312-555-1212"
57
+ ```
58
+
59
+ Is the number valid? (Note: this is not definitive. For example, the number here is "valid" by format, but there are no US numbers that start with 555. The `valid?` method may return false positives, but *should not* return false negatives unless the database is out of date.)
60
+
61
+ ```ruby
62
+ number.valid?
63
+ # => true
64
+ ```
65
+
66
+ Get the number's normalized E.164 international string:
67
+
68
+ ```ruby
69
+ number.international_string
70
+ # => "+13125551212"
71
+ ```
72
+
73
+ Parse a number in national format for a given territory:
74
+
75
+ ```ruby
76
+ number = GlobalPhone.parse("(0) 20-7031-3000", :gb)
77
+ # => #<GlobalPhone::Number territory=#<GlobalPhone::Territory country_code=44 name=GB> national_string="2070313000">
78
+ ```
79
+
80
+ Parse an international number using a territory's international dialing prefix:
81
+
82
+ ```ruby
83
+ number = GlobalPhone.parse("00 1 3125551212", :gb)
84
+ # => #<GlobalPhone::Number territory=#<GlobalPhone::Territory country_code=1 name=US> national_string="3125551212">
85
+ ```
86
+
87
+ Set the default territory to Great Britain (territory names are [ISO 3166-1 Alpha-2](http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) codes):
88
+
89
+ ```ruby
90
+ GlobalPhone.default_territory_name = :gb
91
+ # => :gb
92
+
93
+ GlobalPhone.parse("(0) 20-7031-3000")
94
+ # => #<GlobalPhone::Number territory=#<GlobalPhone::Territory country_code=44 name=GB> national_string="2070313000">
95
+ ```
96
+
97
+ Shortcuts for validating a phone number:
98
+
99
+ ```ruby
100
+ GlobalPhone.validate("+1 312-555-1212")
101
+ # => true
102
+
103
+ GlobalPhone.validate("+442070313000")
104
+ # => true
105
+
106
+ GlobalPhone.validate("(0) 20-7031-3000")
107
+ # => false
108
+
109
+ GlobalPhone.validate("(0) 20-7031-3000", :gb)
110
+ # => true
111
+ ```
112
+
113
+ Shortcuts for normalizing a phone number in E.164 format:
114
+
115
+ ```ruby
116
+ GlobalPhone.normalize("(312) 555-1212")
117
+ # => "+13125551212"
118
+
119
+ GlobalPhone.normalize("+442070313000")
120
+ # => "+442070313000"
121
+
122
+ GlobalPhone.normalize("(0) 20-7031-3000")
123
+ # => nil
124
+
125
+ GlobalPhone.normalize("(0) 20-7031-3000", :gb)
126
+ # => "+442070313000"
127
+ ```
128
+
129
+ ## Caveats
130
+
131
+ GlobalPhone currently does not parse emergency numbers or SMS short code numbers.
132
+
133
+ Validation is not definitive and may return false positives, but *should not* return false negatives unless the database is out of date.
134
+
135
+ Territory heuristics are imprecise. Parsing a number will usually result in the territory being set to the primary territory of the region. For example, Canadian numbers will be parsed with a territory of `US`. (In most cases this does not matter, but if your application needs to perform geolocation using phone numbers, GlobalPhone may not be a good fit.)
136
+
137
+ ## Development
138
+
139
+ The GlobalPhone source code is [hosted on GitHub](https://github.com/sstephenson/global_phone). You can check out a copy of the latest code using Git:
140
+
141
+ $ git clone https://github.com/sstephenson/global_phone.git
142
+
143
+ If you've found a bug or have a question, please open an issue on the [issue tracker](https://github.com/sstephenson/global_phone/issues). Or, clone the GlobalPhone repository, write a failing test case, fix the bug, and submit a pull request.
144
+
145
+ GlobalPhone is heavily inspired by Andreas Gal's [PhoneNumber.js](https://github.com/andreasgal/PhoneNumber.js) library.
146
+
147
+ ### Version History
148
+
149
+ **1.0.1** (May 29, 2013)
150
+
151
+ * GlobalPhone::Number#to_s returns the E.164 international string.
152
+ * Ensure GlobalPhone::Number always returns strings for #national_format, #international_format, and #international_string, regardless of validity.
153
+ * Relax format restrictions to more loosely match available national number patterns.
154
+
155
+ **1.0.0** (May 28, 2013)
156
+
157
+ * Initial public release.
158
+
159
+ ### License
160
+
161
+ Copyright &copy; 2013 Sam Stephenson
162
+
163
+ Released under the MIT license. See [`LICENSE`](LICENSE) for details.
@@ -0,0 +1,10 @@
1
+ require 'global_phone/context'
2
+
3
+ module GlobalPhone
4
+ VERSION = '1.0.1'
5
+
6
+ class Error < ::StandardError; end
7
+ class NoDatabaseError < Error; end
8
+
9
+ extend Context
10
+ end
@@ -0,0 +1,36 @@
1
+ require 'global_phone/database'
2
+
3
+ module GlobalPhone
4
+ module Context
5
+ attr_accessor :db_path
6
+
7
+ def db
8
+ @db ||= begin
9
+ raise NoDatabaseError, "set `db_path=' first" unless db_path
10
+ Database.load_file(db_path)
11
+ end
12
+ end
13
+
14
+ def default_territory_name
15
+ @default_territory_name ||= :US
16
+ end
17
+
18
+ def default_territory_name=(territory_name)
19
+ @default_territory_name = territory_name.to_s.intern
20
+ end
21
+
22
+ def parse(string, territory_name = default_territory_name)
23
+ db.parse(string, territory_name)
24
+ end
25
+
26
+ def normalize(string, territory_name = default_territory_name)
27
+ number = parse(string, territory_name)
28
+ number.international_string if number
29
+ end
30
+
31
+ def validate(string, territory_name = default_territory_name)
32
+ number = parse(string, territory_name)
33
+ number && number.valid?
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ require 'global_phone/parsing'
2
+ require 'global_phone/region'
3
+
4
+ module GlobalPhone
5
+ class Database
6
+ include Parsing
7
+
8
+ def self.load_file(filename)
9
+ load(File.read(filename))
10
+ end
11
+
12
+ def self.load(json)
13
+ require 'json'
14
+ new(JSON.parse(json))
15
+ end
16
+
17
+ attr_reader :regions
18
+
19
+ def initialize(record_data)
20
+ @regions = record_data.map { |data| Region.new(data) }
21
+ @territories_by_name = {}
22
+ end
23
+
24
+ def region(country_code)
25
+ regions_by_country_code[country_code.to_s]
26
+ end
27
+
28
+ def territory(name)
29
+ name = name.to_s.upcase
30
+ @territories_by_name[name] ||= if region = region_for_territory(name)
31
+ region.territory(name)
32
+ end
33
+ end
34
+
35
+ def inspect
36
+ "#<#{self.class.name}>"
37
+ end
38
+
39
+ protected
40
+ def regions_by_country_code
41
+ @regions_by_country_code ||= Hash[*regions.map { |r| [r.country_code, r] }.flatten]
42
+ end
43
+
44
+ def region_for_territory(name)
45
+ regions.find { |r| r.has_territory?(name) }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ require 'global_phone/record'
2
+
3
+ module GlobalPhone
4
+ class Format < Record
5
+ field 0, :pattern do |p| /^#{p}$/ end
6
+ field 1, :national_format_rule
7
+ field 2, :leading_digits do |d| /^#{d}/ end
8
+ field 3, :national_prefix_formatting_rule
9
+ field 4, :international_format_rule, :fallback => :national_format_rule
10
+
11
+ def match(national_string, match_leading_digits = true)
12
+ return false if match_leading_digits && leading_digits && national_string !~ leading_digits
13
+ national_string =~ pattern
14
+ end
15
+
16
+ def format_replacement_string(type)
17
+ format_rule = send(:"#{type}_format_rule")
18
+ format_rule.to_s.gsub("$", "\\") unless format_rule == "NA"
19
+ end
20
+
21
+ def apply(national_string, type)
22
+ if replacement = format_replacement_string(type)
23
+ national_string.gsub(pattern, replacement)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,91 @@
1
+ require 'forwardable'
2
+
3
+ module GlobalPhone
4
+ class Number
5
+ extend Forwardable
6
+
7
+ E161_MAPPING = Hash[*"a2b2c2d3e3f3g4h4i4j5k5l5m6n6o6p7q7r7s7t8u8v8w9x9y9z9".split("")]
8
+ VALID_ALPHA_CHARS = /[a-zA-Z]/
9
+ LEADING_PLUS_CHARS = /^\++/
10
+ NON_DIALABLE_CHARS = /[^,#+\*\d]/
11
+ SPLIT_FIRST_GROUP = /^(\d+)(.*)$/
12
+
13
+ def self.normalize(string)
14
+ string.to_s.
15
+ gsub(VALID_ALPHA_CHARS) { |c| E161_MAPPING[c.downcase] }.
16
+ gsub(LEADING_PLUS_CHARS, "+").
17
+ gsub(NON_DIALABLE_CHARS, "")
18
+ end
19
+
20
+ attr_reader :territory, :national_string
21
+
22
+ def_delegator :territory, :region
23
+ def_delegator :territory, :country_code
24
+ def_delegator :territory, :national_prefix
25
+ def_delegator :territory, :national_pattern
26
+
27
+ def initialize(territory, national_string)
28
+ @territory = territory
29
+ @national_string = national_string
30
+ end
31
+
32
+ def national_format
33
+ @national_format ||= begin
34
+ if format && result = format.apply(national_string, :national)
35
+ apply_national_prefix_format(result)
36
+ else
37
+ national_string
38
+ end
39
+ end
40
+ end
41
+
42
+ def international_string
43
+ @international_string ||= international_format.gsub(NON_DIALABLE_CHARS, "")
44
+ end
45
+
46
+ def international_format
47
+ @international_format ||= begin
48
+ if format && formatted_number = format.apply(national_string, :international)
49
+ "+#{country_code} #{formatted_number}"
50
+ else
51
+ "+#{country_code} #{national_string}"
52
+ end
53
+ end
54
+ end
55
+
56
+ def valid?
57
+ !!(format && national_string =~ national_pattern)
58
+ end
59
+
60
+ def inspect
61
+ "#<#{self.class.name} territory=#{territory.inspect} national_string=#{national_string.inspect}>"
62
+ end
63
+
64
+ def to_s
65
+ international_string
66
+ end
67
+
68
+ protected
69
+ def format
70
+ @format ||= find_format_for(national_string)
71
+ end
72
+
73
+ def find_format_for(string)
74
+ region.formats.detect { |format| format.match(string) } ||
75
+ region.formats.detect { |format| format.match(string, false) }
76
+ end
77
+
78
+ def apply_national_prefix_format(result)
79
+ prefix = national_prefix_formatting_rule
80
+ return result unless prefix && match = result.match(SPLIT_FIRST_GROUP)
81
+
82
+ prefix = prefix.gsub("$NP", national_prefix)
83
+ prefix = prefix.gsub("$FG", match[1])
84
+ result = "#{prefix} #{match[2]}"
85
+ end
86
+
87
+ def national_prefix_formatting_rule
88
+ format.national_prefix_formatting_rule || territory.national_prefix_formatting_rule
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,52 @@
1
+ require 'global_phone/number'
2
+ require 'global_phone/utils'
3
+
4
+ module GlobalPhone
5
+ module Parsing
6
+ def parse(string, territory_name)
7
+ string = Number.normalize(string)
8
+ territory = self.territory(territory_name)
9
+ raise ArgumentError, "unknown territory `#{territory_name}'" unless territory
10
+
11
+ if starts_with_plus?(string)
12
+ parse_international_string(string)
13
+ elsif string =~ territory.international_prefix
14
+ string = strip_international_prefix(territory, string)
15
+ parse_international_string(string)
16
+ else
17
+ territory.parse_national_string(string)
18
+ end
19
+ end
20
+
21
+ def parse_international_string(string)
22
+ string = Number.normalize(string)
23
+ string = strip_leading_plus(string) if starts_with_plus?(string)
24
+
25
+ if region = region_for_string(string)
26
+ region.parse_national_string(string)
27
+ end
28
+ end
29
+
30
+ protected
31
+ def starts_with_plus?(string)
32
+ string[0, 1] == "+"
33
+ end
34
+
35
+ def strip_leading_plus(string)
36
+ string[1..-1]
37
+ end
38
+
39
+ def strip_international_prefix(territory, string)
40
+ string.sub(territory.international_prefix, "")
41
+ end
42
+
43
+ def region_for_string(string)
44
+ candidates = country_code_candidates_for(string)
45
+ Utils.map_detect(candidates) { |country_code| region(country_code) }
46
+ end
47
+
48
+ def country_code_candidates_for(string)
49
+ (1..3).map { |length| string[0, length] }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,26 @@
1
+ require 'forwardable'
2
+
3
+ module GlobalPhone
4
+ class Record
5
+ extend Forwardable
6
+
7
+ def self.field(index, name, options = {}, &block)
8
+ if block
9
+ transform_method_name = :"transform_field_#{name}"
10
+ define_method(transform_method_name, block)
11
+ end
12
+
13
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
14
+ def #{name}
15
+ value = @data[#{index.inspect}]
16
+ #{"value = #{transform_method_name}(value) if value" if block}
17
+ value #{"|| #{options[:fallback]}" if options[:fallback]}
18
+ end
19
+ RUBY
20
+ end
21
+
22
+ def initialize(data)
23
+ @data = data
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,62 @@
1
+ require 'global_phone/format'
2
+ require 'global_phone/record'
3
+ require 'global_phone/territory'
4
+ require 'global_phone/utils'
5
+
6
+ module GlobalPhone
7
+ class Region < Record
8
+ field 0, :country_code
9
+ field 1, :format_record_data
10
+ field 2, :territory_record_data
11
+ field 3, :international_prefix do |p| /^(?:#{p})/ end
12
+ field 4, :national_prefix
13
+ field 5, :national_prefix_for_parsing do |p| /^(?:#{p})/ end
14
+ field 6, :national_prefix_transform_rule
15
+
16
+ def formats
17
+ @formats ||= format_record_data.map { |data| Format.new(data) }
18
+ end
19
+
20
+ def territories
21
+ @territories ||= territory_record_data.map { |data| Territory.new(data, self) }
22
+ end
23
+
24
+ def territory(name)
25
+ name = name.to_s.upcase
26
+ territories.detect { |region| region.name == name }
27
+ end
28
+
29
+ def has_territory?(name)
30
+ territory_names.include?(name.to_s.upcase)
31
+ end
32
+
33
+ def parse_national_string(string)
34
+ string = Number.normalize(string)
35
+ if starts_with_country_code?(string)
36
+ string = strip_country_code(string)
37
+ find_first_parsed_national_string_from_territories(string)
38
+ end
39
+ end
40
+
41
+ def inspect
42
+ "#<#{self.class.name} country_code=#{country_code} territories=[#{territory_names.join(",")}]>"
43
+ end
44
+
45
+ protected
46
+ def territory_names
47
+ territory_record_data.map(&:first)
48
+ end
49
+
50
+ def starts_with_country_code?(string)
51
+ string.index(country_code) == 0
52
+ end
53
+
54
+ def strip_country_code(string)
55
+ string[country_code.length..-1]
56
+ end
57
+
58
+ def find_first_parsed_national_string_from_territories(string)
59
+ Utils.map_detect(territories) { |territory| territory.parse_national_string(string) }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,58 @@
1
+ require 'global_phone/number'
2
+ require 'global_phone/record'
3
+
4
+ module GlobalPhone
5
+ class Territory < Record
6
+ field 0, :name
7
+ field 1, :possible_pattern do |p| /^#{p}$/ end
8
+ field 2, :national_pattern do |p| /^#{p}$/ end
9
+ field 3, :national_prefix_formatting_rule
10
+
11
+ attr_reader :region
12
+
13
+ def_delegator :region, :country_code
14
+ def_delegator :region, :international_prefix
15
+ def_delegator :region, :national_prefix
16
+ def_delegator :region, :national_prefix_for_parsing
17
+ def_delegator :region, :national_prefix_transform_rule
18
+
19
+ def initialize(data, region)
20
+ super(data)
21
+ @region = region
22
+ end
23
+
24
+ def parse_national_string(string)
25
+ string = normalize(string)
26
+ Number.new(self, string) if possible?(string)
27
+ end
28
+
29
+ def inspect
30
+ "#<#{self.class.name} country_code=#{country_code} name=#{name}>"
31
+ end
32
+
33
+ protected
34
+ def strip_national_prefix(string)
35
+ if national_prefix_for_parsing
36
+ transform_rule = national_prefix_transform_rule || ""
37
+ transform_rule = transform_rule.gsub("$", "\\")
38
+ string_without_prefix = string.sub(national_prefix_for_parsing, transform_rule)
39
+ elsif starts_with_national_prefix?(string)
40
+ string_without_prefix = string[national_prefix.length..-1]
41
+ end
42
+
43
+ possible?(string_without_prefix) ? string_without_prefix : string
44
+ end
45
+
46
+ def normalize(string)
47
+ strip_national_prefix(Number.normalize(string))
48
+ end
49
+
50
+ def possible?(string)
51
+ string =~ possible_pattern
52
+ end
53
+
54
+ def starts_with_national_prefix?(string)
55
+ national_prefix && string.index(national_prefix) == 0
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,14 @@
1
+ module GlobalPhone
2
+ module Utils
3
+ extend self
4
+
5
+ def map_detect(collection)
6
+ collection.each do |value|
7
+ if result = yield(value)
8
+ return result
9
+ end
10
+ end
11
+ nil
12
+ end
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: globalphone
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sam Stephenson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-03-01 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: GlobalPhone parses, validates, and formats local and international phone
15
+ numbers according to the E.164 standard using the rules specified in Google's libphonenumber
16
+ database.
17
+ email:
18
+ - sstephenson@gmail.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - README.md
24
+ - LICENSE
25
+ - lib/global_phone/context.rb
26
+ - lib/global_phone/database.rb
27
+ - lib/global_phone/format.rb
28
+ - lib/global_phone/number.rb
29
+ - lib/global_phone/parsing.rb
30
+ - lib/global_phone/record.rb
31
+ - lib/global_phone/region.rb
32
+ - lib/global_phone/territory.rb
33
+ - lib/global_phone/utils.rb
34
+ - lib/global_phone.rb
35
+ homepage: https://github.com/Lundalogik/globalphone_fork
36
+ licenses:
37
+ - MIT
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 1.8.24
57
+ signing_key:
58
+ specification_version: 3
59
+ summary: Parse, validate, and format phone numbers using Google's libphonenumber database
60
+ test_files: []