phoney 0.0.3
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 +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
|