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.
- data/.gitignore +1 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +10 -0
- data/LICENSE +20 -0
- data/Rakefile +13 -0
- data/Readme.rdoc +153 -0
- data/lib/phonie.rb +7 -0
- data/lib/phonie/country.rb +120 -0
- data/lib/phonie/data/phone_countries.yml +1683 -0
- data/lib/phonie/phone.rb +215 -0
- data/lib/phonie/support.rb +78 -0
- data/lib/phonie/version.rb +3 -0
- data/phonie.gemspec +18 -0
- data/test/countries/ae_test.rb +15 -0
- data/test/countries/af_test.rb +12 -0
- data/test/countries/al_test.rb +12 -0
- data/test/countries/ar_test.rb +12 -0
- data/test/countries/at_test.rb +14 -0
- data/test/countries/au_test.rb +48 -0
- data/test/countries/ba_test.rb +9 -0
- data/test/countries/bd_test.rb +17 -0
- data/test/countries/be_test.rb +120 -0
- data/test/countries/bg_test.rb +13 -0
- data/test/countries/bo_test.rb +12 -0
- data/test/countries/br_test.rb +12 -0
- data/test/countries/bt_test.rb +9 -0
- data/test/countries/by_test.rb +12 -0
- data/test/countries/bz_test.rb +12 -0
- data/test/countries/ca_test.rb +20 -0
- data/test/countries/cr_test.rb +12 -0
- data/test/countries/cy_test.rb +12 -0
- data/test/countries/cz_test.rb +12 -0
- data/test/countries/de_test.rb +18 -0
- data/test/countries/dk_test.rb +12 -0
- data/test/countries/dz_test.rb +12 -0
- data/test/countries/ec_test.rb +12 -0
- data/test/countries/ee_test.rb +12 -0
- data/test/countries/eg_test.rb +9 -0
- data/test/countries/et_test.rb +11 -0
- data/test/countries/fi_test.rb +12 -0
- data/test/countries/fr_test.rb +22 -0
- data/test/countries/gb_test.rb +262 -0
- data/test/countries/ge_test.rb +12 -0
- data/test/countries/gh_test.rb +9 -0
- data/test/countries/gr_test.rb +9 -0
- data/test/countries/gt_test.rb +12 -0
- data/test/countries/gu_test.rb +9 -0
- data/test/countries/gy_test.rb +9 -0
- data/test/countries/hr_test.rb +75 -0
- data/test/countries/hu_test.rb +12 -0
- data/test/countries/il_test.rb +12 -0
- data/test/countries/in_test.rb +45 -0
- data/test/countries/ir_test.rb +13 -0
- data/test/countries/ke_test.rb +12 -0
- data/test/countries/lk_test.rb +9 -0
- data/test/countries/ng_test.rb +9 -0
- data/test/countries/nl_test.rb +383 -0
- data/test/countries/no_test.rb +12 -0
- data/test/countries/np_test.rb +15 -0
- data/test/countries/ph_test.rb +9 -0
- data/test/countries/pk_test.rb +9 -0
- data/test/countries/pt_test.rb +129 -0
- data/test/countries/qa_test.rb +9 -0
- data/test/countries/rs_test.rb +15 -0
- data/test/countries/sa_test.rb +9 -0
- data/test/countries/se_test.rb +478 -0
- data/test/countries/si_test.rb +19 -0
- data/test/countries/sv_test.rb +12 -0
- data/test/countries/to_test.rb +12 -0
- data/test/countries/ua_test.rb +17 -0
- data/test/countries/us_test.rb +24 -0
- data/test/countries/uy_test.rb +20 -0
- data/test/countries/za_test.rb +19 -0
- data/test/countries/zw_test.rb +12 -0
- data/test/country_test.rb +27 -0
- data/test/extension_test.rb +30 -0
- data/test/phone_test.rb +137 -0
- data/test/test_helper.rb +37 -0
- data/test_usa_phones_with_extensions.csv +99 -0
- metadata +130 -0
data/lib/phonie/phone.rb
ADDED
@@ -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)
|
data/phonie.gemspec
ADDED
@@ -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
|