phoner 1.0

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.
Files changed (80) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +8 -0
  3. data/Gemfile.lock +31 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +184 -0
  6. data/Rakefile +12 -0
  7. data/lib/phoner.rb +10 -0
  8. data/lib/phoner/country.rb +120 -0
  9. data/lib/phoner/data/phone_countries.yml +1691 -0
  10. data/lib/phoner/phone.rb +222 -0
  11. data/lib/phoner/version.rb +3 -0
  12. data/phoner.gemspec +23 -0
  13. data/test/countries/ae_test.rb +15 -0
  14. data/test/countries/af_test.rb +12 -0
  15. data/test/countries/al_test.rb +12 -0
  16. data/test/countries/ar_test.rb +12 -0
  17. data/test/countries/at_test.rb +14 -0
  18. data/test/countries/au_test.rb +48 -0
  19. data/test/countries/ba_test.rb +9 -0
  20. data/test/countries/bd_test.rb +17 -0
  21. data/test/countries/be_test.rb +120 -0
  22. data/test/countries/bg_test.rb +13 -0
  23. data/test/countries/bo_test.rb +12 -0
  24. data/test/countries/br_test.rb +12 -0
  25. data/test/countries/bt_test.rb +9 -0
  26. data/test/countries/by_test.rb +12 -0
  27. data/test/countries/bz_test.rb +12 -0
  28. data/test/countries/ca_test.rb +20 -0
  29. data/test/countries/cr_test.rb +12 -0
  30. data/test/countries/cy_test.rb +12 -0
  31. data/test/countries/cz_test.rb +12 -0
  32. data/test/countries/de_test.rb +18 -0
  33. data/test/countries/dk_test.rb +12 -0
  34. data/test/countries/dz_test.rb +12 -0
  35. data/test/countries/ec_test.rb +12 -0
  36. data/test/countries/ee_test.rb +12 -0
  37. data/test/countries/eg_test.rb +9 -0
  38. data/test/countries/et_test.rb +11 -0
  39. data/test/countries/fi_test.rb +12 -0
  40. data/test/countries/fr_test.rb +22 -0
  41. data/test/countries/gb_test.rb +262 -0
  42. data/test/countries/ge_test.rb +12 -0
  43. data/test/countries/gh_test.rb +9 -0
  44. data/test/countries/gr_test.rb +9 -0
  45. data/test/countries/gt_test.rb +12 -0
  46. data/test/countries/gu_test.rb +9 -0
  47. data/test/countries/gy_test.rb +9 -0
  48. data/test/countries/hr_test.rb +75 -0
  49. data/test/countries/hu_test.rb +12 -0
  50. data/test/countries/il_test.rb +12 -0
  51. data/test/countries/in_test.rb +45 -0
  52. data/test/countries/ir_test.rb +13 -0
  53. data/test/countries/it_test.rb +16 -0
  54. data/test/countries/ke_test.rb +12 -0
  55. data/test/countries/lk_test.rb +9 -0
  56. data/test/countries/lu_test.rb +16 -0
  57. data/test/countries/ng_test.rb +9 -0
  58. data/test/countries/nl_test.rb +383 -0
  59. data/test/countries/no_test.rb +12 -0
  60. data/test/countries/np_test.rb +15 -0
  61. data/test/countries/ph_test.rb +9 -0
  62. data/test/countries/pk_test.rb +9 -0
  63. data/test/countries/pt_test.rb +129 -0
  64. data/test/countries/qa_test.rb +9 -0
  65. data/test/countries/rs_test.rb +15 -0
  66. data/test/countries/sa_test.rb +9 -0
  67. data/test/countries/se_test.rb +478 -0
  68. data/test/countries/si_test.rb +19 -0
  69. data/test/countries/sv_test.rb +12 -0
  70. data/test/countries/to_test.rb +12 -0
  71. data/test/countries/ua_test.rb +17 -0
  72. data/test/countries/us_test.rb +24 -0
  73. data/test/countries/uy_test.rb +20 -0
  74. data/test/countries/za_test.rb +19 -0
  75. data/test/countries/zw_test.rb +12 -0
  76. data/test/country_test.rb +27 -0
  77. data/test/extension_test.rb +30 -0
  78. data/test/phone_test.rb +149 -0
  79. data/test/test_helper.rb +33 -0
  80. metadata +237 -0
@@ -0,0 +1,222 @@
1
+ # An object representing a phone number.
2
+ #
3
+ # The phone number is recorded in 3 separate parts:
4
+ # * country_code - e.g. '385', '386'
5
+ # * area_code - e.g. '91', '47'
6
+ # * number - e.g. '5125486', '451588'
7
+ #
8
+ # All parts are mandatory, but country code and area code can be set for all phone numbers using
9
+ # Phone.default_country_code
10
+ # Phone.default_area_code
11
+ #
12
+ module Phoner
13
+ class Phone
14
+ attr_accessor :country_code, :area_code, :number, :extension, :country
15
+
16
+ cattr_accessor :default_country_code
17
+ cattr_accessor :default_area_code
18
+ cattr_accessor :named_formats
19
+
20
+ # length of first number part (using multi number format)
21
+ cattr_accessor :n1_length
22
+ # default length of first number part
23
+ @@n1_length = 3
24
+
25
+ @@named_formats = {
26
+ :default => "+%c%a%n",
27
+ :default_with_extension => "+%c%a%nx%x",
28
+ :europe => '+%c (0) %a %f %l',
29
+ :us => "(%a) %f-%l"
30
+ }
31
+
32
+ def initialize(*hash_or_args)
33
+ if hash_or_args.first.is_a?(Hash)
34
+ hash_or_args = hash_or_args.first
35
+ keys = {:country => :country, :number => :number, :area_code => :area_code, :country_code => :country_code, :extension => :extension}
36
+ else
37
+ keys = {:number => 0, :area_code => 1, :country_code => 2, :extension => 3, :country => 4}
38
+ end
39
+
40
+ self.number = hash_or_args[ keys[:number] ]
41
+ self.area_code = hash_or_args[ keys[:area_code] ] || self.default_area_code
42
+ self.country_code = hash_or_args[ keys[:country_code] ] || self.default_country_code
43
+ self.extension = hash_or_args[ keys[:extension] ]
44
+ self.country = hash_or_args[ keys[:country] ]
45
+
46
+ # Santity checks
47
+ raise "Must enter number" if self.number.blank?
48
+ raise "Must enter area code or set default area code" if self.area_code.blank?
49
+ raise "Must enter country code or set default country code" if self.country_code.blank?
50
+ end
51
+
52
+ def self.parse!(string, options={})
53
+ parse(string, options.merge(:raise_exception_on_error => true))
54
+ end
55
+
56
+ # create a new phone number by parsing a string
57
+ # the format of the string is detect automatically (from FORMATS)
58
+ def self.parse(string, options={})
59
+ return nil unless string.present?
60
+
61
+ Country.load
62
+
63
+ extension = extract_extension(string)
64
+ normalized = normalize(string)
65
+
66
+ options[:country_code] ||= self.default_country_code
67
+ options[:area_code] ||= self.default_area_code
68
+
69
+ if options[:country_code].is_a?(Array)
70
+ options[:country_code].find do |country_code|
71
+ parts = split_to_parts(normalized, options.merge(:country_code => country_code))
72
+ return parts if parts
73
+ end
74
+ else
75
+ parts = split_to_parts(normalized, options)
76
+ end
77
+
78
+ pn = Phone.new(parts) if parts
79
+ if pn.present? and extension.present?
80
+ pn.extension = extension
81
+ end
82
+ pn
83
+ end
84
+
85
+ # is this string a valid phone number?
86
+ def self.valid?(string, options = {})
87
+ begin
88
+ parse(string, options).present?
89
+ rescue
90
+ false # don't raise exceptions on parse errors
91
+ end
92
+ end
93
+
94
+ def self.is_mobile?(string, options = {})
95
+ pn = parse(string, options)
96
+ return false if pn.nil?
97
+ pn.is_mobile?
98
+ end
99
+
100
+ private
101
+ # split string into hash with keys :country_code, :area_code and :number
102
+ def self.split_to_parts(string, options = {})
103
+ country = Country.detect(string, options[:country_code], options[:area_code])
104
+
105
+ if country.nil?
106
+ raise "Could not determine country" if options[:raise_exception_on_error]
107
+ return nil
108
+ end
109
+
110
+ country.number_parts(string, options[:area_code])
111
+ end
112
+
113
+ # fix string so it's easier to parse, remove extra characters etc.
114
+ def self.normalize(string_with_number)
115
+ string_with_number.sub(extension_regex, '').gsub(/\(0\)|[^0-9+]/, '').gsub(/^00/, '+')
116
+ end
117
+
118
+ def self.extension_regex
119
+ /[ ]*(ext|ex|x|xt|#|:)+[^0-9]*\(*([-0-9]{1,})\)*#?$/i
120
+ end
121
+
122
+ # pull off anything that look like an extension
123
+ #
124
+ def self.extract_extension(string)
125
+ return nil if string.nil?
126
+ if string.match extension_regex
127
+ extension = $2
128
+ return extension
129
+ end
130
+ #
131
+ # We already returned any recognizable extension.
132
+ # However, we might still have extra junk to the right
133
+ # of the phone number proper, so just chop it off.
134
+ #
135
+ idx = string.rindex(/[0-9]/)
136
+ return nil if idx.nil?
137
+ return nil if idx == (string.length - 1) # at the end
138
+ string.slice!((idx+1)..-1) # chop it
139
+ return nil
140
+ end
141
+
142
+ public # instance methods
143
+
144
+ def area_code_long
145
+ "0" + area_code if area_code
146
+ end
147
+
148
+ # For many countries it's not apparent from the number
149
+ # Will return false positives rather than false negatives.
150
+ def is_mobile?
151
+ country.is_mobile? "#{area_code}#{number}"
152
+ end
153
+
154
+ # first n characters of :number
155
+ def number1
156
+ number[0...self.class.n1_length]
157
+ end
158
+
159
+ # everything left from number after the first n characters (see number1)
160
+ def number2
161
+ n2_length = number.size - self.class.n1_length
162
+ number[-n2_length, n2_length]
163
+ end
164
+
165
+ # Formats the phone number.
166
+ #
167
+ # if the method argument is a String, it is used as a format string, with the following fields being interpolated:
168
+ #
169
+ # * %c - country_code (385)
170
+ # * %a - area_code (91)
171
+ # * %A - area_code with leading zero (091)
172
+ # * %n - number (5125486)
173
+ # * %f - first @@n1_length characters of number (configured through Phone.n1_length), default is 3 (512)
174
+ # * %l - last characters of number (5486)
175
+ # * %x - entire extension
176
+ #
177
+ # if the method argument is a Symbol, it is used as a lookup key for a format String in Phone.named_formats
178
+ # pn.format(:europe)
179
+ def format(fmt)
180
+ if fmt.is_a?(Symbol)
181
+ raise "The format #{fmt} doesn't exist'" unless named_formats.has_key?(fmt)
182
+ format_number named_formats[fmt]
183
+ else
184
+ format_number(fmt)
185
+ end
186
+ end
187
+
188
+ # the default format is "+%c%a%n"
189
+ def to_s
190
+ format(:default)
191
+ end
192
+
193
+ # does this number belong to the default country code?
194
+ def has_default_country_code?
195
+ country_code == self.class.default_country_code
196
+ end
197
+
198
+ # does this number belong to the default area code?
199
+ def has_default_area_code?
200
+ area_code == self.class.default_area_code
201
+ end
202
+
203
+ # comparison of 2 phone objects
204
+ def ==(other)
205
+ methods = [:country_code, :area_code, :number, :extension]
206
+ methods.all? { |method| other.respond_to?(method) && send(method) == other.send(method) }
207
+ end
208
+
209
+ private
210
+
211
+ def format_number(fmt)
212
+ result = fmt.gsub("%c", country_code || "").
213
+ gsub("%a", area_code || "").
214
+ gsub("%A", area_code_long || "").
215
+ gsub("%n", number || "").
216
+ gsub("%f", number1 || "").
217
+ gsub("%l", number2 || "").
218
+ gsub("%x", extension || "")
219
+ return result
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,3 @@
1
+ module Phoner
2
+ VERSION = "1.0"
3
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'phoner/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "phoner"
8
+ gem.version = Phoner::VERSION
9
+ gem.authors = ['Tomislav Car', 'Todd Eichel', 'Don Morrison', 'Wesley Moxam', 'Paul Chavard']
10
+ gem.email = ['tomislav@infinum.hr', 'todd@toddeichel.com', 'elskwid@gmail.com', 'wesley.moxam@gmail.com', "paul@chavard.net"]
11
+
12
+ gem.description = "Phone number parsing, validation and formatting."
13
+ gem.summary = "Phone number parsing, validation and formatting"
14
+ gem.homepage = ""
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_dependency "activesupport"
22
+ gem.add_development_dependency "rake"
23
+ end
@@ -0,0 +1,15 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ ## UAE
4
+ class AETest < Phoner::TestCase
5
+ def test_local
6
+ parse_test('+97142063944', '971', '4', '2063944', 'United Arab Emirates', false)
7
+ parse_test('+97122063944', '971', '2', '2063944', 'United Arab Emirates', false)
8
+ end
9
+
10
+ def test_mobile
11
+ parse_test('+971502063944', '971', '50', '2063944', 'United Arab Emirates', true)
12
+ parse_test('+971552063944', '971', '55', '2063944', 'United Arab Emirates', true)
13
+ end
14
+
15
+ end
@@ -0,0 +1,12 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ ## Afghanistan
4
+ class AFTest < Phoner::TestCase
5
+ def test_local
6
+ parse_test('+93201234567', '93', '20', '1234567', 'Afghanistan', false)
7
+ end
8
+
9
+ def test_mobile
10
+ parse_test('+93712345678', '93', '7', '12345678', 'Afghanistan', true)
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ ## Albania
4
+ class ALTest < Phoner::TestCase
5
+ def test_local
6
+ parse_test('+35541234567', '355', '4', '1234567', 'Albania', false)
7
+ end
8
+
9
+ def test_mobile
10
+ parse_test('+355612345678', '355', '6', '12345678', 'Albania', true)
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ ## Argentina
4
+ class ARTest < Phoner::TestCase
5
+ def test_local
6
+ parse_test('+541112345678', '54', '11', '12345678', 'Argentina', false)
7
+ end
8
+
9
+ def test_mobile
10
+ parse_test('+5498211534567', '54', '9821', '1534567', 'Argentina', true)
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ ## Austria
4
+ # Austrian numbers are, ugh, strange. The matching patterns used are minimal :/
5
+ # and will result in many false positives
6
+ class ATTest < Phoner::TestCase
7
+ def test_local
8
+ parse_test('+4354321', '43', "5432", '1', 'Austria', false)
9
+ end
10
+
11
+ def test_mobile
12
+ parse_test('+43612345678', '43', '6', '12345678', 'Austria', true)
13
+ end
14
+ end
@@ -0,0 +1,48 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ # 0x 5551 reserved for fictitious use. (not including x=3)
4
+ # 0x 7010 reserved for fictitious use.
5
+
6
+ ## Australia
7
+ class AUTest < Phoner::TestCase
8
+
9
+ # 00 Emergency and International access
10
+ # 01 Alternate phone services
11
+ # 014 Satellite phone services
12
+ # 016 Paging [+3D or +6D]
13
+ # 018 Analogue (AMPS) mobile phone - few numbers still in use [+6D]
14
+ # 0198 Data networks [+2D or +6D] - e.g. 0198 379 000 is the Dial-Up POP number for iiNet
15
+
16
+ # 02 Central East region (NSW, ACT)
17
+ def test_central_east
18
+ parse_test('+61 2 5551 1234', '61', '2', '55511234')
19
+ end
20
+
21
+ # 03 South-east region (VIC, TAS)
22
+ def test_south_east
23
+ parse_test('+61 3 5551 1234', '61', '3', '55511234')
24
+ end
25
+
26
+ # 04 Mobile services (Digital - GSM, CDMA, 3G)
27
+ def test_mobile
28
+ parse_test('+61 4 5551 1234', '61', '4', '55511234', 'Australia', true)
29
+ end
30
+
31
+ # 05 Universal/Personal numberings (uncommon)
32
+ def test_personal
33
+ parse_test('+61 5 5551 1234', '61', '5', '55511234', 'Australia', false)
34
+ end
35
+
36
+ # 07 North-east region (QLD)
37
+ def test_north_east
38
+ parse_test('+61 7 5551 1234', '61', '7', '55511234')
39
+ end
40
+
41
+ # 08 Central and West region (SA, NT, WA)
42
+ def test_central
43
+ parse_test('+61 8 5551 1234', '61', '8', '55511234')
44
+ end
45
+
46
+ # (Geographical region boundaries do not exactly follow state borders.)
47
+ # 1 Non-geographic numbers
48
+ end
@@ -0,0 +1,9 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ ## Bosnia and Herzegovina
4
+ class BATest < Phoner::TestCase
5
+ def test_local
6
+ parse_test('+387 33 25 02 33', '387', '33', '250233', "Bosnia and Herzegovina", false)
7
+ parse_test('+387 61 25 02 33', '387', '6', '1250233', "Bosnia and Herzegovina", true)
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ ## Bangladesh
4
+ class BDTest < Phoner::TestCase
5
+ def test_local
6
+ parse_test('+8801715379982', '880', '171', '5379982', 'Bangladesh', true)
7
+ parse_test('+8801191573647', '880', '119', '1573647', 'Bangladesh', true)
8
+ parse_test('+88021915736', '880', '2', '1915736', 'Bangladesh', false)
9
+ end
10
+
11
+ def test_with_default_country
12
+ Phoner::Phone.default_country_code = '880'
13
+ parse_test('1715379982', '880', '171', '5379982', 'Bangladesh', true)
14
+ parse_test('1191573647', '880', '119', '1573647', 'Bangladesh', true)
15
+ parse_test('21915736', '880', '2', '1915736', 'Bangladesh', false)
16
+ end
17
+ end
@@ -0,0 +1,120 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ ## Belgium
4
+ class BETest < Phoner::TestCase
5
+
6
+ def test_mobile
7
+ parse_test('+32 4 12345678', '32', '4', '12345678', 'Belgium', true)
8
+ end
9
+
10
+ ## single digit
11
+ # 02: Brussels (Bruxelles/Brussel)
12
+ def test_brussels
13
+ parse_test('+32 2 1234567', '32', '2', '1234567', 'Belgium', false)
14
+ end
15
+ # 03: Antwerpen (Antwerp), Sint-Niklaas
16
+ def test_antwerpen
17
+ parse_test('+32 3 1234567', '32', '3', '1234567')
18
+ end
19
+ # 04: Liège (Luik), Voeren (Fourons)
20
+ def test_liege
21
+ parse_test('+32 4 1234567', '32', '4', '1234567', 'Belgium', false)
22
+ end
23
+ # 09: Gent (Ghent/Gand)
24
+ def test_gent
25
+ parse_test('+32 9 1234567', '32', '9', '1234567')
26
+ end
27
+
28
+ ## two digit
29
+ # 010: Wavre (Waver)
30
+ def test_wavre
31
+ parse_test('+32 10 123456', '32', '10', '123456')
32
+ end
33
+ # 011: Hasselt
34
+ def test_hasselt
35
+ parse_test('+32 11 123456', '32', '11', '123456')
36
+ end
37
+ # 012: Tongeren (Tongres)
38
+ def test_tongeren
39
+ parse_test('+32 12 123456', '32', '12', '123456')
40
+ end
41
+ # 013: Diest
42
+ def test_diest
43
+ parse_test('+32 13 123456', '32', '13', '123456')
44
+ end
45
+ # 014: Herentals, Turnhout
46
+ # 015: Mechelen (Malines)
47
+ # 016: Leuven (Louvain), Tienen (Tirlemont)
48
+ # 019: Waremme (Borgworm)
49
+ def test_waremme
50
+ parse_test('+32 19 123456', '32', '19', '123456')
51
+ end
52
+
53
+ # 050: Brugge (Bruges), Zeebrugge
54
+ def test_brugge
55
+ parse_test('+32 50 123456', '32', '50', '123456')
56
+ end
57
+ # 051: Roeselare (Roulers)
58
+ def test_roeselare
59
+ parse_test('+32 51 123456', '32', '51', '123456')
60
+ end
61
+ # 052: Dendermonde (Termonde)
62
+ # 053: Aalst (Alost)
63
+ # 054: Ninove
64
+ # 055: Ronse (Renaix)
65
+ # 056: Kortrijk (Courtrai), Comines-Warneton, Mouscron (Moeskroen)
66
+ # 057: Ieper (Ypres)
67
+ # 058: Veurne (Furnes)
68
+ # 059: Oostende (Ostend)
69
+ def test_oostende
70
+ parse_test('+32 59 123456', '32', '59', '123456')
71
+ end
72
+
73
+ # 060: Chimay
74
+ def test_chimay
75
+ parse_test('+32 60 123456', '32', '60', '123456')
76
+ end
77
+ # 061: Bastogne, Libramont-Chevigny
78
+ # 063: Arlon (Aarlen)
79
+ # 064: La Louviere
80
+ # 065: Mons (Bergen)
81
+ # 067: Nivelles (Nijvel)
82
+ def test_nivelles
83
+ parse_test('+32 67 123456', '32', '67', '123456')
84
+ end
85
+ # 068: Ath (Aat)
86
+ # 069: Tournai (Doornik)
87
+
88
+ # 070: Specialty Numbers (i.e. bus information or bank information)
89
+ def test_specialty
90
+ parse_test('+32 70 123456', '32', '70', '123456')
91
+ end
92
+ # 071: Charleroi
93
+
94
+ # 081: Namur (Namen)
95
+ def test_namur
96
+ parse_test('+32 81 123456', '32', '81', '123456')
97
+ end
98
+ # 082: Dinant
99
+ # 083: Ciney
100
+ # 084: Jemelle, Marche-en-Famenne
101
+ # 085: Huy
102
+ # 086: Durbuy
103
+ # 087: Verviers
104
+ # 089: Genk
105
+
106
+ # 0800: toll free service
107
+ def test_toll_free
108
+ parse_test('+32 800 12345', '32', '800', '12345')
109
+ end
110
+
111
+ # 090x: Premium numbers (0900, 0901, 0902, 0903, 0904, 0905, 0906, 0907, 0908, 0909)
112
+ def test_premium_900
113
+ parse_test('+32 900 12345', '32', '900', '12345')
114
+ end
115
+
116
+ def test_premium_901
117
+ parse_test('+32 901 12345', '32', '901', '12345')
118
+ end
119
+
120
+ end