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/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