phonelib 0.2.9 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/data/phone_data.dat CHANGED
Binary file
data/lib/phonelib.rb CHANGED
@@ -10,6 +10,4 @@ module Phonelib
10
10
  }
11
11
  end
12
12
 
13
- if defined?(ActiveModel)
14
- autoload :PhoneValidator, 'validators/phone_validator'
15
- end
13
+ autoload :PhoneValidator, 'validators/phone_validator' if defined? ActiveModel
data/lib/phonelib/core.rb CHANGED
@@ -4,6 +4,11 @@ module Phonelib
4
4
  # variable will include hash with data for validation
5
5
  @@phone_data = nil
6
6
 
7
+ # getter for phone data for other modules of gem, can be used outside
8
+ def phone_data
9
+ @@phone_data
10
+ end
11
+
7
12
  # default country for parsing variable setting
8
13
  @@default_country = nil
9
14
 
@@ -98,23 +103,9 @@ module Phonelib
98
103
 
99
104
  # method for parsing phone number.
100
105
  # On first run fills @@phone_data with data present in yaml file
101
- def parse(original, passed_country = nil)
102
- load_data
103
- sanitized = sanitize_phone original
104
-
105
- country = country_or_default_country(passed_country)
106
- if sanitized.empty?
107
- # has to return instance of Phonelib::Phone even if no phone passed
108
- Phonelib::Phone.new(sanitized, original, @@phone_data)
109
- else
110
- detected = detect_or_parse_by_country(sanitized, original, country)
111
- if passed_country.nil? && @@default_country && detected.invalid?
112
- # try to detect country for number if it's invalid for specified one
113
- detect_or_parse_by_country(sanitized, original)
114
- else
115
- detected
116
- end
117
- end
106
+ def parse(phone, passed_country = nil)
107
+ @@phone_data ||= load_data
108
+ Phonelib::Phone.new phone, passed_country
118
109
  end
119
110
 
120
111
  # method checks if passed phone number is valid
@@ -152,52 +143,7 @@ module Phonelib
152
143
  # Load data file into memory
153
144
  def load_data
154
145
  data_file = File.dirname(__FILE__) + '/../../data/phone_data.dat'
155
- @@phone_data ||= Marshal.load(File.read(data_file))
156
- end
157
-
158
- # Get country that was provided or default country in needable format
159
- def country_or_default_country(country)
160
- country = country || @@default_country
161
- country.to_s.upcase unless country.nil?
162
- end
163
-
164
- # Get Phone instance for provided phone with country specified
165
- def detect_or_parse_by_country(phone, original, country = nil)
166
- if country.nil?
167
- Phonelib::Phone.new(phone, original, @@phone_data)
168
- else
169
- detected = @@phone_data.find { |data| data[:id] == country }
170
- if detected
171
- phone = convert_phone_to_e164(phone, detected)
172
- if phone[0] == '+'
173
- detect_or_parse_by_country(phone[1..-1], original)
174
- else
175
- Phonelib::Phone.new(phone, original, [detected])
176
- end
177
- end
178
- end
179
- end
180
-
181
- # Create phone representation in e164 format
182
- def convert_phone_to_e164(phone, data) #prefix, national_prefix)
183
- rx = []
184
- rx << "(#{data[Core::INTERNATIONAL_PREFIX]})?"
185
- rx << "(#{data[Core::COUNTRY_CODE]})?"
186
- rx << "(#{data[Core::NATIONAL_PREFIX]})?"
187
- rx << "(#{data[Core::TYPES][Core::GENERAL][Core::VALID_PATTERN]})"
188
-
189
- match = phone.match(/^#{rx.join}$/)
190
- if match
191
- national_start = (1..3).map { |i| match[i].to_s.length }.inject(:+)
192
- "#{data[Core::COUNTRY_CODE]}#{phone[national_start..-1]}"
193
- else
194
- phone.sub(/^#{data[Core::INTERNATIONAL_PREFIX]}/, '+')
195
- end
196
- end
197
-
198
- # Sanitizes passed phone number. Returns only digits from passed string.
199
- def sanitize_phone(phone)
200
- phone && phone.gsub(/[^0-9]+/, '') || ''
146
+ Marshal.load(File.read(data_file))
201
147
  end
202
148
  end
203
149
  end
@@ -2,37 +2,39 @@ module Phonelib
2
2
  # class for parsed phone number, includes validation and formatting methods
3
3
  class Phone
4
4
  # defining reader methods for class variables
5
- attr_reader :original, # original phone number passed for parsing
6
- :sanitized # sanitized phone number representation
5
+ attr_reader :original # original phone number passed for parsing
7
6
 
8
- # including module that has all phone analyze logic
7
+ # including module that has all phone analyzing methods
9
8
  include Phonelib::PhoneAnalyzer
10
9
 
11
10
  # class initialization method
12
11
  #
13
12
  # ==== Attributes
14
13
  #
15
- # * +phone+ - Phone number for parsing
16
- # * +country_data+ - Hash of data for parsing
14
+ # * +phone+ - Phone number for parsing
15
+ # * +country+ - Country specification for parsing. Must be ISO code of
16
+ # country (2 letters) like 'US', 'us' or :us for United States
17
17
  #
18
- def initialize(sanitized, original, country_data)
19
- @sanitized = sanitized
18
+ def initialize(original, country = nil)
20
19
  @original = original
21
- if @sanitized.empty?
22
- @analyzed_data = {}
20
+
21
+ if sanitized.empty?
22
+ @data = {}
23
23
  else
24
- @analyzed_data = analyze(@sanitized, country_data)
25
- if country
26
- @national_number,= @analyzed_data[country][:national]
27
- else
28
- @national_number = @sanitized
29
- end
24
+ @data = analyze(sanitized, country)
25
+ first = @data.values.first
26
+ @national_number = first ? first[:national] : sanitized
30
27
  end
31
28
  end
32
29
 
30
+ # method to get sanitized phone number (only numbers)
31
+ def sanitized
32
+ @original && @original.gsub(/[^0-9]+/, '') || ''
33
+ end
34
+
33
35
  # Returns all phone types that matched valid patterns
34
36
  def types
35
- @analyzed_data.flat_map { |iso2, data| data[:valid] }.uniq
37
+ @data.flat_map { |iso2, data| data[:valid] }.uniq
36
38
  end
37
39
 
38
40
  # Returns first phone type that matched
@@ -52,13 +54,13 @@ module Phonelib
52
54
 
53
55
  # Returns all countries that matched valid patterns
54
56
  def countries
55
- @analyzed_data.map { |iso2, data| iso2 }
57
+ @data.map { |iso2, data| iso2 }
56
58
  end
57
59
 
58
60
  # Return countries with valid patterns
59
61
  def valid_countries
60
62
  @valid_countries ||= countries.select do |iso2|
61
- @analyzed_data[iso2][:valid].any?
63
+ @data[iso2][:valid].any?
62
64
  end
63
65
  end
64
66
 
@@ -66,14 +68,14 @@ module Phonelib
66
68
  def country
67
69
  @country ||= begin
68
70
  valid_countries.find do |iso2|
69
- @analyzed_data[iso2][Core::MAIN_COUNTRY_FOR_CODE] == 'true'
71
+ @data[iso2][Core::MAIN_COUNTRY_FOR_CODE] == 'true'
70
72
  end || valid_countries.first || countries.first
71
73
  end
72
74
  end
73
75
 
74
76
  # Returns whether a current parsed phone number is valid
75
77
  def valid?
76
- @analyzed_data.select { |iso2, data| data[:valid].any? }.any?
78
+ @data.select { |iso2, data| data[:valid].any? }.any?
77
79
  end
78
80
 
79
81
  # Returns whether a current parsed phone number is invalid
@@ -83,7 +85,7 @@ module Phonelib
83
85
 
84
86
  # Returns whether a current parsed phone number is possible
85
87
  def possible?
86
- @analyzed_data.select { |iso2, data| data[:possible].any? }.any?
88
+ @data.select { |iso2, data| data[:possible].any? }.any?
87
89
  end
88
90
 
89
91
  # Returns whether a current parsed phone number is impossible
@@ -110,16 +112,16 @@ module Phonelib
110
112
 
111
113
  # Returns e164 formatted phone number
112
114
  def international
113
- return "+#{@sanitized}" unless valid?
115
+ return "+#{sanitized}" unless valid?
114
116
 
115
- format = @analyzed_data[country][:format]
117
+ format = @data[country][:format]
116
118
  if matches = @national_number.match(/#{format[Core::PATTERN]}/)
117
119
  national = format[:format].gsub(/\$\d/) { |el| matches[el[1].to_i] }
118
120
  else
119
121
  national = @national_number
120
122
  end
121
123
 
122
- "+#{@analyzed_data[country][Core::COUNTRY_CODE]} #{national}"
124
+ "+#{@data[country][Core::COUNTRY_CODE]} #{national}"
123
125
  end
124
126
 
125
127
  # Returns whether a current parsed phone number is valid for specified
@@ -128,11 +130,11 @@ module Phonelib
128
130
  # ==== Attributes
129
131
  #
130
132
  # * +country+ - ISO code of country (2 letters) like 'US', 'us' or :us
131
- # for United States
133
+ # for United States
132
134
  #
133
135
  def valid_for_country?(country)
134
136
  country = country.to_s.upcase
135
- @analyzed_data.select do |iso2, data|
137
+ @data.select do |iso2, data|
136
138
  country == iso2 && data[:valid].any?
137
139
  end.any?
138
140
  end
@@ -143,10 +145,22 @@ module Phonelib
143
145
  # ==== Attributes
144
146
  #
145
147
  # * +country+ - ISO code of country (2 letters) like 'US', 'us' or :us
146
- # for United States
148
+ # for United States
147
149
  #
148
150
  def invalid_for_country?(country)
149
151
  !valid_for_country?(country)
150
152
  end
153
+
154
+ private
155
+
156
+ # Get needable data for formatting phone as national number
157
+ def get_formatting_data
158
+ format = @data[country][:format]
159
+ prefix = @data[country][Core::NATIONAL_PREFIX]
160
+ rule = (format[Core::NATIONAL_PREFIX_RULE] ||
161
+ @data[country][Core::NATIONAL_PREFIX_RULE] || '$1')
162
+
163
+ [format, prefix, rule]
164
+ end
151
165
  end
152
166
  end
@@ -1,81 +1,194 @@
1
1
  module Phonelib
2
+ # phone analyzing methods module
2
3
  module PhoneAnalyzer
3
-
4
4
  # array of types not included for validation check in cycle
5
5
  NOT_FOR_CHECK = [:general_desc, :fixed_line, :mobile, :fixed_or_mobile]
6
6
 
7
- # analyze provided phone if it matches country data ang returns result of
7
+ # parses provided phone if it is valid for country data and returns result of
8
8
  # analyze
9
- def analyze(phone, country_data)
10
- all_data = {}
11
- country_data.each do |data|
12
- if country_match = phone_match_data?(phone, data)
9
+ #
10
+ # ==== Attributes
11
+ #
12
+ # * +phone+ - Phone number for parsing
13
+ # * +passed_country+ - Country provided for parsing. Must be ISO code of
14
+ # country (2 letters) like 'US', 'us' or :us for United States
15
+ def analyze(phone, passed_country)
16
+ country = country_or_default_country passed_country
13
17
 
14
- all_data.merge! get_national_and_data(phone, data, country_match)
15
- end
18
+ result = try_to_parse_single_country(phone, country)
19
+ # if previous parsing failed, trying for all countries
20
+ if result.nil? || result.values.first[:valid].empty?
21
+ result = detect_and_parse phone
16
22
  end
17
- all_data
23
+ result
18
24
  end
19
25
 
20
26
  private
21
27
 
22
- # returns national number for provided phone and analyzing results for
23
- # provided phone number
28
+ # trying to parse phone for single country including international prefix
29
+ # check for provided country
30
+ #
31
+ # ==== Attributes
32
+ #
33
+ # * +phone+ - phone for parsing
34
+ # * +country+ - country to parse phone with
35
+ def try_to_parse_single_country(phone, country)
36
+ if country && Phonelib.phone_data[country]
37
+ # if country was provided and it's a valid country, trying to
38
+ # create e164 representation of phone number,
39
+ # kind of normalization for parsing
40
+ e164 = convert_to_e164 phone, Phonelib.phone_data[country]
41
+ # if phone starts with international prefix of provided
42
+ # country try to reanalyze without international prefix for
43
+ # all countries
44
+ return analyze(e164.gsub('+', ''), nil) if e164[0] == '+'
45
+ # trying to parse number for provided country
46
+ parse_single_country e164, Phonelib.phone_data[country]
47
+ end
48
+ end
49
+
50
+ # method checks if phone is valid against single provided country data
51
+ #
52
+ # ==== Attributes
53
+ #
54
+ # * +e164+ - e164 representation of phone for parsing
55
+ # * +data+ - country data for single country for parsing
56
+ def parse_single_country(e164, data)
57
+ country_match = phone_match_data?(e164, data)
58
+ country_match && get_national_and_data(e164, data, country_match)
59
+ end
60
+
61
+ # method tries to detect what is the country for provided phone
62
+ #
63
+ # ==== Attributes
64
+ #
65
+ # * +phone+ - phone number for parsing
66
+ def detect_and_parse(phone)
67
+ result = {}
68
+ Phonelib.phone_data.each do |key, data|
69
+ parsed = parse_single_country(phone, data)
70
+ result.merge!(parsed) unless parsed.nil?
71
+ end
72
+ result
73
+ end
74
+
75
+ # Get country that was provided or default country in needable format
76
+ #
77
+ # ==== Attributes
78
+ #
79
+ # * +country+ - country passed for parsing
80
+ def country_or_default_country(country)
81
+ country = country || Phonelib.default_country
82
+ country && country.to_s.upcase
83
+ end
84
+
85
+ # Create phone representation in e164 format
86
+ #
87
+ # ==== Attributes
88
+ #
89
+ # * +phone+ - phone number for parsing
90
+ # * +data+ - country data to be based on for creating e164 representation
91
+ def convert_to_e164(phone, data)
92
+ match = phone.match full_valid_regex_for_data(data)
93
+ if match
94
+ national_start = (1..3).map { |i| match[i].to_s.length }.inject(:+)
95
+ "#{data[Core::COUNTRY_CODE]}#{phone[national_start..-1]}"
96
+ else
97
+ phone.sub(/^#{data[Core::INTERNATIONAL_PREFIX]}/, '+')
98
+ end
99
+ end
100
+
101
+ # constructs full regex for phone validation for provided phone data
102
+ # (international prefix, country code, national prefix, valid number)
103
+ #
104
+ # ==== Attributes
105
+ #
106
+ # * +data+ - country data hash
107
+ # * +country_optional+ - whether to put country code as optional group
108
+ def full_valid_regex_for_data(data, country_optional = true)
109
+ regex = []
110
+ regex << "(#{data[Core::INTERNATIONAL_PREFIX]})?"
111
+ regex << if country_optional
112
+ "(#{data[Core::COUNTRY_CODE]})?"
113
+ else
114
+ data[Core::COUNTRY_CODE]
115
+ end
116
+ regex << "(#{data[Core::NATIONAL_PREFIX]})?"
117
+ regex << "(#{data[Core::TYPES][Core::GENERAL][Core::VALID_PATTERN]})"
118
+
119
+ /^#{regex.join}$/
120
+ end
121
+
122
+ # returns national number and analyzing results for provided phone number
123
+ #
124
+ # ==== Attributes
125
+ #
126
+ # * +phone+ - phone number for parsing
127
+ # * +data+ - country data
128
+ # * +country_match+ - result of match of phone within full regex
24
129
  def get_national_and_data(phone, data, country_match)
25
130
  prefix_length = data[Core::COUNTRY_CODE].length
26
- prefix_length += [1, 2].map {|i| country_match[i].to_s.length}.inject(:+)
131
+ prefix_length += [1, 2].map { |i| country_match[i].to_s.size }.inject(:+)
27
132
  result = data.select { |k, v| ![:types, :formats].include?(k) }
28
133
  result[:national] = phone[prefix_length..-1]
29
- result[:format] = get_number_format(result[:national], data[Core::FORMATS])
134
+ result[:format] = get_number_format(result[:national],
135
+ data[Core::FORMATS])
30
136
  result.merge! all_number_types(result[:national], data[Core::TYPES])
31
137
  { result[:id] => result }
32
138
  end
33
139
 
34
- # Check if sanitized phone match country data
140
+ # Check if phone match country data
141
+ #
142
+ # ==== Attributes
143
+ #
144
+ # * +phone+ - phone number for parsing
145
+ # * +data+ - country data
35
146
  def phone_match_data?(phone, data)
36
147
  country_code = "#{data[Core::COUNTRY_CODE]}"
37
148
  inter_prefix = "(#{data[Core::INTERNATIONAL_PREFIX]})?"
38
149
  if phone =~ /^#{inter_prefix}#{country_code}/
39
- national_prefix = "(#{data[Core::NATIONAL_PREFIX]})?"
40
- _possible, valid = get_patterns(data[Core::TYPES], Core::GENERAL)
41
- phone.match /^#{inter_prefix}#{country_code}#{national_prefix}#{valid}$/
150
+ phone.match full_valid_regex_for_data(data, false)
42
151
  end
43
152
  end
44
153
 
45
- # Get needable data for formatting phone as national number
46
- def get_formatting_data
47
- format = @analyzed_data[country][:format]
48
- prefix = @analyzed_data[country][Core::NATIONAL_PREFIX]
49
- rule = (format[Core::NATIONAL_PREFIX_RULE] ||
50
- @analyzed_data[country][Core::NATIONAL_PREFIX_RULE] || '$1')
51
-
52
- [format, prefix, rule]
53
- end
54
-
55
154
  # Returns all valid and possible phone number types for currently parsed
56
155
  # phone for provided data hash.
57
- def all_number_types(number, data)
156
+ #
157
+ # ==== Attributes
158
+ #
159
+ # * +phone+ - phone number for parsing
160
+ # * +data+ - country data
161
+ def all_number_types(phone, data)
58
162
  response = { valid: [], possible: [] }
59
163
 
60
164
  types_for_check(data).each do |type|
61
165
  possible, valid = get_patterns(data, type)
62
166
 
63
- response[:possible] << type if number_possible?(number, possible)
64
- response[:valid] << type if number_valid_and_possible?(number,
65
- possible,
66
- valid)
167
+ valid_and_possible, possible_result =
168
+ number_valid_and_possible?(phone, possible, valid)
169
+ response[:possible] << type if possible_result
170
+ response[:valid] << type if valid_and_possible
67
171
  end
68
172
 
69
173
  response
70
174
  end
71
175
 
72
176
  # returns array of phone types for check for current country data
177
+ #
178
+ # ==== Attributes
179
+ #
180
+ # * +data+ - country data hash
73
181
  def types_for_check(data)
74
182
  Core::TYPES_DESC.keys - PhoneAnalyzer::NOT_FOR_CHECK +
75
183
  fixed_and_mobile_keys(data)
76
184
  end
77
185
 
78
186
  # Gets matched number formatting rule or default one
187
+ #
188
+ # ==== Attributes
189
+ #
190
+ # * +national+ - national phone number
191
+ # * +format_data+ - formatting data from country data
79
192
  def get_number_format(national, format_data)
80
193
  format_data && format_data.find do |format|
81
194
  (format[Core::LEADING_DIGITS].nil? \
@@ -86,6 +199,10 @@ module Phonelib
86
199
 
87
200
  # Checks if fixed line pattern and mobile pattern are the same and returns
88
201
  # appropriate keys
202
+ #
203
+ # ==== Attributes
204
+ #
205
+ # * +data+ - country data
89
206
  def fixed_and_mobile_keys(data)
90
207
  if data[Core::FIXED_LINE] == data[Core::MOBILE]
91
208
  [Core::FIXED_OR_MOBILE]
@@ -94,13 +211,18 @@ module Phonelib
94
211
  end
95
212
  end
96
213
 
97
- # Returns array of two elements. Valid phone pattern and possible pattern
98
- def get_patterns(all_types, type)
214
+ # Returns possible and valid patterns for validation for provided type
215
+ #
216
+ # ==== Attributes
217
+ #
218
+ # * +all_patterns+ - hash of all patterns for validation
219
+ # * +type+ - type of phone to get patterns for
220
+ def get_patterns(all_patterns, type)
99
221
  patterns = case type
100
222
  when Core::FIXED_OR_MOBILE
101
- all_types[Core::FIXED_LINE]
223
+ all_patterns[Core::FIXED_LINE]
102
224
  else
103
- all_types[type]
225
+ all_patterns[type]
104
226
  end
105
227
  return [nil, nil] if patterns.nil?
106
228
  national_pattern = patterns[Core::VALID_PATTERN]
@@ -109,20 +231,26 @@ module Phonelib
109
231
  [possible_pattern, national_pattern]
110
232
  end
111
233
 
112
- # Checks if passed number matches both valid and possible patterns
234
+ # Checks if passed number matches valid and possible patterns
235
+ #
236
+ # ==== Attributes
237
+ #
238
+ # * +number+ - phone number for validation
239
+ # * +possible_pattern+ - possible pattern for validation
240
+ # * +national_pattern+ - valid pattern for validation
113
241
  def number_valid_and_possible?(number, possible_pattern, national_pattern)
114
- national_match = number.match(/^(?:#{national_pattern})$/)
115
242
  possible_match = number.match(/^(?:#{possible_pattern})$/)
243
+ possible = possible_match && possible_match.to_s.length == number.length
116
244
 
117
- national_match && possible_match &&
118
- national_match.to_s.length == number.length &&
119
- possible_match.to_s.length == number.length
120
- end
245
+ if possible
246
+ # doing national pattern match only in case possible matches
247
+ national_match = number.match(/^(?:#{national_pattern})$/)
248
+ valid = national_match && national_match.to_s.length == number.length
249
+ else
250
+ valid = false
251
+ end
121
252
 
122
- # Checks if passed number matches possible pattern
123
- def number_possible?(number, possible_pattern)
124
- possible_match = number.match(/^(?:#{possible_pattern})$/)
125
- possible_match && possible_match.to_s.length == number.length
253
+ [valid && possible, possible]
126
254
  end
127
255
  end
128
- end
256
+ end