superp-phone 1.1

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