phonie 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.
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