superp-phone 1.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/country.rb ADDED
@@ -0,0 +1,120 @@
1
+ module Phoner
2
+ class Country < Struct.new(:name, :country_code, :char_2_code, :char_3_code, :area_code, :local_number_format, :mobile_format, :full_number_length, :number_format)
3
+ cattr_accessor :all
4
+
5
+ def self.load
6
+ return @@all if @@all.present?
7
+
8
+ data_file = File.join(File.dirname(__FILE__), '..', 'data', 'phone_countries.yml')
9
+
10
+ @@all = []
11
+ YAML.load(File.read(data_file)).each_pair do |key, c|
12
+ next unless c[:area_code] && c[:local_number_format]
13
+ @@all << Country.new(c[:name], c[:country_code], c[:char_2_code], c[:char_3_code], c[:area_code], c[:local_number_format], c[:mobile_format], c[:full_number_length], c[:number_format])
14
+ end
15
+ @@all
16
+ end
17
+
18
+ def to_s
19
+ name
20
+ end
21
+
22
+ def self.find_all_by_phone_code(code)
23
+ return [] if code.nil?
24
+ @@all.select {|c| c.country_code == code }
25
+ end
26
+
27
+ def self.find_by_country_code(code)
28
+ return nil if code.nil?
29
+ @@all.each {|c| return c if c.char_3_code.downcase == code.downcase }
30
+ nil
31
+ end
32
+
33
+ def self.find_by_name(name)
34
+ return nil if name.nil?
35
+ @@all.each {|c| return c if c.name.downcase == name.downcase }
36
+ nil
37
+ end
38
+
39
+ # detect country from the string entered
40
+ def self.detect(string, default_country_code, default_area_code)
41
+ Country.find_all_by_phone_code(default_country_code).each do |country|
42
+ return country if country.matches_local_number?(string, default_area_code)
43
+ end
44
+
45
+ # find if the number has a country code
46
+ Country.all.each do |country|
47
+ return country if country.matches_full_number?(string)
48
+ end
49
+ return nil
50
+ end
51
+
52
+ def is_mobile?(number)
53
+ return true if mobile_format.nil?
54
+ number =~ mobile_number_regex ? true : false
55
+ end
56
+
57
+ def matches_local_number?(string, default_area_code)
58
+ ((string =~ full_number_regexp ||
59
+ string =~ area_code_number_regexp) && string =~ number_format_regex) ||
60
+ ((string =~ number_regex) && (default_area_code =~ area_code_regex))
61
+ end
62
+
63
+ def matches_full_number?(string)
64
+ string =~ full_number_regexp && string =~ number_format_regex
65
+ end
66
+
67
+ def number_parts(number, default_area_code)
68
+ number_part = if default_area_code
69
+ number.match(number_regex)
70
+ $1
71
+ else
72
+ nil
73
+ end
74
+
75
+ if number_part.nil?
76
+ matches = number.match(area_code_number_regexp)
77
+ area_part = $1
78
+ number_part = matches.to_a.last
79
+ end
80
+
81
+ if number_part.nil?
82
+ matches = number.match(full_number_regexp)
83
+ country_part, area_part = $1, $2
84
+ number_part = matches.to_a.last
85
+ end
86
+
87
+ area_part ||= default_area_code
88
+
89
+ raise "Could not determine area code" if area_part.nil?
90
+ raise "Could not determine number" if number_part.nil?
91
+
92
+ {:number => number_part, :area_code => area_part, :country_code => country_code, :country => self}
93
+ end
94
+
95
+ private
96
+ def number_format_regex
97
+ Regexp.new("^[+0]?(#{country_code})?(#{number_format})$")
98
+ end
99
+
100
+ def full_number_regexp
101
+ Regexp.new("^[+]?(#{country_code})(#{area_code})(#{local_number_format})$")
102
+ end
103
+
104
+ def area_code_number_regexp
105
+ Regexp.new("^0?(#{area_code})(#{local_number_format})$")
106
+ end
107
+
108
+ def area_code_regex
109
+ Regexp.new("^0?(#{area_code})$")
110
+ end
111
+
112
+ def mobile_number_regex
113
+ Regexp.new("^0?(#{mobile_format})$")
114
+ end
115
+
116
+ def number_regex
117
+ Regexp.new("^(#{local_number_format})$")
118
+ end
119
+ end
120
+ end
data/lib/phone.rb ADDED
@@ -0,0 +1,218 @@
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
+ require File.join(File.dirname(__FILE__), 'support') unless defined? ActiveSupport
13
+ require File.join(File.dirname(__FILE__), 'country')
14
+
15
+ module Phoner
16
+ class Phone
17
+ attr_accessor :country_code, :area_code, :number, :extension, :country
18
+
19
+ cattr_accessor :default_country_code
20
+ cattr_accessor :default_area_code
21
+ cattr_accessor :named_formats
22
+
23
+ # length of first number part (using multi number format)
24
+ cattr_accessor :n1_length
25
+ # default length of first number part
26
+ @@n1_length = 3
27
+
28
+ @@named_formats = {
29
+ :default => "+%c%a%n",
30
+ :default_with_extension => "+%c%a%nx%x",
31
+ :europe => '+%c (0) %a %f %l',
32
+ :us => "(%a) %f-%l"
33
+ }
34
+
35
+ def initialize(*hash_or_args)
36
+ if hash_or_args.first.is_a?(Hash)
37
+ hash_or_args = hash_or_args.first
38
+ keys = {:country => :country, :number => :number, :area_code => :area_code, :country_code => :country_code, :extension => :extension}
39
+ else
40
+ keys = {:number => 0, :area_code => 1, :country_code => 2, :extension => 3, :country => 4}
41
+ end
42
+
43
+ self.number = hash_or_args[ keys[:number] ]
44
+ self.area_code = hash_or_args[ keys[:area_code] ] || self.default_area_code
45
+ self.country_code = hash_or_args[ keys[:country_code] ] || self.default_country_code
46
+ self.extension = hash_or_args[ keys[:extension] ]
47
+ self.country = hash_or_args[ keys[:country] ]
48
+
49
+ # Santity checks
50
+ raise "Must enter number" if self.number.blank?
51
+ raise "Must enter area code or set default area code" if self.area_code.blank?
52
+ raise "Must enter country code or set default country code" if self.country_code.blank?
53
+ end
54
+
55
+ def self.parse!(string, options={})
56
+ parse(string, options.merge(:raise_exception_on_error => true))
57
+ end
58
+
59
+ # create a new phone number by parsing a string
60
+ # the format of the string is detect automatically (from FORMATS)
61
+ def self.parse(string, options={})
62
+ return nil unless string.present?
63
+
64
+ Country.load
65
+
66
+ extension = extract_extension(string)
67
+ normalized = normalize(string)
68
+
69
+ options[:country_code] ||= self.default_country_code
70
+ options[:area_code] ||= self.default_area_code
71
+
72
+ parts = split_to_parts(normalized, options)
73
+
74
+ pn = Phone.new(parts) if parts
75
+ if pn.present? and extension.present?
76
+ pn.extension = extension
77
+ end
78
+ pn
79
+ end
80
+
81
+ # is this string a valid phone number?
82
+ def self.valid?(string, options = {})
83
+ begin
84
+ parse(string, options).present?
85
+ rescue
86
+ false # don't raise exceptions on parse errors
87
+ end
88
+ end
89
+
90
+ def self.is_mobile?(string, options = {})
91
+ pn = parse(string, options)
92
+ return false if pn.nil?
93
+ pn.is_mobile?
94
+ end
95
+
96
+ private
97
+ # split string into hash with keys :country_code, :area_code and :number
98
+ def self.split_to_parts(string, options = {})
99
+ country = Country.detect(string, options[:country_code], options[:area_code])
100
+
101
+ if country.nil?
102
+ raise "Could not determine country" if options[:raise_exception_on_error]
103
+ return nil
104
+ end
105
+
106
+ country.number_parts(string, options[:area_code])
107
+ end
108
+
109
+ # fix string so it's easier to parse, remove extra characters etc.
110
+ def self.normalize(string_with_number)
111
+ string_with_number.sub(extension_regex, '').gsub(/\(0\)|[^0-9+]/, '').gsub(/^00/, '+')
112
+ end
113
+
114
+ def self.extension_regex
115
+ /[ ]*(ext|ex|x|xt|#|:)+[^0-9]*\(*([-0-9]{1,})\)*#?$/i
116
+ end
117
+
118
+ # pull off anything that look like an extension
119
+ #
120
+ def self.extract_extension(string)
121
+ return nil if string.nil?
122
+ if string.match extension_regex
123
+ extension = $2
124
+ return extension
125
+ end
126
+ #
127
+ # We already returned any recognizable extension.
128
+ # However, we might still have extra junk to the right
129
+ # of the phone number proper, so just chop it off.
130
+ #
131
+ idx = string.rindex(/[0-9]/)
132
+ return nil if idx.nil?
133
+ return nil if idx == (string.length - 1) # at the end
134
+ string.slice!((idx+1)..-1) # chop it
135
+ return nil
136
+ end
137
+
138
+ public # instance methods
139
+
140
+ def area_code_long
141
+ "0" + area_code if area_code
142
+ end
143
+
144
+ # For many countries it's not apparent from the number
145
+ # Will return false positives rather than false negatives.
146
+ def is_mobile?
147
+ country.is_mobile? "#{area_code}#{number}"
148
+ end
149
+
150
+ # first n characters of :number
151
+ def number1
152
+ number[0...self.class.n1_length]
153
+ end
154
+
155
+ # everything left from number after the first n characters (see number1)
156
+ def number2
157
+ n2_length = number.size - self.class.n1_length
158
+ number[-n2_length, n2_length]
159
+ end
160
+
161
+ # Formats the phone number.
162
+ #
163
+ # if the method argument is a String, it is used as a format string, with the following fields being interpolated:
164
+ #
165
+ # * %c - country_code (385)
166
+ # * %a - area_code (91)
167
+ # * %A - area_code with leading zero (091)
168
+ # * %n - number (5125486)
169
+ # * %f - first @@n1_length characters of number (configured through Phone.n1_length), default is 3 (512)
170
+ # * %l - last characters of number (5486)
171
+ # * %x - entire extension
172
+ #
173
+ # if the method argument is a Symbol, it is used as a lookup key for a format String in Phone.named_formats
174
+ # pn.format(:europe)
175
+ def format(fmt)
176
+ if fmt.is_a?(Symbol)
177
+ raise "The format #{fmt} doesn't exist'" unless named_formats.has_key?(fmt)
178
+ format_number named_formats[fmt]
179
+ else
180
+ format_number(fmt)
181
+ end
182
+ end
183
+
184
+ # the default format is "+%c%a%n"
185
+ def to_s
186
+ format(:default)
187
+ end
188
+
189
+ # does this number belong to the default country code?
190
+ def has_default_country_code?
191
+ country_code == self.class.default_country_code
192
+ end
193
+
194
+ # does this number belong to the default area code?
195
+ def has_default_area_code?
196
+ area_code == self.class.default_area_code
197
+ end
198
+
199
+ # comparison of 2 phone objects
200
+ def ==(other)
201
+ methods = [:country_code, :area_code, :number, :extension]
202
+ methods.all? { |method| other.respond_to?(method) && send(method) == other.send(method) }
203
+ end
204
+
205
+ private
206
+
207
+ def format_number(fmt)
208
+ result = fmt.gsub("%c", country_code || "").
209
+ gsub("%a", area_code || "").
210
+ gsub("%A", area_code_long || "").
211
+ gsub("%n", number || "").
212
+ gsub("%f", number1 || "").
213
+ gsub("%l", number2 || "").
214
+ gsub("%x", extension || "")
215
+ return result
216
+ end
217
+ end
218
+ end
data/lib/support.rb ADDED
@@ -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)
@@ -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 < Phoner::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