phonie 1.0.4 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/phonie.rb +3 -1
- data/lib/phonie/country.rb +51 -65
- data/lib/phonie/data/phone_countries.yml +415 -415
- data/lib/phonie/phone.rb +58 -85
- data/lib/phonie/version.rb +1 -1
- data/phonie.gemspec +5 -3
- data/test/countries/gr_test.rb +1 -1
- data/test/countries/gu_test.rb +1 -1
- data/test/country_test.rb +5 -5
- data/test/phone_test.rb +21 -21
- data/tools/generate +12 -10
- metadata +8 -6
data/lib/phonie/phone.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
require 'active_model/naming'
|
2
|
+
require 'active_model/translation'
|
3
|
+
require 'active_model/validations'
|
4
|
+
|
1
5
|
# An object representing a phone number.
|
2
6
|
#
|
3
7
|
# The phone number is recorded in 3 separate parts:
|
@@ -11,6 +15,8 @@
|
|
11
15
|
#
|
12
16
|
module Phonie
|
13
17
|
class Phone
|
18
|
+
EXTENSION = /[ ]*(ext|ex|x|xt|#|:)+[^0-9]*\(*([-0-9]{1,})\)*#?$/i
|
19
|
+
|
14
20
|
attr_accessor :country_code, :area_code, :number, :extension, :country
|
15
21
|
|
16
22
|
cattr_accessor :default_country_code
|
@@ -29,6 +35,11 @@ module Phonie
|
|
29
35
|
:us => "(%a) %f-%l"
|
30
36
|
}
|
31
37
|
|
38
|
+
include ActiveModel::Validations
|
39
|
+
validates :country_code, :presence => true
|
40
|
+
validates :area_code, :presence => true
|
41
|
+
validates :number, :presence => true
|
42
|
+
|
32
43
|
def initialize(*hash_or_args)
|
33
44
|
if hash_or_args.first.is_a?(Hash)
|
34
45
|
hash_or_args = hash_or_args.first
|
@@ -37,103 +48,49 @@ module Phonie
|
|
37
48
|
keys = {:number => 0, :area_code => 1, :country_code => 2, :extension => 3, :country => 4}
|
38
49
|
end
|
39
50
|
|
40
|
-
self.number
|
41
|
-
self.area_code
|
51
|
+
self.number = hash_or_args[ keys[:number] ]
|
52
|
+
self.area_code = hash_or_args[ keys[:area_code] ] || self.default_area_code
|
42
53
|
self.country_code = hash_or_args[ keys[:country_code] ] || self.default_country_code
|
43
|
-
self.extension
|
44
|
-
self.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?
|
54
|
+
self.extension = hash_or_args[ keys[:extension] ]
|
55
|
+
self.country = hash_or_args[ keys[:country] ]
|
50
56
|
end
|
51
57
|
|
52
|
-
def self.parse!(string, options={})
|
53
|
-
|
58
|
+
def self.parse!(string, options = {})
|
59
|
+
pn = parse(string, options)
|
60
|
+
raise ArgumentError.new("#{string} is not a valid phone number") unless pn && pn.valid?
|
61
|
+
pn
|
54
62
|
end
|
55
63
|
|
56
64
|
# create a new phone number by parsing a string
|
57
65
|
# the format of the string is detect automatically (from FORMATS)
|
58
|
-
def self.parse(string, options={})
|
59
|
-
return
|
66
|
+
def self.parse(string, options = {})
|
67
|
+
return unless string.present?
|
60
68
|
|
61
|
-
|
69
|
+
options[:country_code] ||= self.default_country_code
|
70
|
+
options[:area_code] ||= self.default_area_code
|
62
71
|
|
63
72
|
extension = extract_extension(string)
|
64
73
|
normalized = normalize(string)
|
65
74
|
|
66
|
-
options[:country_code]
|
67
|
-
options[:area_code]
|
68
|
-
|
69
|
-
parts =
|
70
|
-
|
71
|
-
|
72
|
-
if pn.present? and extension.present?
|
73
|
-
pn.extension = extension
|
74
|
-
end
|
75
|
-
pn
|
75
|
+
return unless country = Country.detect(normalized, options[:country_code], options[:area_code])
|
76
|
+
parts = country.parse(normalized, options[:area_code])
|
77
|
+
parts[:country] = country
|
78
|
+
parts[:country_code] = country.country_code
|
79
|
+
parts[:extension] = extension
|
80
|
+
new(parts)
|
76
81
|
end
|
77
82
|
|
78
83
|
# is this string a valid phone number?
|
79
84
|
def self.valid?(string, options = {})
|
80
|
-
|
81
|
-
|
82
|
-
rescue
|
83
|
-
false # don't raise exceptions on parse errors
|
84
|
-
end
|
85
|
+
pn = parse(string, options)
|
86
|
+
pn && pn.valid?
|
85
87
|
end
|
86
88
|
|
87
89
|
def self.is_mobile?(string, options = {})
|
88
90
|
pn = parse(string, options)
|
89
|
-
|
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])
|
91
|
+
pn && pn.is_mobile?
|
104
92
|
end
|
105
93
|
|
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
94
|
def area_code_long
|
138
95
|
"0" + area_code if area_code
|
139
96
|
end
|
@@ -157,7 +114,7 @@ module Phonie
|
|
157
114
|
|
158
115
|
# Formats the phone number.
|
159
116
|
#
|
160
|
-
# if the method argument is a String, it is used as a format string, with the following fields being interpolated:
|
117
|
+
# if the method argument is a String, it is used as a format string, with the following fields being interpolated:
|
161
118
|
#
|
162
119
|
# * %c - country_code (385)
|
163
120
|
# * %a - area_code (91)
|
@@ -171,7 +128,7 @@ module Phonie
|
|
171
128
|
# pn.format(:europe)
|
172
129
|
def format(fmt)
|
173
130
|
if fmt.is_a?(Symbol)
|
174
|
-
raise "The format #{fmt} doesn't exist
|
131
|
+
raise ArgumentError.new("The format #{fmt} doesn't exist") unless named_formats.has_key?(fmt)
|
175
132
|
format_number named_formats[fmt]
|
176
133
|
else
|
177
134
|
format_number(fmt)
|
@@ -201,15 +158,31 @@ module Phonie
|
|
201
158
|
|
202
159
|
private
|
203
160
|
|
161
|
+
# split string into hash with keys :country_code, :area_code and :number
|
162
|
+
def self.split_to_parts(string, options = {})
|
163
|
+
country = Country.detect(string, options[:country_code], options[:area_code])
|
164
|
+
country && country.parse(string, options[:area_code])
|
165
|
+
end
|
166
|
+
|
167
|
+
# fix string so it's easier to parse, remove extra characters etc.
|
168
|
+
def self.normalize(string_with_number)
|
169
|
+
string_with_number.sub(EXTENSION, '').gsub(/\(0\)|[^0-9+]/, '').gsub(/^00/, '+')
|
170
|
+
end
|
171
|
+
|
172
|
+
# pull off anything that look like an extension
|
173
|
+
def self.extract_extension(string)
|
174
|
+
return unless string && string.match(EXTENSION)
|
175
|
+
Regexp.last_match[2]
|
176
|
+
end
|
177
|
+
|
204
178
|
def format_number(fmt)
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
return result
|
179
|
+
fmt.gsub("%c", country_code || "").
|
180
|
+
gsub("%a", area_code || "").
|
181
|
+
gsub("%A", area_code_long || "").
|
182
|
+
gsub("%n", number || "").
|
183
|
+
gsub("%f", number1 || "").
|
184
|
+
gsub("%l", number2 || "").
|
185
|
+
gsub("%x", extension || "")
|
213
186
|
end
|
214
187
|
end
|
215
188
|
end
|
data/lib/phonie/version.rb
CHANGED
data/phonie.gemspec
CHANGED
@@ -6,8 +6,8 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.name = "phonie"
|
7
7
|
s.version = Phonie::VERSION
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
|
-
s.authors
|
10
|
-
s.email
|
9
|
+
s.authors = ['Tomislav Car', 'Todd Eichel', 'Don Morrison', 'Wesley Moxam', 'Lance Ivy']
|
10
|
+
s.email = ['tomislav@infinum.hr', 'todd@toddeichel.com', 'elskwid@gmail.com', 'wesley@wmoxam.com', 'lance@kickstarter.com']
|
11
11
|
s.homepage = "http://github.com/wmoxam/phonie"
|
12
12
|
s.summary = %q{Phone number parsing, validation and formatting}
|
13
13
|
s.description = %q{Phone number parsing, validation and formatting}
|
@@ -15,7 +15,9 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.files = `git ls-files`.split("\n")
|
16
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
17
|
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_dependency 'activemodel'
|
20
|
+
|
18
21
|
s.add_development_dependency 'rake'
|
19
22
|
s.add_development_dependency 'nokogiri'
|
20
|
-
s.add_development_dependency 'activemodel'
|
21
23
|
end
|
data/test/countries/gr_test.rb
CHANGED
data/test/countries/gu_test.rb
CHANGED
data/test/country_test.rb
CHANGED
@@ -3,10 +3,10 @@ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
|
|
3
3
|
class CountryTest < Phonie::TestCase
|
4
4
|
def test_find_by_country_name
|
5
5
|
country = Phonie::Country.find_by_name('canada')
|
6
|
-
assert_equal country.name
|
6
|
+
assert_equal "Canada", country.name
|
7
7
|
|
8
8
|
country = Phonie::Country.find_by_name('Canada')
|
9
|
-
assert_equal country.name
|
9
|
+
assert_equal "Canada", country.name
|
10
10
|
|
11
11
|
assert_nil Phonie::Country.find_by_name(nil)
|
12
12
|
assert_nil Phonie::Country.find_by_country_code(nil)
|
@@ -15,13 +15,13 @@ class CountryTest < Phonie::TestCase
|
|
15
15
|
|
16
16
|
def test_find_by_country_code
|
17
17
|
country = Phonie::Country.find_by_country_code('NO')
|
18
|
-
assert_equal country.name
|
18
|
+
assert_equal "Norway", country.name
|
19
19
|
end
|
20
20
|
|
21
21
|
def test_find_all_by_phone_code
|
22
22
|
countries = Phonie::Country.find_all_by_phone_code('47')
|
23
|
-
assert_equal countries.length
|
24
|
-
assert_equal countries.first.name
|
23
|
+
assert_equal 1, countries.length
|
24
|
+
assert_equal "Norway", countries.first.name
|
25
25
|
end
|
26
26
|
|
27
27
|
end
|
data/test/phone_test.rb
CHANGED
@@ -9,18 +9,18 @@ class PhoneTest < Phonie::TestCase
|
|
9
9
|
def test_number_without_country_code_initialize
|
10
10
|
Phonie::Phone.default_country_code = nil
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
pn = Phonie::Phone.new '5125486', '91'
|
13
|
+
assert !pn.valid?
|
14
|
+
assert_equal ["can't be blank"], pn.errors[:country_code]
|
15
15
|
end
|
16
16
|
|
17
|
-
def
|
18
|
-
Phonie::Phone.default_country_code =
|
17
|
+
def test_number_without_area_code_initialize
|
18
|
+
Phonie::Phone.default_country_code = '1'
|
19
19
|
Phonie::Phone.default_area_code = nil
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
pn = Phonie::Phone.new '451588'
|
22
|
+
assert !pn.valid?
|
23
|
+
assert_equal ["can't be blank"], pn.errors[:area_code]
|
24
24
|
end
|
25
25
|
|
26
26
|
def test_number_with_default_area_code_initialize
|
@@ -52,8 +52,8 @@ class PhoneTest < Phonie::TestCase
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def test_parse_empty
|
55
|
-
assert_equal Phonie::Phone.parse('')
|
56
|
-
assert_equal Phonie::Phone.parse(nil)
|
55
|
+
assert_equal nil, Phonie::Phone.parse('')
|
56
|
+
assert_equal nil, Phonie::Phone.parse(nil)
|
57
57
|
end
|
58
58
|
|
59
59
|
def test_parse_short_without_special_characters_without_country
|
@@ -61,7 +61,7 @@ class PhoneTest < Phonie::TestCase
|
|
61
61
|
|
62
62
|
assert_nil Phonie::Phone.parse "0915125486"
|
63
63
|
|
64
|
-
assert_raise
|
64
|
+
assert_raise ArgumentError do
|
65
65
|
Phonie::Phone.parse! "0915125486"
|
66
66
|
end
|
67
67
|
end
|
@@ -71,7 +71,7 @@ class PhoneTest < Phonie::TestCase
|
|
71
71
|
|
72
72
|
assert_nil Phonie::Phone.parse "091/512-5486"
|
73
73
|
|
74
|
-
assert_raise
|
74
|
+
assert_raise ArgumentError do
|
75
75
|
Phonie::Phone.parse! "091/512-5486"
|
76
76
|
end
|
77
77
|
end
|
@@ -97,23 +97,23 @@ class PhoneTest < Phonie::TestCase
|
|
97
97
|
def test_format_special_without_country_code
|
98
98
|
Phonie::Phone.default_country_code = '385'
|
99
99
|
pn = Phonie::Phone.new '5125486', '91'
|
100
|
-
assert_equal pn.format("%A/%f-%l")
|
100
|
+
assert_equal '091/512-5486', pn.format("%A/%f-%l")
|
101
101
|
end
|
102
102
|
|
103
103
|
def test_format_with_symbol_specifier
|
104
104
|
Phonie::Phone.default_country_code = nil
|
105
105
|
pn = Phonie::Phone.new '5125486', '91', '385'
|
106
|
-
assert_equal
|
106
|
+
assert_equal '+385 (0) 91 512 5486', pn.format(:europe)
|
107
107
|
end
|
108
108
|
|
109
109
|
def test_valid
|
110
|
-
|
111
|
-
|
110
|
+
assert Phonie::Phone.valid?('915125486', :country_code => '385')
|
111
|
+
assert Phonie::Phone.valid?('385915125486')
|
112
112
|
end
|
113
113
|
|
114
114
|
def test_doesnt_validate
|
115
|
-
|
116
|
-
|
115
|
+
assert !Phonie::Phone.valid?('asdas')
|
116
|
+
assert !Phonie::Phone.valid?('38591512548678')
|
117
117
|
end
|
118
118
|
|
119
119
|
def test_comparison_true
|
@@ -129,9 +129,9 @@ class PhoneTest < Phonie::TestCase
|
|
129
129
|
end
|
130
130
|
|
131
131
|
def test_parse_number_without_international_code
|
132
|
-
assert_equal
|
133
|
-
assert_equal
|
134
|
-
assert_equal
|
132
|
+
assert_equal nil, Phonie::Phone.parse("90123456")
|
133
|
+
assert_equal "+4790123456", Phonie::Phone.parse("90123456", :country_code => '47').format(:default)
|
134
|
+
assert_equal "+4790123456", Phonie::Phone.parse("90123456", :country_code => '47', :area_code => '').format(:default)
|
135
135
|
end
|
136
136
|
|
137
137
|
end
|
data/tools/generate
CHANGED
@@ -23,15 +23,15 @@ def already_exists?
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def get_country_code
|
26
|
-
YAML.load(File.read(@data_file)).
|
27
|
-
return c[:
|
26
|
+
YAML.load(File.read(@data_file)).each do |c|
|
27
|
+
return c[:iso_3166_code].downcase if c[:name].downcase == @country.downcase
|
28
28
|
end
|
29
29
|
nil
|
30
30
|
end
|
31
31
|
|
32
32
|
def get_country_call_code
|
33
|
-
YAML.load(File.read(@data_file)).
|
34
|
-
return
|
33
|
+
YAML.load(File.read(@data_file)).each do |c|
|
34
|
+
return c[:country_code] if c[:name].downcase == @country.downcase
|
35
35
|
end
|
36
36
|
nil
|
37
37
|
end
|
@@ -48,24 +48,26 @@ class #{@country_code.upcase}Test < Phonie::TestCase
|
|
48
48
|
def test_local
|
49
49
|
parse_test('+#{country_call_code}', '#{country_call_code}', '', '', '#{@country}', false)
|
50
50
|
end
|
51
|
-
|
51
|
+
|
52
52
|
def test_mobile
|
53
53
|
parse_test('+#{country_call_code}', '#{country_call_code}', '', '', '#{@country}', true)
|
54
54
|
end
|
55
|
-
end
|
55
|
+
end
|
56
56
|
eof
|
57
57
|
end
|
58
58
|
puts "Create: #{@test_filename}"
|
59
59
|
end
|
60
60
|
|
61
61
|
def add_missing_fields
|
62
|
-
|
63
|
-
|
62
|
+
arr = YAML.load(File.read(@data_file))
|
63
|
+
arr.find{|c| c[:name].downcase == @country.downcase}.merge!(
|
64
|
+
:area_code => ' ',
|
64
65
|
:local_number_format => ' ',
|
65
66
|
:mobile_format => ' ',
|
66
|
-
:number_format => ' '
|
67
|
+
:number_format => ' '
|
68
|
+
)
|
67
69
|
File.open(@data_file, 'w') do |f|
|
68
|
-
f.puts
|
70
|
+
f.puts arr.to_yaml
|
69
71
|
end
|
70
72
|
|
71
73
|
puts "Modified: #{@data_file}"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: phonie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,20 +9,21 @@ authors:
|
|
9
9
|
- Todd Eichel
|
10
10
|
- Don Morrison
|
11
11
|
- Wesley Moxam
|
12
|
+
- Lance Ivy
|
12
13
|
autorequire:
|
13
14
|
bindir: bin
|
14
15
|
cert_chain: []
|
15
|
-
date: 2013-
|
16
|
+
date: 2013-05-01 00:00:00.000000000 Z
|
16
17
|
dependencies:
|
17
18
|
- !ruby/object:Gem::Dependency
|
18
|
-
name:
|
19
|
+
name: activemodel
|
19
20
|
requirement: !ruby/object:Gem::Requirement
|
20
21
|
none: false
|
21
22
|
requirements:
|
22
23
|
- - ! '>='
|
23
24
|
- !ruby/object:Gem::Version
|
24
25
|
version: '0'
|
25
|
-
type: :
|
26
|
+
type: :runtime
|
26
27
|
prerelease: false
|
27
28
|
version_requirements: !ruby/object:Gem::Requirement
|
28
29
|
none: false
|
@@ -31,7 +32,7 @@ dependencies:
|
|
31
32
|
- !ruby/object:Gem::Version
|
32
33
|
version: '0'
|
33
34
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
35
|
+
name: rake
|
35
36
|
requirement: !ruby/object:Gem::Requirement
|
36
37
|
none: false
|
37
38
|
requirements:
|
@@ -47,7 +48,7 @@ dependencies:
|
|
47
48
|
- !ruby/object:Gem::Version
|
48
49
|
version: '0'
|
49
50
|
- !ruby/object:Gem::Dependency
|
50
|
-
name:
|
51
|
+
name: nokogiri
|
51
52
|
requirement: !ruby/object:Gem::Requirement
|
52
53
|
none: false
|
53
54
|
requirements:
|
@@ -68,6 +69,7 @@ email:
|
|
68
69
|
- todd@toddeichel.com
|
69
70
|
- elskwid@gmail.com
|
70
71
|
- wesley@wmoxam.com
|
72
|
+
- lance@kickstarter.com
|
71
73
|
executables: []
|
72
74
|
extensions: []
|
73
75
|
extra_rdoc_files: []
|