has_phone_numbers 0.1.0 → 0.2.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.
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