phoney 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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')
@@ -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
@@ -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
@@ -0,0 +1,11 @@
1
+ class PhoneNumber
2
+
3
+ module VERSION #:nodoc:
4
+ MAJOR = 0
5
+ MINOR = 0
6
+ TINY = 3
7
+
8
+ STRING = [MAJOR, MINOR, TINY].join('.')
9
+ end
10
+
11
+ end