superp-phone 1.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.
- data/LICENSE +20 -0
- data/Readme.rdoc +153 -0
- data/data/phone_countries.yml +1683 -0
- data/lib/country.rb +120 -0
- data/lib/phone.rb +218 -0
- data/lib/support.rb +78 -0
- data/test/countries/au_test.rb +48 -0
- data/test/countries/ba_test.rb +9 -0
- data/test/countries/be_test.rb +120 -0
- data/test/countries/ca_test.rb +20 -0
- data/test/countries/de_test.rb +18 -0
- data/test/countries/fr_test.rb +22 -0
- data/test/countries/gb_test.rb +262 -0
- data/test/countries/hr_test.rb +75 -0
- data/test/countries/hu_test.rb +12 -0
- data/test/countries/in_test.rb +45 -0
- data/test/countries/nl_test.rb +383 -0
- data/test/countries/pt_test.rb +129 -0
- data/test/countries/rs_test.rb +15 -0
- data/test/countries/se_test.rb +478 -0
- data/test/countries/si_test.rb +19 -0
- data/test/countries/ua_test.rb +17 -0
- data/test/countries/us_test.rb +24 -0
- data/test/countries/za_test.rb +19 -0
- data/test/extension_test.rb +30 -0
- data/test/phone_test.rb +137 -0
- data/test/test_helper.rb +37 -0
- metadata +105 -0
data/lib/country.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
module Phoner
|
2
|
+
class Country < Struct.new(:name, :country_code, :char_2_code, :char_3_code, :area_code, :local_number_format, :mobile_format, :full_number_length, :number_format)
|
3
|
+
cattr_accessor :all
|
4
|
+
|
5
|
+
def self.load
|
6
|
+
return @@all if @@all.present?
|
7
|
+
|
8
|
+
data_file = File.join(File.dirname(__FILE__), '..', 'data', 'phone_countries.yml')
|
9
|
+
|
10
|
+
@@all = []
|
11
|
+
YAML.load(File.read(data_file)).each_pair do |key, c|
|
12
|
+
next unless c[:area_code] && c[:local_number_format]
|
13
|
+
@@all << Country.new(c[:name], c[:country_code], c[:char_2_code], c[:char_3_code], c[:area_code], c[:local_number_format], c[:mobile_format], c[:full_number_length], c[:number_format])
|
14
|
+
end
|
15
|
+
@@all
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
name
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.find_all_by_phone_code(code)
|
23
|
+
return [] if code.nil?
|
24
|
+
@@all.select {|c| c.country_code == code }
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.find_by_country_code(code)
|
28
|
+
return nil if code.nil?
|
29
|
+
@@all.each {|c| return c if c.char_3_code.downcase == code.downcase }
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.find_by_name(name)
|
34
|
+
return nil if name.nil?
|
35
|
+
@@all.each {|c| return c if c.name.downcase == name.downcase }
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# detect country from the string entered
|
40
|
+
def self.detect(string, default_country_code, default_area_code)
|
41
|
+
Country.find_all_by_phone_code(default_country_code).each do |country|
|
42
|
+
return country if country.matches_local_number?(string, default_area_code)
|
43
|
+
end
|
44
|
+
|
45
|
+
# find if the number has a country code
|
46
|
+
Country.all.each do |country|
|
47
|
+
return country if country.matches_full_number?(string)
|
48
|
+
end
|
49
|
+
return nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def is_mobile?(number)
|
53
|
+
return true if mobile_format.nil?
|
54
|
+
number =~ mobile_number_regex ? true : false
|
55
|
+
end
|
56
|
+
|
57
|
+
def matches_local_number?(string, default_area_code)
|
58
|
+
((string =~ full_number_regexp ||
|
59
|
+
string =~ area_code_number_regexp) && string =~ number_format_regex) ||
|
60
|
+
((string =~ number_regex) && (default_area_code =~ area_code_regex))
|
61
|
+
end
|
62
|
+
|
63
|
+
def matches_full_number?(string)
|
64
|
+
string =~ full_number_regexp && string =~ number_format_regex
|
65
|
+
end
|
66
|
+
|
67
|
+
def number_parts(number, default_area_code)
|
68
|
+
number_part = if default_area_code
|
69
|
+
number.match(number_regex)
|
70
|
+
$1
|
71
|
+
else
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
75
|
+
if number_part.nil?
|
76
|
+
matches = number.match(area_code_number_regexp)
|
77
|
+
area_part = $1
|
78
|
+
number_part = matches.to_a.last
|
79
|
+
end
|
80
|
+
|
81
|
+
if number_part.nil?
|
82
|
+
matches = number.match(full_number_regexp)
|
83
|
+
country_part, area_part = $1, $2
|
84
|
+
number_part = matches.to_a.last
|
85
|
+
end
|
86
|
+
|
87
|
+
area_part ||= default_area_code
|
88
|
+
|
89
|
+
raise "Could not determine area code" if area_part.nil?
|
90
|
+
raise "Could not determine number" if number_part.nil?
|
91
|
+
|
92
|
+
{:number => number_part, :area_code => area_part, :country_code => country_code, :country => self}
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
def number_format_regex
|
97
|
+
Regexp.new("^[+0]?(#{country_code})?(#{number_format})$")
|
98
|
+
end
|
99
|
+
|
100
|
+
def full_number_regexp
|
101
|
+
Regexp.new("^[+]?(#{country_code})(#{area_code})(#{local_number_format})$")
|
102
|
+
end
|
103
|
+
|
104
|
+
def area_code_number_regexp
|
105
|
+
Regexp.new("^0?(#{area_code})(#{local_number_format})$")
|
106
|
+
end
|
107
|
+
|
108
|
+
def area_code_regex
|
109
|
+
Regexp.new("^0?(#{area_code})$")
|
110
|
+
end
|
111
|
+
|
112
|
+
def mobile_number_regex
|
113
|
+
Regexp.new("^0?(#{mobile_format})$")
|
114
|
+
end
|
115
|
+
|
116
|
+
def number_regex
|
117
|
+
Regexp.new("^(#{local_number_format})$")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
data/lib/phone.rb
ADDED
@@ -0,0 +1,218 @@
|
|
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
|
+
require File.join(File.dirname(__FILE__), 'support') unless defined? ActiveSupport
|
13
|
+
require File.join(File.dirname(__FILE__), 'country')
|
14
|
+
|
15
|
+
module Phoner
|
16
|
+
class Phone
|
17
|
+
attr_accessor :country_code, :area_code, :number, :extension, :country
|
18
|
+
|
19
|
+
cattr_accessor :default_country_code
|
20
|
+
cattr_accessor :default_area_code
|
21
|
+
cattr_accessor :named_formats
|
22
|
+
|
23
|
+
# length of first number part (using multi number format)
|
24
|
+
cattr_accessor :n1_length
|
25
|
+
# default length of first number part
|
26
|
+
@@n1_length = 3
|
27
|
+
|
28
|
+
@@named_formats = {
|
29
|
+
:default => "+%c%a%n",
|
30
|
+
:default_with_extension => "+%c%a%nx%x",
|
31
|
+
:europe => '+%c (0) %a %f %l',
|
32
|
+
:us => "(%a) %f-%l"
|
33
|
+
}
|
34
|
+
|
35
|
+
def initialize(*hash_or_args)
|
36
|
+
if hash_or_args.first.is_a?(Hash)
|
37
|
+
hash_or_args = hash_or_args.first
|
38
|
+
keys = {:country => :country, :number => :number, :area_code => :area_code, :country_code => :country_code, :extension => :extension}
|
39
|
+
else
|
40
|
+
keys = {:number => 0, :area_code => 1, :country_code => 2, :extension => 3, :country => 4}
|
41
|
+
end
|
42
|
+
|
43
|
+
self.number = hash_or_args[ keys[:number] ]
|
44
|
+
self.area_code = hash_or_args[ keys[:area_code] ] || self.default_area_code
|
45
|
+
self.country_code = hash_or_args[ keys[:country_code] ] || self.default_country_code
|
46
|
+
self.extension = hash_or_args[ keys[:extension] ]
|
47
|
+
self.country = hash_or_args[ keys[:country] ]
|
48
|
+
|
49
|
+
# Santity checks
|
50
|
+
raise "Must enter number" if self.number.blank?
|
51
|
+
raise "Must enter area code or set default area code" if self.area_code.blank?
|
52
|
+
raise "Must enter country code or set default country code" if self.country_code.blank?
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.parse!(string, options={})
|
56
|
+
parse(string, options.merge(:raise_exception_on_error => true))
|
57
|
+
end
|
58
|
+
|
59
|
+
# create a new phone number by parsing a string
|
60
|
+
# the format of the string is detect automatically (from FORMATS)
|
61
|
+
def self.parse(string, options={})
|
62
|
+
return nil unless string.present?
|
63
|
+
|
64
|
+
Country.load
|
65
|
+
|
66
|
+
extension = extract_extension(string)
|
67
|
+
normalized = normalize(string)
|
68
|
+
|
69
|
+
options[:country_code] ||= self.default_country_code
|
70
|
+
options[:area_code] ||= self.default_area_code
|
71
|
+
|
72
|
+
parts = split_to_parts(normalized, options)
|
73
|
+
|
74
|
+
pn = Phone.new(parts) if parts
|
75
|
+
if pn.present? and extension.present?
|
76
|
+
pn.extension = extension
|
77
|
+
end
|
78
|
+
pn
|
79
|
+
end
|
80
|
+
|
81
|
+
# is this string a valid phone number?
|
82
|
+
def self.valid?(string, options = {})
|
83
|
+
begin
|
84
|
+
parse(string, options).present?
|
85
|
+
rescue
|
86
|
+
false # don't raise exceptions on parse errors
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.is_mobile?(string, options = {})
|
91
|
+
pn = parse(string, options)
|
92
|
+
return false if pn.nil?
|
93
|
+
pn.is_mobile?
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
# split string into hash with keys :country_code, :area_code and :number
|
98
|
+
def self.split_to_parts(string, options = {})
|
99
|
+
country = Country.detect(string, options[:country_code], options[:area_code])
|
100
|
+
|
101
|
+
if country.nil?
|
102
|
+
raise "Could not determine country" if options[:raise_exception_on_error]
|
103
|
+
return nil
|
104
|
+
end
|
105
|
+
|
106
|
+
country.number_parts(string, options[:area_code])
|
107
|
+
end
|
108
|
+
|
109
|
+
# fix string so it's easier to parse, remove extra characters etc.
|
110
|
+
def self.normalize(string_with_number)
|
111
|
+
string_with_number.sub(extension_regex, '').gsub(/\(0\)|[^0-9+]/, '').gsub(/^00/, '+')
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.extension_regex
|
115
|
+
/[ ]*(ext|ex|x|xt|#|:)+[^0-9]*\(*([-0-9]{1,})\)*#?$/i
|
116
|
+
end
|
117
|
+
|
118
|
+
# pull off anything that look like an extension
|
119
|
+
#
|
120
|
+
def self.extract_extension(string)
|
121
|
+
return nil if string.nil?
|
122
|
+
if string.match extension_regex
|
123
|
+
extension = $2
|
124
|
+
return extension
|
125
|
+
end
|
126
|
+
#
|
127
|
+
# We already returned any recognizable extension.
|
128
|
+
# However, we might still have extra junk to the right
|
129
|
+
# of the phone number proper, so just chop it off.
|
130
|
+
#
|
131
|
+
idx = string.rindex(/[0-9]/)
|
132
|
+
return nil if idx.nil?
|
133
|
+
return nil if idx == (string.length - 1) # at the end
|
134
|
+
string.slice!((idx+1)..-1) # chop it
|
135
|
+
return nil
|
136
|
+
end
|
137
|
+
|
138
|
+
public # instance methods
|
139
|
+
|
140
|
+
def area_code_long
|
141
|
+
"0" + area_code if area_code
|
142
|
+
end
|
143
|
+
|
144
|
+
# For many countries it's not apparent from the number
|
145
|
+
# Will return false positives rather than false negatives.
|
146
|
+
def is_mobile?
|
147
|
+
country.is_mobile? "#{area_code}#{number}"
|
148
|
+
end
|
149
|
+
|
150
|
+
# first n characters of :number
|
151
|
+
def number1
|
152
|
+
number[0...self.class.n1_length]
|
153
|
+
end
|
154
|
+
|
155
|
+
# everything left from number after the first n characters (see number1)
|
156
|
+
def number2
|
157
|
+
n2_length = number.size - self.class.n1_length
|
158
|
+
number[-n2_length, n2_length]
|
159
|
+
end
|
160
|
+
|
161
|
+
# Formats the phone number.
|
162
|
+
#
|
163
|
+
# if the method argument is a String, it is used as a format string, with the following fields being interpolated:
|
164
|
+
#
|
165
|
+
# * %c - country_code (385)
|
166
|
+
# * %a - area_code (91)
|
167
|
+
# * %A - area_code with leading zero (091)
|
168
|
+
# * %n - number (5125486)
|
169
|
+
# * %f - first @@n1_length characters of number (configured through Phone.n1_length), default is 3 (512)
|
170
|
+
# * %l - last characters of number (5486)
|
171
|
+
# * %x - entire extension
|
172
|
+
#
|
173
|
+
# if the method argument is a Symbol, it is used as a lookup key for a format String in Phone.named_formats
|
174
|
+
# pn.format(:europe)
|
175
|
+
def format(fmt)
|
176
|
+
if fmt.is_a?(Symbol)
|
177
|
+
raise "The format #{fmt} doesn't exist'" unless named_formats.has_key?(fmt)
|
178
|
+
format_number named_formats[fmt]
|
179
|
+
else
|
180
|
+
format_number(fmt)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# the default format is "+%c%a%n"
|
185
|
+
def to_s
|
186
|
+
format(:default)
|
187
|
+
end
|
188
|
+
|
189
|
+
# does this number belong to the default country code?
|
190
|
+
def has_default_country_code?
|
191
|
+
country_code == self.class.default_country_code
|
192
|
+
end
|
193
|
+
|
194
|
+
# does this number belong to the default area code?
|
195
|
+
def has_default_area_code?
|
196
|
+
area_code == self.class.default_area_code
|
197
|
+
end
|
198
|
+
|
199
|
+
# comparison of 2 phone objects
|
200
|
+
def ==(other)
|
201
|
+
methods = [:country_code, :area_code, :number, :extension]
|
202
|
+
methods.all? { |method| other.respond_to?(method) && send(method) == other.send(method) }
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
def format_number(fmt)
|
208
|
+
result = fmt.gsub("%c", country_code || "").
|
209
|
+
gsub("%a", area_code || "").
|
210
|
+
gsub("%A", area_code_long || "").
|
211
|
+
gsub("%n", number || "").
|
212
|
+
gsub("%f", number1 || "").
|
213
|
+
gsub("%l", number2 || "").
|
214
|
+
gsub("%x", extension || "")
|
215
|
+
return result
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
data/lib/support.rb
ADDED
@@ -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,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 < Phoner::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
|