lib/phonelib/phone_analyzer.rb in phonelib-0.2.9 vs lib/phonelib/phone_analyzer.rb in phonelib-0.3.0
- old
+ new
@@ -1,128 +1,256 @@
module Phonelib
+ # phone analyzing methods module
module PhoneAnalyzer
-
# array of types not included for validation check in cycle
NOT_FOR_CHECK = [:general_desc, :fixed_line, :mobile, :fixed_or_mobile]
- # analyze provided phone if it matches country data ang returns result of
+ # parses provided phone if it is valid for country data and returns result of
# analyze
- def analyze(phone, country_data)
- all_data = {}
- country_data.each do |data|
- if country_match = phone_match_data?(phone, data)
+ #
+ # ==== Attributes
+ #
+ # * +phone+ - Phone number for parsing
+ # * +passed_country+ - Country provided for parsing. Must be ISO code of
+ # country (2 letters) like 'US', 'us' or :us for United States
+ def analyze(phone, passed_country)
+ country = country_or_default_country passed_country
- all_data.merge! get_national_and_data(phone, data, country_match)
- end
+ result = try_to_parse_single_country(phone, country)
+ # if previous parsing failed, trying for all countries
+ if result.nil? || result.values.first[:valid].empty?
+ result = detect_and_parse phone
end
- all_data
+ result
end
private
- # returns national number for provided phone and analyzing results for
- # provided phone number
+ # trying to parse phone for single country including international prefix
+ # check for provided country
+ #
+ # ==== Attributes
+ #
+ # * +phone+ - phone for parsing
+ # * +country+ - country to parse phone with
+ def try_to_parse_single_country(phone, country)
+ if country && Phonelib.phone_data[country]
+ # if country was provided and it's a valid country, trying to
+ # create e164 representation of phone number,
+ # kind of normalization for parsing
+ e164 = convert_to_e164 phone, Phonelib.phone_data[country]
+ # if phone starts with international prefix of provided
+ # country try to reanalyze without international prefix for
+ # all countries
+ return analyze(e164.gsub('+', ''), nil) if e164[0] == '+'
+ # trying to parse number for provided country
+ parse_single_country e164, Phonelib.phone_data[country]
+ end
+ end
+
+ # method checks if phone is valid against single provided country data
+ #
+ # ==== Attributes
+ #
+ # * +e164+ - e164 representation of phone for parsing
+ # * +data+ - country data for single country for parsing
+ def parse_single_country(e164, data)
+ country_match = phone_match_data?(e164, data)
+ country_match && get_national_and_data(e164, data, country_match)
+ end
+
+ # method tries to detect what is the country for provided phone
+ #
+ # ==== Attributes
+ #
+ # * +phone+ - phone number for parsing
+ def detect_and_parse(phone)
+ result = {}
+ Phonelib.phone_data.each do |key, data|
+ parsed = parse_single_country(phone, data)
+ result.merge!(parsed) unless parsed.nil?
+ end
+ result
+ end
+
+ # Get country that was provided or default country in needable format
+ #
+ # ==== Attributes
+ #
+ # * +country+ - country passed for parsing
+ def country_or_default_country(country)
+ country = country || Phonelib.default_country
+ country && country.to_s.upcase
+ end
+
+ # Create phone representation in e164 format
+ #
+ # ==== Attributes
+ #
+ # * +phone+ - phone number for parsing
+ # * +data+ - country data to be based on for creating e164 representation
+ def convert_to_e164(phone, data)
+ match = phone.match full_valid_regex_for_data(data)
+ if match
+ national_start = (1..3).map { |i| match[i].to_s.length }.inject(:+)
+ "#{data[Core::COUNTRY_CODE]}#{phone[national_start..-1]}"
+ else
+ phone.sub(/^#{data[Core::INTERNATIONAL_PREFIX]}/, '+')
+ end
+ end
+
+ # constructs full regex for phone validation for provided phone data
+ # (international prefix, country code, national prefix, valid number)
+ #
+ # ==== Attributes
+ #
+ # * +data+ - country data hash
+ # * +country_optional+ - whether to put country code as optional group
+ def full_valid_regex_for_data(data, country_optional = true)
+ regex = []
+ regex << "(#{data[Core::INTERNATIONAL_PREFIX]})?"
+ regex << if country_optional
+ "(#{data[Core::COUNTRY_CODE]})?"
+ else
+ data[Core::COUNTRY_CODE]
+ end
+ regex << "(#{data[Core::NATIONAL_PREFIX]})?"
+ regex << "(#{data[Core::TYPES][Core::GENERAL][Core::VALID_PATTERN]})"
+
+ /^#{regex.join}$/
+ end
+
+ # returns national number and analyzing results for provided phone number
+ #
+ # ==== Attributes
+ #
+ # * +phone+ - phone number for parsing
+ # * +data+ - country data
+ # * +country_match+ - result of match of phone within full regex
def get_national_and_data(phone, data, country_match)
prefix_length = data[Core::COUNTRY_CODE].length
- prefix_length += [1, 2].map {|i| country_match[i].to_s.length}.inject(:+)
+ prefix_length += [1, 2].map { |i| country_match[i].to_s.size }.inject(:+)
result = data.select { |k, v| ![:types, :formats].include?(k) }
result[:national] = phone[prefix_length..-1]
- result[:format] = get_number_format(result[:national], data[Core::FORMATS])
+ result[:format] = get_number_format(result[:national],
+ data[Core::FORMATS])
result.merge! all_number_types(result[:national], data[Core::TYPES])
{ result[:id] => result }
end
- # Check if sanitized phone match country data
+ # Check if phone match country data
+ #
+ # ==== Attributes
+ #
+ # * +phone+ - phone number for parsing
+ # * +data+ - country data
def phone_match_data?(phone, data)
country_code = "#{data[Core::COUNTRY_CODE]}"
inter_prefix = "(#{data[Core::INTERNATIONAL_PREFIX]})?"
if phone =~ /^#{inter_prefix}#{country_code}/
- national_prefix = "(#{data[Core::NATIONAL_PREFIX]})?"
- _possible, valid = get_patterns(data[Core::TYPES], Core::GENERAL)
- phone.match /^#{inter_prefix}#{country_code}#{national_prefix}#{valid}$/
+ phone.match full_valid_regex_for_data(data, false)
end
end
- # Get needable data for formatting phone as national number
- def get_formatting_data
- format = @analyzed_data[country][:format]
- prefix = @analyzed_data[country][Core::NATIONAL_PREFIX]
- rule = (format[Core::NATIONAL_PREFIX_RULE] ||
- @analyzed_data[country][Core::NATIONAL_PREFIX_RULE] || '$1')
-
- [format, prefix, rule]
- end
-
# Returns all valid and possible phone number types for currently parsed
# phone for provided data hash.
- def all_number_types(number, data)
+ #
+ # ==== Attributes
+ #
+ # * +phone+ - phone number for parsing
+ # * +data+ - country data
+ def all_number_types(phone, data)
response = { valid: [], possible: [] }
types_for_check(data).each do |type|
possible, valid = get_patterns(data, type)
- response[:possible] << type if number_possible?(number, possible)
- response[:valid] << type if number_valid_and_possible?(number,
- possible,
- valid)
+ valid_and_possible, possible_result =
+ number_valid_and_possible?(phone, possible, valid)
+ response[:possible] << type if possible_result
+ response[:valid] << type if valid_and_possible
end
response
end
# returns array of phone types for check for current country data
+ #
+ # ==== Attributes
+ #
+ # * +data+ - country data hash
def types_for_check(data)
Core::TYPES_DESC.keys - PhoneAnalyzer::NOT_FOR_CHECK +
fixed_and_mobile_keys(data)
end
# Gets matched number formatting rule or default one
+ #
+ # ==== Attributes
+ #
+ # * +national+ - national phone number
+ # * +format_data+ - formatting data from country data
def get_number_format(national, format_data)
format_data && format_data.find do |format|
(format[Core::LEADING_DIGITS].nil? \
|| /^#{format[Core::LEADING_DIGITS]}/ =~ national) \
&& /^#{format[Core::PATTERN]}$/ =~ national
end || Core::DEFAULT_NUMBER_FORMAT
end
# Checks if fixed line pattern and mobile pattern are the same and returns
# appropriate keys
+ #
+ # ==== Attributes
+ #
+ # * +data+ - country data
def fixed_and_mobile_keys(data)
if data[Core::FIXED_LINE] == data[Core::MOBILE]
[Core::FIXED_OR_MOBILE]
else
[Core::FIXED_LINE, Core::MOBILE]
end
end
- # Returns array of two elements. Valid phone pattern and possible pattern
- def get_patterns(all_types, type)
+ # Returns possible and valid patterns for validation for provided type
+ #
+ # ==== Attributes
+ #
+ # * +all_patterns+ - hash of all patterns for validation
+ # * +type+ - type of phone to get patterns for
+ def get_patterns(all_patterns, type)
patterns = case type
when Core::FIXED_OR_MOBILE
- all_types[Core::FIXED_LINE]
+ all_patterns[Core::FIXED_LINE]
else
- all_types[type]
+ all_patterns[type]
end
return [nil, nil] if patterns.nil?
national_pattern = patterns[Core::VALID_PATTERN]
possible_pattern = patterns[Core::POSSIBLE_PATTERN] || national_pattern
[possible_pattern, national_pattern]
end
- # Checks if passed number matches both valid and possible patterns
+ # Checks if passed number matches valid and possible patterns
+ #
+ # ==== Attributes
+ #
+ # * +number+ - phone number for validation
+ # * +possible_pattern+ - possible pattern for validation
+ # * +national_pattern+ - valid pattern for validation
def number_valid_and_possible?(number, possible_pattern, national_pattern)
- national_match = number.match(/^(?:#{national_pattern})$/)
possible_match = number.match(/^(?:#{possible_pattern})$/)
+ possible = possible_match && possible_match.to_s.length == number.length
- national_match && possible_match &&
- national_match.to_s.length == number.length &&
- possible_match.to_s.length == number.length
- end
+ if possible
+ # doing national pattern match only in case possible matches
+ national_match = number.match(/^(?:#{national_pattern})$/)
+ valid = national_match && national_match.to_s.length == number.length
+ else
+ valid = false
+ end
- # Checks if passed number matches possible pattern
- def number_possible?(number, possible_pattern)
- possible_match = number.match(/^(?:#{possible_pattern})$/)
- possible_match && possible_match.to_s.length == number.length
+ [valid && possible, possible]
end
end
-end
\ No newline at end of file
+end