has_phone_numbers 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,5 +1,15 @@
1
1
  == master
2
2
 
3
+ == 0.2.0 / 2009-04-19
4
+
5
+ * Add the ability to parse raw values [Matt Lightner]
6
+ * Validate that the country code is known
7
+ * Validate number lengths on a per-country code basis
8
+ * Add the list of available country codes
9
+ * Remove PhoneNumber#display_value in favor of using the Rails helper
10
+ * Add dependency on Rails 2.3
11
+ * Remove dependency on plugins_plus
12
+
3
13
  == 0.1.0 / 2008-12-14
4
14
 
5
15
  * Remove the PluginAWeek namespace
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2006-2008 Aaron Pfeifer
1
+ Copyright (c) 2006-2009 Aaron Pfeifer
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -52,5 +52,8 @@ To run against a specific version of Rails:
52
52
 
53
53
  == Dependencies
54
54
 
55
- * Rails 2.1 or later
56
- * plugins_plus[http://github.com/pluginaweek/plugins_plugins] (optional if app files are copied to your project tree)
55
+ * Rails 2.3 or later
56
+
57
+ == References
58
+
59
+ * Casey West - {Parse-PhoneNumber}[http://search.cpan.org/~cwest/Parse-PhoneNumber-1.1]
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake/contrib/sshpublisher'
5
5
 
6
6
  spec = Gem::Specification.new do |s|
7
7
  s.name = 'has_phone_numbers'
8
- s.version = '0.1.0'
8
+ s.version = '0.2.0'
9
9
  s.platform = Gem::Platform::RUBY
10
10
  s.summary = 'Demonstrates a reference implementation for handling phone numbers.'
11
11
 
@@ -1,42 +1,131 @@
1
1
  # Represents a phone number split into multiple parts:
2
- # * +country_code+ - Uniquely identifiers the country to which the number belongs. This value is based on the E.164 standard (http://en.wikipedia.org/wiki/E.164)
2
+ # * +country_code+ - Uniquely identifiers the country to which the number belongs.
3
+ # This value is based on the E.164 standard (http://en.wikipedia.org/wiki/E.164)
3
4
  # * +number+ - The subscriber number (10 digits in length)
4
5
  # * +extension+ - A number that can route to different phones at a location
5
6
  #
6
7
  # This phone number format is biased towards those types found in the United
7
8
  # States and may need to be adjusted for international support.
8
9
  class PhoneNumber < ActiveRecord::Base
9
- belongs_to :phoneable,
10
- :polymorphic => true
10
+ # The valid lengths allowed per country code for area code + subscriber number
11
+ VALID_LENGTHS = {
12
+ 1 => 10, 7 => 10, 20 => 9, 27 => 9, 30 => 10, 31 => 9,
13
+ 32 => 8..9, 33 => 9..10, 34 => 9, 36 => 8..9, 39 => 9, 40 => 9,
14
+ 41 => 9..10, 43 => 7..13, 44 => 10..11, 45 => 8, 46 => 9, 47 => 8,
15
+ 48 => 9, 49 => 7..11, 51 => 8..9, 52 => 8..10, 53 => 8, 54 => 8..9,
16
+ 55 => 10, 56 => 8..9, 57 => 9..10, 58 => 10, 60 => 9..10, 61 => 9,
17
+ 62 => 8..11, 63 => 8..10, 64 => 8..9, 65 => 8..9, 66 => 9..10, 81 => 9..10,
18
+ 82 => 8..9, 84 => 7..10, 86 => 7..11, 90 => 10, 91 => 10, 92 => 9..10,
19
+ 93 => 8..9, 94 => 10, 95 => 7..8, 98 => 10, 212 => 8, 213 => 8,
20
+ 216 => 8, 218 => 8..9, 220 => 7, 221 => 7, 222 => 7, 223 => 8,
21
+ 224 => 8, 225 => 8, 226 => 8, 227 => 8, 228 => 7, 229 => 8,
22
+ 230 => 7, 231 => 6..8, 232 => 8, 233 => 5..8, 234 => 7..8, 235 => 7,
23
+ 236 => 8, 237 => 8, 238 => 7, 239 => 6..7, 240 => 6, 241 => 6..8,
24
+ 242 => 7, 243 => 8, 244 => 9, 245 => 7, 246 => 4..7, 247 => 4,
25
+ 248 => 6, 249 => 9, 250 => 5..8, 251 => 9, 252 => 7..8, 253 => 6,
26
+ 254 => 9, 255 => 9, 256 => 9, 257 => 8, 258 => 8..9, 260 => 9,
27
+ 261 => 9, 262 => 10, 263 => 8..11, 264 => 6..7, 265 => 8, 266 => 8,
28
+ 267 => 7, 268 => 7, 269 => 7, 290 => 4, 291 => 7, 297 => 7,
29
+ 298 => 6, 299 => 6, 350 => 8, 351 => 9, 352 => 9, 353 => 9,
30
+ 354 => 7..9, 355 => 7..9, 356 => 8, 357 => 8, 358 => 9, 359 => 8..10,
31
+ 370 => 8, 371 => 8, 372 => 7..8, 373 => 8, 374 => 8, 375 => 9,
32
+ 376 => 6..9, 377 => 8..9, 378 => 9..12, 380 => 8..9, 381 => 9, 382 => 8,
33
+ 385 => 8, 386 => 8, 387 => 8, 388 => 8..10, 389 => 7..8, 420 => 9,
34
+ 421 => 9, 423 => 7, 500 => 5, 501 => 7, 502 => 8, 503 => 8,
35
+ 504 => 7..8, 505 => 8, 506 => 8, 507 => 7, 508 => 6, 509 => 8,
36
+ 590 => 10, 591 => 8, 592 => 6..7, 593 => 8..9, 594 => 10, 595 => 9,
37
+ 596 => 10, 597 => 6, 598 => 7..8, 599 => 7, 670 => 7, 672 => 6,
38
+ 673 => 7, 674 => 7, 675 => 7, 676 => 5..7, 677 => 5, 678 => 5..7,
39
+ 679 => 7, 680 => 7, 681 => 6, 682 => 5, 683 => 4, 685 => 6..7,
40
+ 686 => 5, 687 => 6, 688 => 5, 689 => 6, 690 => 4, 691 => 7,
41
+ 692 => 7, 800 => 8..12, 808 => 8, 850 => 8..9, 852 => 8, 853 => 8,
42
+ 855 => 8, 856 => 8, 870 => 9, 871 => 9, 872 => 9, 873 => 9,
43
+ 874 => 9, 878 => 9, 880 => 10, 881 => 6, 882 => 6, 883 => 6,
44
+ 886 => 7..8, 960 => 7, 961 => 8, 962 => 8..9, 963 => 7..8, 964 => 8..10,
45
+ 965 => 7, 966 => 8..9, 967 => 7..9, 968 => 8, 970 => 8, 971 => 7..9,
46
+ 972 => 7..9, 973 => 8, 974 => 7, 975 => 7..8, 976 => 7..10, 977 => 7..8,
47
+ 979 => 9, 991 => 9, 992 => 9, 993 => 8, 994 => 8..9, 995 => 9,
48
+ 996 => 9, 998 => 9
49
+ }.stringify_keys
11
50
 
12
- validates_presence_of :phoneable_id,
13
- :phoneable_type,
14
- :country_code,
15
- :number
16
- validates_numericality_of :country_code,
17
- :number
18
- validates_numericality_of :extension,
19
- :allow_nil => true
20
- validates_length_of :country_code,
21
- :in => 1..3
22
- validates_length_of :number,
23
- :is => 10
24
- validates_length_of :extension,
25
- :maximum => 10,
26
- :allow_nil => true
51
+ # The list of country calling codes as defined by ITU-T recommendation E.164
52
+ COUNTRY_CODES = VALID_LENGTHS.keys
27
53
 
28
- # Generates a human-readable version of the phone number, based on all of the
29
- # various parts of the number.
30
- #
31
- # For example,
32
- #
33
- # phone = PhoneNumber.new(:country_code => '1', :number => '123-456-7890')
34
- # phone.display_value # => "1- 123-456-7890"
35
- # phone.extension = "123"
36
- # phone.display_value # => "1- 123-456-7890 ext. 123"
37
- def display_value
38
- human_number = "#{country_code}- #{number}"
39
- human_number << " ext. #{extension}" if extension
40
- human_number
54
+ # Whether to always use the default country code configured for this model
55
+ # when parsing the raw content of a phone number
56
+ cattr_accessor :use_default_country_code_on_parse
57
+ @@use_default_country_code_on_parse = false
58
+
59
+ belongs_to :phoneable, :polymorphic => true
60
+
61
+ validates_presence_of :phoneable_id, :phoneable_type, :country_code, :number
62
+ validates_numericality_of :number
63
+ validates_numericality_of :extension, :allow_nil => true
64
+ validates_length_of :extension, :maximum => 10, :allow_nil => true
65
+ validates_inclusion_of :country_code, :in => COUNTRY_CODES
66
+ validates_each :number do |phone_number, attr, value|
67
+ country_code = phone_number.country_code
68
+
69
+ if country_code && length = VALID_LENGTHS[country_code]
70
+ if length.is_a?(Range)
71
+ if value.nil? || value.size < length.begin
72
+ phone_number.errors.add(attr, :too_short, :count => length.begin)
73
+ elsif value.size > length.end
74
+ phone_number.errors.add(attr, :too_long, :count => length.end)
75
+ end
76
+ elsif value.nil? || value.size != length
77
+ phone_number.errors.add(attr, :wrong_length, :count => length)
78
+ end
79
+ end
41
80
  end
81
+
82
+ # The raw, unparsed content containing the phone number. This can be parsed
83
+ # in various formats, such as:
84
+ # * 600 600 11 22
85
+ # * + 386 1 5853 449
86
+ # * +48 (22) 64 0001
87
+ # * 36 1 267-4636
88
+ # * +39-02-48230001
89
+ # * 202 331 996 x4621
90
+ # * 358 2 141 540 65 ext. 1423
91
+ attr_accessor :content
92
+ before_validation_on_create :parse_content, :if => :content
93
+
94
+ private
95
+ # Parses the raw content of a phone number, extracting the following
96
+ # attributes:
97
+ # * country_code
98
+ # * number
99
+ # * extension
100
+ def parse_content
101
+ content = self.content.strip
102
+
103
+ # Check for extension
104
+ if match = content.match(/\s*(?:(?:ext|ex|xt|x)[\s.:]*(\d+))$/i)
105
+ self.extension = match[1]
106
+ content.gsub!(match.to_s, '')
107
+ end
108
+
109
+ # Remove non-digits and leading 0
110
+ content.gsub!(/\D/, '')
111
+ content.gsub!(/^0+/, '')
112
+
113
+ if use_default_country_code_on_parse
114
+ # Scrub pre-determined country code
115
+ content.gsub!(/^#{country_code}/, '')
116
+ else
117
+ # Try to figure out the country code. It is not possible for one
118
+ # country code's number to overlap another.
119
+ (1..3).each do |length|
120
+ code = content[0, length]
121
+ if VALID_LENGTHS[code] # Fast lookup
122
+ self.country_code = code
123
+ content.gsub!(/^#{code}/, '')
124
+ break
125
+ end
126
+ end
127
+ end
128
+
129
+ self.number = content
130
+ end
42
131
  end
@@ -4,8 +4,7 @@ module HasPhoneNumbers
4
4
  # Creates the following association:
5
5
  # * +phone_number+ - All phone numbers associated with the current record.
6
6
  def has_phone_numbers
7
- has_many :phone_numbers,
8
- :as => :phoneable
7
+ has_many :phone_numbers, :as => :phoneable
9
8
  end
10
9
  end
11
10
  end
@@ -1,9 +1,13 @@
1
1
  class MigrateHasPhoneNumbersToVersion1 < ActiveRecord::Migration
2
2
  def self.up
3
- Rails::Plugin.find(:has_phone_numbers).migrate(1)
3
+ ActiveRecord::Migrator.new(:up, "#{Rails.root}/../../db/migrate", 0).migrations.each do |migration|
4
+ migration.migrate(:up)
5
+ end
4
6
  end
5
7
 
6
8
  def self.down
7
- Rails::Plugin.find(:has_phone_numbers).migrate(0)
9
+ ActiveRecord::Migrator.new(:up, "#{Rails.root}/../../db/migrate", 0).migrations.each do |migration|
10
+ migration.migrate(:down)
11
+ end
8
12
  end
9
13
  end
@@ -50,26 +50,13 @@ class PhoneNumberTest < Test::Unit::TestCase
50
50
  assert phone_number.errors.invalid?(:country_code)
51
51
  end
52
52
 
53
- def test_should_require_country_code_be_a_number
54
- phone_number = new_phone_number(:country_code => 'a')
53
+ def test_should_require_a_known_country_code
54
+ phone_number = new_phone_number(:country_code => '2')
55
55
  assert !phone_number.valid?
56
56
  assert phone_number.errors.invalid?(:country_code)
57
- end
58
-
59
- def test_should_require_country_codes_be_between_1_and_3_numbers
60
- phone_number = new_phone_number(:country_code => '')
61
- assert !phone_number.valid?
62
- assert phone_number.errors.invalid?(:country_code)
63
-
64
- phone_number.country_code += '1'
65
- assert phone_number.valid?
66
57
 
67
- phone_number.country_code += '11'
58
+ phone_number.country_code = '1'
68
59
  assert phone_number.valid?
69
-
70
- phone_number.country_code += '1'
71
- assert !phone_number.valid?
72
- assert phone_number.errors.invalid?(:country_code)
73
60
  end
74
61
 
75
62
  def test_should_require_a_number
@@ -84,17 +71,33 @@ class PhoneNumberTest < Test::Unit::TestCase
84
71
  assert phone_number.errors.invalid?(:number)
85
72
  end
86
73
 
87
- def test_should_require_number_be_exactly_10_numbers
88
- phone_number = new_phone_number(:number => '1' * 9)
74
+ def test_should_require_number_be_exact_for_country_code_without_range
75
+ phone_number = new_phone_number(:country_code => '1', :number => '1' * 9)
89
76
  assert !phone_number.valid?
90
- assert phone_number.errors.invalid?(:number)
77
+ assert_equal 'is the wrong length (should be 10 characters)', phone_number.errors.on(:number)
91
78
 
92
79
  phone_number.number += '1'
93
80
  assert phone_number.valid?
94
81
 
95
82
  phone_number.number += '1'
96
83
  assert !phone_number.valid?
97
- assert phone_number.errors.invalid?(:number)
84
+ assert_equal 'is the wrong length (should be 10 characters)', phone_number.errors.on(:number)
85
+ end
86
+
87
+ def test_should_require_number_be_within_range_for_country_code_with_range
88
+ phone_number = new_phone_number(:country_code => '32', :number => '1' * 7)
89
+ assert !phone_number.valid?
90
+ assert_equal 'is too short (minimum is 8 characters)', phone_number.errors.on(:number)
91
+
92
+ phone_number.number += '1'
93
+ assert phone_number.valid?
94
+
95
+ phone_number.number += '1'
96
+ assert phone_number.valid?
97
+
98
+ phone_number.number += '1'
99
+ assert !phone_number.valid?
100
+ assert_equal 'is too long (maximum is 9 characters)', phone_number.errors.on(:number)
98
101
  end
99
102
 
100
103
  def test_should_not_require_an_extension
@@ -102,7 +105,7 @@ class PhoneNumberTest < Test::Unit::TestCase
102
105
  assert phone_number.valid?
103
106
  end
104
107
 
105
- def test_should_require_extension_be_at_most_10_numbers
108
+ def test_should_require_extension_be_at_most_10_digits
106
109
  phone_number = new_phone_number(:extension => '1' * 9)
107
110
  assert phone_number.valid?
108
111
 
@@ -150,18 +153,167 @@ class PhoneNumberAfterBeingCreatedTest < Test::Unit::TestCase
150
153
  def test_should_have_a_phoneable_association
151
154
  assert_equal @person, @phone_number.phoneable
152
155
  end
156
+ end
157
+
158
+ class PhoneNumberParserTest < Test::Unit::TestCase
159
+ def setup
160
+ @person = create_person
161
+ @phone_number = new_phone_number(:phoneable => @person, :country_code => nil, :number => nil, :extension => nil)
162
+ end
163
+
164
+ def test_should_parse_country_code_with_1_digit
165
+ @phone_number.content = '11234567890'
166
+
167
+ assert @phone_number.valid?
168
+ assert_equal '1', @phone_number.country_code
169
+ assert_equal '1234567890', @phone_number.number
170
+ assert_nil @phone_number.extension
171
+ end
153
172
 
154
- def test_should_have_a_display_value
155
- assert_equal '1- 1234567890', @phone_number.display_value
173
+ def test_should_parse_country_code_with_2_digits
174
+ @phone_number.content = '20123456789'
175
+
176
+ assert @phone_number.valid?
177
+ assert_equal '20', @phone_number.country_code
178
+ assert_equal '123456789', @phone_number.number
179
+ assert_nil @phone_number.extension
180
+ end
181
+
182
+ def test_should_parse_country_code_with_3_digits
183
+ @phone_number.content = '21212345678'
184
+
185
+ assert @phone_number.valid?
186
+ assert_equal '212', @phone_number.country_code
187
+ assert_equal '12345678', @phone_number.number
188
+ assert_nil @phone_number.extension
189
+ end
190
+
191
+ def test_should_parse_number_with_spaces
192
+ @phone_number.content = '1 123 456 7890'
193
+
194
+ assert @phone_number.valid?
195
+ assert_equal '1', @phone_number.country_code
196
+ assert_equal '1234567890', @phone_number.number
197
+ assert_nil @phone_number.extension
198
+ end
199
+
200
+ def test_should_parse_number_with_dashes
201
+ @phone_number.content = '1-123-456-7890'
202
+
203
+ assert @phone_number.valid?
204
+ assert_equal '1', @phone_number.country_code
205
+ assert_equal '1234567890', @phone_number.number
206
+ assert_nil @phone_number.extension
207
+ end
208
+
209
+ def test_should_parse_number_with_parentheses
210
+ @phone_number.content = '1- (123) 456-7890'
211
+
212
+ assert @phone_number.valid?
213
+ assert_equal '1', @phone_number.country_code
214
+ assert_equal '1234567890', @phone_number.number
215
+ assert_nil @phone_number.extension
216
+ end
217
+
218
+ def test_should_parse_number_with_leading_zero
219
+ @phone_number.content = '011234567890'
220
+
221
+ assert @phone_number.valid?
222
+ assert_equal '1', @phone_number.country_code
223
+ assert_equal '1234567890', @phone_number.number
224
+ assert_nil @phone_number.extension
225
+ end
226
+
227
+ def test_should_parse_number_with_multiple_leading_zeroes
228
+ @phone_number.content = '0011234567890'
229
+
230
+ assert @phone_number.valid?
231
+ assert_equal '1', @phone_number.country_code
232
+ assert_equal '1234567890', @phone_number.number
233
+ assert_nil @phone_number.extension
234
+ end
235
+
236
+ def test_should_parse_extension_with_leading_ext
237
+ @phone_number.content = '11234567890 ext. 123'
238
+
239
+ assert @phone_number.valid?
240
+ assert_equal '1', @phone_number.country_code
241
+ assert_equal '1234567890', @phone_number.number
242
+ assert_equal '123', @phone_number.extension
243
+ end
244
+
245
+ def test_should_parse_extension_with_leading_ex
246
+ @phone_number.content = '11234567890 ex:123'
247
+
248
+ assert @phone_number.valid?
249
+ assert_equal '1', @phone_number.country_code
250
+ assert_equal '1234567890', @phone_number.number
251
+ assert_equal '123', @phone_number.extension
252
+ end
253
+
254
+ def test_should_parse_extension_with_leading_xt
255
+ @phone_number.content = '11234567890 xt: 123'
256
+
257
+ assert @phone_number.valid?
258
+ assert_equal '1', @phone_number.country_code
259
+ assert_equal '1234567890', @phone_number.number
260
+ assert_equal '123', @phone_number.extension
261
+ end
262
+
263
+ def test_should_parse_extension_with_leading_x
264
+ @phone_number.content = '11234567890 x. 123'
265
+
266
+ assert @phone_number.valid?
267
+ assert_equal '1', @phone_number.country_code
268
+ assert_equal '1234567890', @phone_number.number
269
+ assert_equal '123', @phone_number.extension
270
+ end
271
+
272
+ def test_should_parse_extension_with_mixed_case
273
+ @phone_number.content = '11234567890 xT 123'
274
+
275
+ assert @phone_number.valid?
276
+ assert_equal '1', @phone_number.country_code
277
+ assert_equal '1234567890', @phone_number.number
278
+ assert_equal '123', @phone_number.extension
279
+ end
280
+
281
+ def test_should_not_be_valid_if_parse_fails
282
+ @phone_number.content = '1123456789'
283
+
284
+ assert !@phone_number.valid?
285
+ assert_equal '1', @phone_number.country_code
286
+ assert_equal '123456789', @phone_number.number
287
+ assert_nil @phone_number.extension
156
288
  end
157
289
  end
158
290
 
159
- class PhoneNumberWithExtensionTest < Test::Unit::TestCase
291
+ class PhoneNumberParserWithDefaultCountryCodeTest < Test::Unit::TestCase
160
292
  def setup
161
- @phone_number = create_phone_number(:country_code => '1', :number => '1234567890', :extension => '123')
293
+ PhoneNumber.use_default_country_code_on_parse = true
294
+
295
+ @person = create_person
296
+ @phone_number = new_phone_number(:phoneable => @person, :content => '7234567890 ex. 12')
297
+ @valid = @phone_number.valid?
298
+ end
299
+
300
+ def test_should_be_valid
301
+ assert @valid
302
+ end
303
+
304
+ def test_should_use_default_country_code
305
+ assert_equal '1', @phone_number.country_code
306
+ end
307
+
308
+ def test_should_parse_number
309
+ assert_equal '7234567890', @phone_number.number
310
+ end
311
+
312
+ def test_should_parse_extension
313
+ assert_equal '12', @phone_number.extension
162
314
  end
163
315
 
164
- def test_should_have_a_display_value
165
- assert_equal '1- 1234567890 ext. 123', @phone_number.display_value
316
+ def teardown
317
+ PhoneNumber.use_default_country_code_on_parse = false
166
318
  end
167
319
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: has_phone_numbers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Pfeifer
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-12-14 00:00:00 -05:00
12
+ date: 2009-04-19 00:00:00 -04:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -38,8 +38,6 @@ files:
38
38
  - test/app_root/db/migrate
39
39
  - test/app_root/db/migrate/002_migrate_has_phone_numbers_to_version_1.rb
40
40
  - test/app_root/db/migrate/001_create_people.rb
41
- - test/app_root/config
42
- - test/app_root/config/environment.rb
43
41
  - test/app_root/app
44
42
  - test/app_root/app/models
45
43
  - test/app_root/app/models/person.rb
@@ -70,7 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
68
  requirements: []
71
69
 
72
70
  rubyforge_project: pluginaweek
73
- rubygems_version: 1.2.0
71
+ rubygems_version: 1.3.1
74
72
  signing_key:
75
73
  specification_version: 2
76
74
  summary: Demonstrates a reference implementation for handling phone numbers.
@@ -1,9 +0,0 @@
1
- require 'config/boot'
2
- require "#{File.dirname(__FILE__)}/../../../../plugins_plus/boot"
3
-
4
- Rails::Initializer.run do |config|
5
- config.plugin_paths << '..'
6
- config.plugins = %w(plugins_plus has_phone_numbers)
7
- config.cache_classes = false
8
- config.whiny_nils = true
9
- end