phonelib 0.2.9 → 0.3.0

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