phonie 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +5 -0
  3. data/Gemfile.lock +10 -0
  4. data/LICENSE +20 -0
  5. data/Rakefile +13 -0
  6. data/Readme.rdoc +153 -0
  7. data/lib/phonie.rb +7 -0
  8. data/lib/phonie/country.rb +120 -0
  9. data/lib/phonie/data/phone_countries.yml +1683 -0
  10. data/lib/phonie/phone.rb +215 -0
  11. data/lib/phonie/support.rb +78 -0
  12. data/lib/phonie/version.rb +3 -0
  13. data/phonie.gemspec +18 -0
  14. data/test/countries/ae_test.rb +15 -0
  15. data/test/countries/af_test.rb +12 -0
  16. data/test/countries/al_test.rb +12 -0
  17. data/test/countries/ar_test.rb +12 -0
  18. data/test/countries/at_test.rb +14 -0
  19. data/test/countries/au_test.rb +48 -0
  20. data/test/countries/ba_test.rb +9 -0
  21. data/test/countries/bd_test.rb +17 -0
  22. data/test/countries/be_test.rb +120 -0
  23. data/test/countries/bg_test.rb +13 -0
  24. data/test/countries/bo_test.rb +12 -0
  25. data/test/countries/br_test.rb +12 -0
  26. data/test/countries/bt_test.rb +9 -0
  27. data/test/countries/by_test.rb +12 -0
  28. data/test/countries/bz_test.rb +12 -0
  29. data/test/countries/ca_test.rb +20 -0
  30. data/test/countries/cr_test.rb +12 -0
  31. data/test/countries/cy_test.rb +12 -0
  32. data/test/countries/cz_test.rb +12 -0
  33. data/test/countries/de_test.rb +18 -0
  34. data/test/countries/dk_test.rb +12 -0
  35. data/test/countries/dz_test.rb +12 -0
  36. data/test/countries/ec_test.rb +12 -0
  37. data/test/countries/ee_test.rb +12 -0
  38. data/test/countries/eg_test.rb +9 -0
  39. data/test/countries/et_test.rb +11 -0
  40. data/test/countries/fi_test.rb +12 -0
  41. data/test/countries/fr_test.rb +22 -0
  42. data/test/countries/gb_test.rb +262 -0
  43. data/test/countries/ge_test.rb +12 -0
  44. data/test/countries/gh_test.rb +9 -0
  45. data/test/countries/gr_test.rb +9 -0
  46. data/test/countries/gt_test.rb +12 -0
  47. data/test/countries/gu_test.rb +9 -0
  48. data/test/countries/gy_test.rb +9 -0
  49. data/test/countries/hr_test.rb +75 -0
  50. data/test/countries/hu_test.rb +12 -0
  51. data/test/countries/il_test.rb +12 -0
  52. data/test/countries/in_test.rb +45 -0
  53. data/test/countries/ir_test.rb +13 -0
  54. data/test/countries/ke_test.rb +12 -0
  55. data/test/countries/lk_test.rb +9 -0
  56. data/test/countries/ng_test.rb +9 -0
  57. data/test/countries/nl_test.rb +383 -0
  58. data/test/countries/no_test.rb +12 -0
  59. data/test/countries/np_test.rb +15 -0
  60. data/test/countries/ph_test.rb +9 -0
  61. data/test/countries/pk_test.rb +9 -0
  62. data/test/countries/pt_test.rb +129 -0
  63. data/test/countries/qa_test.rb +9 -0
  64. data/test/countries/rs_test.rb +15 -0
  65. data/test/countries/sa_test.rb +9 -0
  66. data/test/countries/se_test.rb +478 -0
  67. data/test/countries/si_test.rb +19 -0
  68. data/test/countries/sv_test.rb +12 -0
  69. data/test/countries/to_test.rb +12 -0
  70. data/test/countries/ua_test.rb +17 -0
  71. data/test/countries/us_test.rb +24 -0
  72. data/test/countries/uy_test.rb +20 -0
  73. data/test/countries/za_test.rb +19 -0
  74. data/test/countries/zw_test.rb +12 -0
  75. data/test/country_test.rb +27 -0
  76. data/test/extension_test.rb +30 -0
  77. data/test/phone_test.rb +137 -0
  78. data/test/test_helper.rb +37 -0
  79. data/test_usa_phones_with_extensions.csv +99 -0
  80. metadata +130 -0
@@ -0,0 +1,215 @@
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 Phonie
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
+ parts = split_to_parts(normalized, options)
70
+
71
+ pn = Phone.new(parts) if parts
72
+ if pn.present? and extension.present?
73
+ pn.extension = extension
74
+ end
75
+ pn
76
+ end
77
+
78
+ # is this string a valid phone number?
79
+ def self.valid?(string, options = {})
80
+ begin
81
+ parse(string, options).present?
82
+ rescue
83
+ false # don't raise exceptions on parse errors
84
+ end
85
+ end
86
+
87
+ def self.is_mobile?(string, options = {})
88
+ pn = parse(string, options)
89
+ return false if pn.nil?
90
+ pn.is_mobile?
91
+ end
92
+
93
+ private
94
+ # split string into hash with keys :country_code, :area_code and :number
95
+ def self.split_to_parts(string, options = {})
96
+ country = Country.detect(string, options[:country_code], options[:area_code])
97
+
98
+ if country.nil?
99
+ raise "Could not determine country" if options[:raise_exception_on_error]
100
+ return nil
101
+ end
102
+
103
+ country.number_parts(string, options[:area_code])
104
+ end
105
+
106
+ # fix string so it's easier to parse, remove extra characters etc.
107
+ def self.normalize(string_with_number)
108
+ string_with_number.sub(extension_regex, '').gsub(/\(0\)|[^0-9+]/, '').gsub(/^00/, '+')
109
+ end
110
+
111
+ def self.extension_regex
112
+ /[ ]*(ext|ex|x|xt|#|:)+[^0-9]*\(*([-0-9]{1,})\)*#?$/i
113
+ end
114
+
115
+ # pull off anything that look like an extension
116
+ #
117
+ def self.extract_extension(string)
118
+ return nil if string.nil?
119
+ if string.match extension_regex
120
+ extension = $2
121
+ return extension
122
+ end
123
+ #
124
+ # We already returned any recognizable extension.
125
+ # However, we might still have extra junk to the right
126
+ # of the phone number proper, so just chop it off.
127
+ #
128
+ idx = string.rindex(/[0-9]/)
129
+ return nil if idx.nil?
130
+ return nil if idx == (string.length - 1) # at the end
131
+ string.slice!((idx+1)..-1) # chop it
132
+ return nil
133
+ end
134
+
135
+ public # instance methods
136
+
137
+ def area_code_long
138
+ "0" + area_code if area_code
139
+ end
140
+
141
+ # For many countries it's not apparent from the number
142
+ # Will return false positives rather than false negatives.
143
+ def is_mobile?
144
+ country.is_mobile? "#{area_code}#{number}"
145
+ end
146
+
147
+ # first n characters of :number
148
+ def number1
149
+ number[0...self.class.n1_length]
150
+ end
151
+
152
+ # everything left from number after the first n characters (see number1)
153
+ def number2
154
+ n2_length = number.size - self.class.n1_length
155
+ number[-n2_length, n2_length]
156
+ end
157
+
158
+ # Formats the phone number.
159
+ #
160
+ # if the method argument is a String, it is used as a format string, with the following fields being interpolated:
161
+ #
162
+ # * %c - country_code (385)
163
+ # * %a - area_code (91)
164
+ # * %A - area_code with leading zero (091)
165
+ # * %n - number (5125486)
166
+ # * %f - first @@n1_length characters of number (configured through Phone.n1_length), default is 3 (512)
167
+ # * %l - last characters of number (5486)
168
+ # * %x - entire extension
169
+ #
170
+ # if the method argument is a Symbol, it is used as a lookup key for a format String in Phone.named_formats
171
+ # pn.format(:europe)
172
+ def format(fmt)
173
+ if fmt.is_a?(Symbol)
174
+ raise "The format #{fmt} doesn't exist'" unless named_formats.has_key?(fmt)
175
+ format_number named_formats[fmt]
176
+ else
177
+ format_number(fmt)
178
+ end
179
+ end
180
+
181
+ # the default format is "+%c%a%n"
182
+ def to_s
183
+ format(:default)
184
+ end
185
+
186
+ # does this number belong to the default country code?
187
+ def has_default_country_code?
188
+ country_code == self.class.default_country_code
189
+ end
190
+
191
+ # does this number belong to the default area code?
192
+ def has_default_area_code?
193
+ area_code == self.class.default_area_code
194
+ end
195
+
196
+ # comparison of 2 phone objects
197
+ def ==(other)
198
+ methods = [:country_code, :area_code, :number, :extension]
199
+ methods.all? { |method| other.respond_to?(method) && send(method) == other.send(method) }
200
+ end
201
+
202
+ private
203
+
204
+ def format_number(fmt)
205
+ result = fmt.gsub("%c", country_code || "").
206
+ gsub("%a", area_code || "").
207
+ gsub("%A", area_code_long || "").
208
+ gsub("%n", number || "").
209
+ gsub("%f", number1 || "").
210
+ gsub("%l", number2 || "").
211
+ gsub("%x", extension || "")
212
+ return result
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,78 @@
1
+ require 'yaml'
2
+ # support methods to remove dependencies on ActiveSupport
3
+ class String
4
+ def present?
5
+ !blank?
6
+ end
7
+
8
+ def blank?
9
+ if respond_to?(:empty?) && respond_to?(:strip)
10
+ empty? or strip.empty?
11
+ elsif respond_to?(:empty?)
12
+ empty?
13
+ else
14
+ !self
15
+ end
16
+ end
17
+ end
18
+
19
+ class Hash
20
+ alias_method :blank?, :empty?
21
+
22
+ def present?
23
+ !blank?
24
+ end
25
+ end
26
+
27
+ class Object
28
+ def present?
29
+ self.class!=NilClass
30
+ end
31
+ end
32
+
33
+ class NilClass #:nodoc:
34
+ def blank?
35
+ true
36
+ end
37
+
38
+ def present?
39
+ false
40
+ end
41
+ end
42
+
43
+ module Accessorize
44
+ module ClassMethods
45
+ def cattr_accessor(*syms)
46
+ syms.flatten.each do |sym|
47
+ class_eval(<<-EOS, __FILE__, __LINE__)
48
+ unless defined? @@#{sym}
49
+ @@#{sym} = nil
50
+ end
51
+
52
+ def self.#{sym}
53
+ @@#{sym}
54
+ end
55
+
56
+ def #{sym}=(value)
57
+ @@#{sym} = value
58
+ end
59
+
60
+ def self.#{sym}=(value)
61
+ @@#{sym} = value
62
+ end
63
+
64
+ def #{sym}
65
+ @@#{sym}
66
+ end
67
+ EOS
68
+ end
69
+ end
70
+ end
71
+
72
+
73
+ def self.included(receiver)
74
+ receiver.extend ClassMethods
75
+ end
76
+ end
77
+
78
+ Object.send(:include, Accessorize)
@@ -0,0 +1,3 @@
1
+ module Phonie
2
+ VERSION = '1.0.1'
3
+ end
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "phonie/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "phonie"
7
+ s.version = Phonie::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Tomislav Car', 'Todd Eichel', 'Don Morrison', 'Wesley Moxam']
10
+ s.email = ['tomislav@infinum.hr', 'todd@toddeichel.com', 'elskwid@gmail.com', 'wesley@wmoxam.com']
11
+ s.homepage = "http://github.com/wmoxam/phonie"
12
+ s.summary = %q{Phone number parsing, validation and formatting}
13
+ s.description = %q{Phone number parsing, validation and formatting}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.require_paths = ["lib"]
18
+ end
@@ -0,0 +1,15 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
+
3
+ ## UAE
4
+ class AETest < Phonie::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 < Phonie::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 < Phonie::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 < Phonie::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 < Phonie::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 < Phonie::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 < Phonie::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