phoney 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/LICENCE +20 -0
- data/README.rdoc +85 -0
- data/Rakefile +37 -0
- data/lib/data/regions.yml +4179 -0
- data/lib/phoney.rb +5 -0
- data/lib/phoney/base.rb +100 -0
- data/lib/phoney/parser.rb +216 -0
- data/lib/phoney/region.rb +60 -0
- data/lib/phoney/utils.rb +52 -0
- data/lib/phoney/version.rb +11 -0
- data/phoney.gemspec +61 -0
- data/spec/parser/br_spec.rb +62 -0
- data/spec/parser/de_spec.rb +73 -0
- data/spec/parser/us_spec.rb +49 -0
- data/spec/phone_number_spec.rb +221 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +6 -0
- metadata +76 -0
data/lib/phoney.rb
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'phoney', 'version')
|
2
|
+
require File.join(File.dirname(__FILE__), 'phoney', 'utils')
|
3
|
+
require File.join(File.dirname(__FILE__), 'phoney', 'region')
|
4
|
+
require File.join(File.dirname(__FILE__), 'phoney', 'parser')
|
5
|
+
require File.join(File.dirname(__FILE__), 'phoney', 'base')
|
data/lib/phoney/base.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
class PhoneNumber
|
2
|
+
attr_accessor :country_code, :area_code, :number
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def default_region
|
6
|
+
@@region ||= Region[:us]
|
7
|
+
end
|
8
|
+
|
9
|
+
def default_region=(region)
|
10
|
+
if region.is_a?(Region)
|
11
|
+
@@region = region
|
12
|
+
else
|
13
|
+
@@region = Region[region.to_s.to_sym]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def default_country_code
|
18
|
+
@@country_code ||= default_region.country_code.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_country_code=(country_code)
|
22
|
+
@@country_code = country_code
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_area_code
|
26
|
+
@@area_code ||= nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_area_code=(area_code)
|
30
|
+
@@area_code = area_code
|
31
|
+
end
|
32
|
+
|
33
|
+
def version
|
34
|
+
VERSION::STRING
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(params, region_code=nil)
|
39
|
+
region = Region.find(region_code)
|
40
|
+
country_code = region.country_code.to_s if region
|
41
|
+
|
42
|
+
if params.is_a?(String)
|
43
|
+
params = Parser.parse_to_parts(params, region_code)
|
44
|
+
end
|
45
|
+
|
46
|
+
self.number = params[:number].to_s
|
47
|
+
# Can be empty, because some special numbers just don't have an area code (e.g. 911)
|
48
|
+
self.area_code = params[:area_code].to_s || self.class.default_area_code
|
49
|
+
self.country_code = params[:country_code].to_s || country_code || self.class.default_country_code
|
50
|
+
|
51
|
+
raise "Must enter number" if(self.number.nil? || self.number.empty?)
|
52
|
+
raise "Must enter country code or set default country code" if(self.country_code.nil? || self.country_code.empty?)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Does this number belong to the default country code?
|
56
|
+
def has_default_country_code?
|
57
|
+
country_code.to_s == self.class.default_country_code.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
# Does this number belong to the default area code?
|
61
|
+
def has_default_area_code?
|
62
|
+
(!area_code.to_s.empty? && area_code.to_s == self.class.default_area_code.to_s)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Formats the phone number.
|
66
|
+
# If the method argument is a String, it is used as a format string, with the following fields being interpolated:
|
67
|
+
#
|
68
|
+
# * %c - country_code (385)
|
69
|
+
# * %a - area_code (91)
|
70
|
+
# * %n - number (5125486)
|
71
|
+
#
|
72
|
+
# If the method argument is a Symbol, we use one of the default formattings and let the parser do the rest.
|
73
|
+
def format(fmt)
|
74
|
+
if fmt.is_a?(Symbol)
|
75
|
+
case fmt
|
76
|
+
when :default
|
77
|
+
Parser::parse("+#{country_code} #{area_code} #{number}", country_code)
|
78
|
+
when :national
|
79
|
+
Parser::parse("#{area_code} #{number}", country_code)
|
80
|
+
when :local
|
81
|
+
STDERR.puts "Warning: Using local format without setting a default area code!?" if PhoneNumber.default_area_code.nil?
|
82
|
+
Parser::parse(number, country_code)
|
83
|
+
else
|
84
|
+
raise "The format #{fmt} doesn't exist'"
|
85
|
+
end
|
86
|
+
else
|
87
|
+
format_number(fmt)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# The default format is the canonical format: "+{country_code} {area_code} {number}"
|
92
|
+
def to_s
|
93
|
+
format(:default)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
def format_number(fmt)
|
98
|
+
fmt.gsub("%c", country_code || "").gsub("%a", area_code || "").gsub("%n", number || "")
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
class PhoneNumber
|
4
|
+
|
5
|
+
module Parser
|
6
|
+
class << self
|
7
|
+
include Utils
|
8
|
+
|
9
|
+
def parse(phone_number, region_code=nil)
|
10
|
+
parse_to_parts(phone_number, region_code)[:formatted_number]
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse_to_parts(phone_number, region_code=nil)
|
14
|
+
phone_number = normalize(phone_number.to_s)
|
15
|
+
|
16
|
+
# we don't really need to do anything unless we get more input
|
17
|
+
unless phone_number.length > 1
|
18
|
+
return { :formatted_number => phone_number, :number => normalize(phone_number) }
|
19
|
+
end
|
20
|
+
|
21
|
+
region = Region.find(region_code) || PhoneNumber.default_region
|
22
|
+
country_code = region.country_code.to_s
|
23
|
+
area_code = nil
|
24
|
+
|
25
|
+
dialout_prefix = get_dialout_prefix(phone_number, region)
|
26
|
+
national_prefix = get_national_prefix(phone_number, region)
|
27
|
+
dialout_region = get_dialout_region(phone_number, region)
|
28
|
+
dialout_country = ''
|
29
|
+
rule_sets = get_rule_sets_for_region(phone_number, dialout_region || region)
|
30
|
+
|
31
|
+
# build our total prefix
|
32
|
+
if dialout_region
|
33
|
+
prefix = dialout_prefix.delete(' ') + dialout_region.country_code.to_s
|
34
|
+
country_code = dialout_region.country_code.to_s
|
35
|
+
dialout_country = country_code
|
36
|
+
else
|
37
|
+
prefix = national_prefix
|
38
|
+
prefix += dialout_prefix unless(dialout_prefix.empty?)
|
39
|
+
end
|
40
|
+
|
41
|
+
# strip the total prefix from the beginning of the number
|
42
|
+
phone_number = phone_number[prefix.length..-1]
|
43
|
+
number = phone_number
|
44
|
+
|
45
|
+
prefered_type = 0 # for sorting the priority
|
46
|
+
|
47
|
+
# if we're dialing out or using the national prefix
|
48
|
+
if(dialout_region || !national_prefix.empty?)
|
49
|
+
# we need to sort the rules slightly different
|
50
|
+
prefered_type = dialout_region.nil? ? 1 : 2
|
51
|
+
end
|
52
|
+
|
53
|
+
# sorting for rule priorities
|
54
|
+
rule_sets.each do |rule_set|
|
55
|
+
rule_set[:rules] = rule_set[:rules].sort_by do |rule|
|
56
|
+
# [ prefered rule type ASC, total_digits ASC ]
|
57
|
+
[ (rule[:type]==prefered_type) ? -1 : rule[:type], rule[:total_digits] ]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# finally...find our matching rule
|
62
|
+
matching_rule = find_matching_rule(phone_number, rule_sets)
|
63
|
+
|
64
|
+
# now that know how to format the number, do the formatting work...
|
65
|
+
if(matching_rule)
|
66
|
+
area_code = phone_number[matching_rule[:areacode_offset], matching_rule[:areacode_length]]
|
67
|
+
number = phone_number[matching_rule[:areacode_offset]+matching_rule[:areacode_length]..-1]
|
68
|
+
phone_number = format(phone_number, matching_rule[:format].to_s)
|
69
|
+
|
70
|
+
# replace 'n' with our national_prefix if it exists
|
71
|
+
if(phone_number[/n/])
|
72
|
+
phone_number.gsub!(/n{1}/, national_prefix)
|
73
|
+
|
74
|
+
# reset the national_prefix so we don't add it twice
|
75
|
+
national_prefix = ''
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# strip possible whitespace from the left
|
80
|
+
phone_number.lstrip!
|
81
|
+
|
82
|
+
if(matching_rule && phone_number[/c/])
|
83
|
+
# format the country code
|
84
|
+
if(dialout_prefix == '+')
|
85
|
+
phone_number.gsub!(/c{1}/, "+#{dialout_country}")
|
86
|
+
else
|
87
|
+
phone_number.gsub!(/c{1}/, dialout_country)
|
88
|
+
phone_number = "#{dialout_prefix} #{phone_number}" unless dialout_prefix.empty?
|
89
|
+
end
|
90
|
+
else
|
91
|
+
# default formatting
|
92
|
+
if(dialout_prefix == '+')
|
93
|
+
if(dialout_country.empty?)
|
94
|
+
phone_number = "+#{phone_number}"
|
95
|
+
else
|
96
|
+
phone_number = "+#{dialout_country} #{phone_number}"
|
97
|
+
end
|
98
|
+
else
|
99
|
+
phone_number = "#{dialout_country} #{phone_number}" unless dialout_country.empty?
|
100
|
+
phone_number = "#{dialout_prefix} #{phone_number}" unless dialout_prefix.empty?
|
101
|
+
phone_number = national_prefix + phone_number unless national_prefix.empty?
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# strip possible whitespace
|
106
|
+
phone_number.rstrip!
|
107
|
+
phone_number.lstrip!
|
108
|
+
|
109
|
+
# Finally...we can output our parts as a hash
|
110
|
+
{ :formatted_number => phone_number, :area_code => area_code, :country_code => country_code, :number => normalize(number) }
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
def get_rule_sets_for_region(string, region)
|
115
|
+
rule_sets = []
|
116
|
+
|
117
|
+
if(region && region.rule_sets)
|
118
|
+
rule_sets = region.rule_sets.select do |rule_set|
|
119
|
+
rule_set[:digits] <= string.length
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
rule_sets
|
124
|
+
end
|
125
|
+
|
126
|
+
def find_matching_rule(string, rule_sets)
|
127
|
+
match = nil
|
128
|
+
|
129
|
+
# go through all our given rules
|
130
|
+
for rule_set in rule_sets do
|
131
|
+
digits = rule_set[:digits]
|
132
|
+
prefix = string[0,digits].to_i
|
133
|
+
rules = rule_set[:rules].select { |rule| rule[:total_digits] >= string.length }
|
134
|
+
|
135
|
+
rules.each do |rule|
|
136
|
+
if(prefix >= rule[:min] && prefix <= rule[:max])
|
137
|
+
match = rule
|
138
|
+
break
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
break if match
|
143
|
+
end
|
144
|
+
|
145
|
+
match
|
146
|
+
end
|
147
|
+
|
148
|
+
def dialing_out?(string, region=nil)
|
149
|
+
region ||= PhoneNumber.default_region
|
150
|
+
!get_dialout_prefix(string, region).empty?
|
151
|
+
end
|
152
|
+
|
153
|
+
def get_dialout_prefix(string, region=nil)
|
154
|
+
region ||= PhoneNumber.default_region
|
155
|
+
prefixes = region.dialout_prefixes
|
156
|
+
dialout_prefix = ''
|
157
|
+
|
158
|
+
# check if we're dialing outside our region
|
159
|
+
if string[0].chr == '+'
|
160
|
+
dialout_prefix = '+'
|
161
|
+
end
|
162
|
+
|
163
|
+
for prefix in prefixes do
|
164
|
+
regexp = Regexp.escape(prefix)
|
165
|
+
match_str = string
|
166
|
+
|
167
|
+
# we have matching wild cards
|
168
|
+
if(prefix[/X/] && string =~ Regexp.new("^#{Regexp.escape(prefix.delete('X '))}"))
|
169
|
+
regexp.gsub!(/X/, "[0-9]{0,1}")
|
170
|
+
match_str = format(string[prefix.scan(/[0-9]/).size, prefix.count('X')], prefix)
|
171
|
+
prefix = match_str
|
172
|
+
end
|
173
|
+
|
174
|
+
if(match_str =~ Regexp.new("^#{regexp}"))
|
175
|
+
dialout_prefix = prefix
|
176
|
+
break
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
dialout_prefix
|
181
|
+
end
|
182
|
+
|
183
|
+
def get_national_prefix(string, region=nil)
|
184
|
+
region ||= PhoneNumber.default_region
|
185
|
+
prefix = region.national_prefix
|
186
|
+
national_prefix = ''
|
187
|
+
|
188
|
+
# in case we're not dialing out and the number starts with the national_prefix
|
189
|
+
if(!dialing_out?(string, region) && string =~ Regexp.new("^#{Regexp.escape(prefix)}"))
|
190
|
+
national_prefix = prefix
|
191
|
+
end
|
192
|
+
|
193
|
+
national_prefix
|
194
|
+
end
|
195
|
+
|
196
|
+
def get_dialout_region(string, region)
|
197
|
+
region ||= PhoneNumber.default_region
|
198
|
+
dialout_prefix = get_dialout_prefix(string, region)
|
199
|
+
dialout_region = nil
|
200
|
+
|
201
|
+
unless dialout_prefix.empty?
|
202
|
+
# region codes are 1 to 3 digits
|
203
|
+
range_end = [string.length-dialout_prefix.delete(' ').length, 3].min
|
204
|
+
|
205
|
+
(1..range_end).each do |i|
|
206
|
+
dialout_region = Region.find(string[dialout_prefix.delete(' ').length, i])
|
207
|
+
break if dialout_region
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
dialout_region
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class PhoneNumber
|
4
|
+
|
5
|
+
class Region
|
6
|
+
@@regions = []
|
7
|
+
|
8
|
+
attr_reader :country_code, :country_abbr
|
9
|
+
attr_reader :national_prefix, :dialout_prefixes
|
10
|
+
attr_reader :rule_sets
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def load
|
14
|
+
data_file = File.join(File.dirname(__FILE__), '..', 'data', 'regions.yml')
|
15
|
+
|
16
|
+
@@regions = []
|
17
|
+
YAML.load(File.read(data_file)).each_pair do |key, region_hash|
|
18
|
+
new_region = Region.new(region_hash)
|
19
|
+
@@regions.push(new_region)
|
20
|
+
end
|
21
|
+
@@regions
|
22
|
+
end
|
23
|
+
|
24
|
+
def all
|
25
|
+
return @@regions unless @@regions.empty?
|
26
|
+
|
27
|
+
load
|
28
|
+
end
|
29
|
+
|
30
|
+
def find(param)
|
31
|
+
return nil unless param
|
32
|
+
|
33
|
+
param = param.to_sym
|
34
|
+
|
35
|
+
all.detect do |region|
|
36
|
+
region.country_code == param || region.country_abbr == param
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def [](param)
|
41
|
+
find(param)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(hash)
|
46
|
+
@country_abbr = hash[:country_abbr]
|
47
|
+
@country_code = hash[:country_code]
|
48
|
+
|
49
|
+
@national_prefix = hash[:national_prefix]
|
50
|
+
@dialout_prefixes = hash[:dialout_prefixes]
|
51
|
+
|
52
|
+
@rule_sets = hash[:rule_sets]
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
"#{@country_abbr.to_s} [+#{@country_code.to_s}]"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/lib/phoney/utils.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
class PhoneNumber
|
2
|
+
|
3
|
+
module Utils
|
4
|
+
|
5
|
+
# Returns the string formatted according to a pattern.
|
6
|
+
#
|
7
|
+
# Examples:
|
8
|
+
# format('123456789', 'XXX-XX-XXXX')
|
9
|
+
# => "123-45-6789"
|
10
|
+
# format('12345', 'XXX-XX-XXXX')
|
11
|
+
# => "123-45"
|
12
|
+
#
|
13
|
+
# Parameters:
|
14
|
+
# string -- The string to be formatted.
|
15
|
+
# pattern -- The format string, see above examples.
|
16
|
+
# fixchar -- The single-character placeholder. Default is 'X'.
|
17
|
+
def format(string, pattern, fixchar='X')
|
18
|
+
raise ArgumentError.new("First parameter 'string' must be a String") unless string.is_a?(String)
|
19
|
+
raise ArgumentError.new("#{fixchar} too long") if fixchar.length > 1
|
20
|
+
|
21
|
+
slots = pattern.count(fixchar)
|
22
|
+
|
23
|
+
# Return the string if it doesn't fit and we shouldn't even try,
|
24
|
+
return string if string.length > slots
|
25
|
+
|
26
|
+
# Make the result.
|
27
|
+
scanner = ::StringScanner.new(pattern)
|
28
|
+
regexp = Regexp.new(Regexp.escape(fixchar))
|
29
|
+
index = 0
|
30
|
+
result = ''
|
31
|
+
|
32
|
+
while(!scanner.eos? && index < string.length)
|
33
|
+
if scanner.scan(regexp) then
|
34
|
+
result += string[index].chr
|
35
|
+
index += 1
|
36
|
+
else
|
37
|
+
result += scanner.getch
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
# Strips all non-numberpad characters from a string
|
45
|
+
# => For example: "+45 (123) 023 1.1.1" -> "+45123023111"
|
46
|
+
def normalize(string_with_number)
|
47
|
+
string_with_number.scan(/[0-9+*#]/).to_s unless string_with_number.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|