telephone_number 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,5 +7,9 @@ module TelephoneNumber
7
7
  def valid?(number, country, keys = [])
8
8
  parse(number, country).valid?(keys)
9
9
  end
10
+
11
+ def invalid?(*args)
12
+ !valid?(*args)
13
+ end
10
14
  end
11
15
  end
@@ -28,19 +28,29 @@ module TelephoneNumber
28
28
  private
29
29
 
30
30
  def load_formats(country_data, territory)
31
- country_data[TelephoneNumber::PhoneData::FORMATS] = territory.css("availableFormats numberFormat").map do |format|
31
+ country_data[PhoneData::FORMATS] = territory.css("availableFormats numberFormat").map do |format|
32
32
  format_hash = {}.tap do |fhash|
33
- format.attributes.values.each { |attr| fhash[attr.name.to_sym] = attr.value.delete("\n ") }
34
- format.elements.each { |child| fhash[child.name.to_sym] = child.text.delete("\n ") }
33
+ format.attributes.values.each do |attr|
34
+ key = underscore(attr.name).to_sym
35
+ fhash[key] = if key == PhoneData::NATIONAL_PREFIX_FORMATTING_RULE
36
+ attr.value
37
+ else
38
+ attr.value.delete("\n ")
39
+ end
40
+ end
41
+ format.elements.each do |child|
42
+ key = underscore(child.name).to_sym
43
+ fhash[key] = [PhoneData::FORMAT, PhoneData::INTL_FORMAT].include?(key) ? child.text : child.text.delete("\n ")
44
+ end
35
45
  end
36
46
  end
37
47
  end
38
48
 
39
49
  def load_validations(country_data, territory)
40
- country_data[TelephoneNumber::PhoneData::VALIDATIONS] = {}
50
+ country_data[PhoneData::VALIDATIONS] = {}
41
51
  territory.elements.each do |element|
42
52
  next if element.name == "references" || element.name == "availableFormats"
43
- country_data[TelephoneNumber::PhoneData::VALIDATIONS][underscore(element.name).to_sym] = {}.tap do |validation_hash|
53
+ country_data[PhoneData::VALIDATIONS][underscore(element.name).to_sym] = {}.tap do |validation_hash|
44
54
  element.elements.each{|child| validation_hash[underscore(child.name).to_sym] = child.text.delete("\n ")}
45
55
  end
46
56
  end
@@ -48,7 +58,12 @@ module TelephoneNumber
48
58
 
49
59
  def load_base_attributes(country_data, territory)
50
60
  territory.attributes.each do |key, value_object|
51
- country_data[underscore(key).to_sym] = value_object.value
61
+ underscored_key = underscore(key).to_sym
62
+ country_data[underscored_key] = if underscored_key == PhoneData::NATIONAL_PREFIX_FOR_PARSING
63
+ value_object.value.delete("\n ")
64
+ else
65
+ value_object.value
66
+ end
52
67
  end
53
68
  end
54
69
 
@@ -0,0 +1,60 @@
1
+ module TelephoneNumber
2
+ module Formatter
3
+ def build_national_number(formatted: true)
4
+ return normalized_number if !valid? || format.nil?
5
+ captures = normalized_number.match(Regexp.new(format[PhoneData::PATTERN])).captures
6
+ national_prefix_formatting_rule = format[PhoneData::NATIONAL_PREFIX_FORMATTING_RULE] \
7
+ || country_data[PhoneData::NATIONAL_PREFIX_FORMATTING_RULE]
8
+
9
+ format_string = format[PhoneData::FORMAT].gsub(/(\$\d)/) { |cap| "%#{cap.reverse}s" }
10
+ formatted_string = sprintf(format_string, *captures)
11
+ captures.delete(PhoneData::MOBILE_TOKEN_COUNTRIES[country])
12
+
13
+ if national_prefix_formatting_rule
14
+ national_prefix_string = national_prefix_formatting_rule.dup
15
+ national_prefix_string.gsub!(/\$NP/, country_data[PhoneData::NATIONAL_PREFIX])
16
+ national_prefix_string.gsub!(/\$FG/, captures[0])
17
+ formatted_string.sub!(captures[0], national_prefix_string)
18
+ end
19
+
20
+ formatted ? formatted_string : sanitize(formatted_string)
21
+ end
22
+
23
+ def build_e164_number(formatted: true)
24
+ formatted_string = "+#{country_data[PhoneData::COUNTRY_CODE]}#{normalized_number}"
25
+ formatted ? formatted_string : sanitize(formatted_string)
26
+ end
27
+
28
+ def build_international_number(formatted: true)
29
+ return normalized_number if !valid? || format.nil?
30
+ captures = normalized_number.match(Regexp.new(format[PhoneData::PATTERN])).captures
31
+ key = format.fetch(PhoneData::INTL_FORMAT, 'NA') != 'NA' ? PhoneData::INTL_FORMAT : PhoneData::FORMAT
32
+ format_string = format[key].gsub(/(\$\d)/) { |cap| "%#{cap.reverse}s" }
33
+ "+#{country_data[PhoneData::COUNTRY_CODE]} #{sprintf(format_string, *captures)}"
34
+ end
35
+
36
+ private
37
+
38
+ def extract_format
39
+ native_country_format = detect_format(country.to_sym)
40
+ return native_country_format if native_country_format
41
+
42
+ # This means we couldn't find an applicable format so we now need to scan through the hierarchy
43
+ parent_country_code = PhoneData.phone_data.detect do |country_code, country_data|
44
+ country_data[PhoneData::COUNTRY_CODE] == PhoneData.phone_data[self.country.to_sym][PhoneData::COUNTRY_CODE] \
45
+ && country_data[PhoneData::MAIN_COUNTRY_FOR_CODE] == 'true'
46
+ end
47
+ detect_format(parent_country_code[0])
48
+ end
49
+
50
+ def detect_format(country_code)
51
+ PhoneData.phone_data[country_code.to_sym][PhoneData::FORMATS].detect do |format|
52
+ (format[PhoneData::LEADING_DIGITS].nil? \
53
+ || normalized_number =~ Regexp.new("^(#{format[PhoneData::LEADING_DIGITS]})")) \
54
+ && normalized_number =~ Regexp.new("^(#{format[PhoneData::PATTERN]})$")
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+
@@ -1,27 +1,59 @@
1
1
  module TelephoneNumber
2
2
  class Number
3
3
  include TelephoneNumber::Parser
4
+ include TelephoneNumber::Formatter
4
5
 
5
- attr_reader :original_number, :country, :e164_number, :national_number
6
+ attr_reader :original_number, :normalized_number, :country, :country_data
6
7
 
7
8
  def initialize(number, country)
8
9
  return unless number && country
9
10
 
10
- @original_number = sanitize(number)
11
+ @original_number = sanitize(number).freeze
11
12
  @country = country.upcase.to_sym
12
- @national_number, @e164_number = extract_number_types(@original_number, @country)
13
+ @country_data = PhoneData.phone_data[@country]
14
+
15
+ # normalized_number is basically a "best effort" at national number without
16
+ # any formatting. This is what we will use to derive formats, validations and
17
+ # basically anything else that uses google data
18
+ @normalized_number = build_normalized_number
13
19
  end
14
20
 
15
21
  def valid_types
16
- @valid_types ||= validate(e164_number, country)
22
+ @valid_types ||= validate
17
23
  end
18
24
 
19
25
  def valid?(keys = [])
20
26
  keys.empty? ? !valid_types.empty? : !(valid_types & keys.map(&:to_sym)).empty?
21
27
  end
22
28
 
29
+ def national_number(formatted: true)
30
+ if formatted
31
+ @formatted_national_number ||= build_national_number
32
+ else
33
+ @unformatted_national_number ||= build_national_number(formatted: false)
34
+ end
35
+ end
36
+
37
+ def e164_number(formatted: true)
38
+ if formatted
39
+ @formatted_e164_number ||= build_e164_number
40
+ else
41
+ @e164_number ||= build_e164_number(formatted: false)
42
+ end
43
+ end
44
+
45
+ def international_number(formatted: true)
46
+ if formatted
47
+ @formatted_international_number ||= build_international_number
48
+ else
49
+ @international_number ||= build_international_number(formatted: false)
50
+ end
51
+ end
52
+
53
+ private
54
+
23
55
  def format
24
- @format ||= extract_format(national_number, country)
56
+ @format ||= extract_format
25
57
  end
26
58
  end
27
59
  end
@@ -1,39 +1,68 @@
1
1
  module TelephoneNumber
2
2
  module Parser
3
- KEYS_TO_SKIP = [TelephoneNumber::PhoneData::GENERAL,
4
- TelephoneNumber::PhoneData::AREA_CODE_OPTIONAL]
3
+ KEYS_TO_SKIP = [PhoneData::GENERAL, PhoneData::AREA_CODE_OPTIONAL]
5
4
 
6
5
  def sanitize(input_number)
7
6
  return input_number.gsub(/[^0-9]/, "")
8
7
  end
9
8
 
10
- def extract_number_types(input_number, country)
11
- country_data = TelephoneNumber::PhoneData.phone_data[country.to_sym]
9
+ # returns an array of valid types for the normalized number
10
+ # if array is empty, we can assume that the number is invalid
11
+ def validate
12
+ return [] unless country_data
13
+ applicable_keys = country_data[PhoneData::VALIDATIONS].reject{ |key, _value| KEYS_TO_SKIP.include?(key) }
14
+ applicable_keys.map do |phone_type, validations|
15
+ full = "^(#{validations[PhoneData::VALID_PATTERN]})$"
16
+ phone_type.to_sym if normalized_number =~ Regexp.new(full)
17
+ end.compact
18
+ end
12
19
 
13
- return [input_number, nil] unless country_data
14
- country_code = country_data[TelephoneNumber::PhoneData::COUNTRY_CODE]
20
+ private
15
21
 
16
- reg_string = "^(#{country_code})?"
17
- reg_string += "(#{country_data[TelephoneNumber::PhoneData::NATIONAL_PREFIX]})?"
18
- reg_string += "(#{country_data[TelephoneNumber::PhoneData::VALIDATIONS]\
19
- [TelephoneNumber::PhoneData::GENERAL]\
20
- [TelephoneNumber::PhoneData::VALID_PATTERN]})$"
22
+ def build_normalized_number
23
+ return original_number unless country_data
24
+ country_code = country_data[PhoneData::COUNTRY_CODE]
25
+
26
+ number_with_correct_prefix = parse_prefix
21
27
 
22
- match_result = input_number.match(Regexp.new(reg_string)) || []
28
+ reg_string = "^(#{country_code})?"
29
+ reg_string << "(#{country_data[PhoneData::NATIONAL_PREFIX]})?"
30
+ reg_string << "(#{country_data[PhoneData::VALIDATIONS][PhoneData::GENERAL][PhoneData::VALID_PATTERN]})$"
23
31
 
32
+ match_result = number_with_correct_prefix.match(Regexp.new(reg_string))
33
+ return original_number unless match_result
24
34
  prefix_results = [match_result[1], match_result[2]]
25
- without_prefix = input_number.sub(prefix_results.join, "")
26
- [without_prefix, "#{country_code}#{without_prefix}"]
35
+ number_with_correct_prefix.sub(prefix_results.join, '')
27
36
  end
28
37
 
29
- def validate(normalized_number, country)
30
- country_data = TelephoneNumber::PhoneData.phone_data[country.to_sym]
31
- return [] unless country_data
32
- applicable_keys = country_data[TelephoneNumber::PhoneData::VALIDATIONS].reject{ |key, _value| KEYS_TO_SKIP.include?(key) }
33
- applicable_keys.map do |phone_type, validations|
34
- full = "^(#{country_data[TelephoneNumber::PhoneData::COUNTRY_CODE]})(#{validations[TelephoneNumber::PhoneData::VALID_PATTERN]})$"
35
- phone_type if normalized_number =~ Regexp.new(full)
36
- end.compact
38
+ def parse_prefix
39
+ return original_number unless country_data[:national_prefix_for_parsing]
40
+ duped = original_number.dup
41
+ match_object = duped.match(Regexp.new(country_data[:national_prefix_for_parsing]))
42
+
43
+ # we need to do the "start_with?" here because we need to make sure it's not finding
44
+ # something in the middle of the number. However, we can't modify the regex to do this
45
+ # for us because it will offset the match groups that are referenced in the transform rules
46
+ return original_number unless match_object && duped.start_with?(match_object[0])
47
+ if country_data[:national_prefix_transform_rule]
48
+ transform_national_prefix(duped, match_object)
49
+ else
50
+ duped.sub!(match_object[0], '')
51
+ end
52
+ end
53
+
54
+ def transform_national_prefix(duped, match_object)
55
+ if PhoneData::MOBILE_TOKEN_COUNTRIES.include?(country) && match_object.captures.any?
56
+ sprintf(build_format_string, duped.sub!(match_object[0], match_object[1]))
57
+ elsif match_object.captures.none?
58
+ duped.sub!(match_object[0], '')
59
+ else
60
+ sprintf(build_format_string, *match_object.captures)
61
+ end
62
+ end
63
+
64
+ def build_format_string
65
+ country_data[:national_prefix_transform_rule].gsub(/(\$\d)/) {|cap| "%#{cap.reverse}s"}
37
66
  end
38
67
  end
39
68
  end
@@ -1,25 +1,32 @@
1
1
  module TelephoneNumber
2
2
  module PhoneData
3
- VALIDATIONS = :validations.freeze
3
+ AREA_CODE_OPTIONAL = :area_code_optional.freeze
4
+ COUNTRY_CODE = :country_code.freeze
5
+ FIXED_LINE = :fixed_line.freeze
4
6
  FORMATS = :formats.freeze
7
+ FORMAT = :format.freeze
5
8
  GENERAL = :general_desc.freeze
9
+ INTERNATIONAL_PREFIX = :international_prefix.freeze
10
+ INTL_FORMAT = :intl_format.freeze
11
+ LEADING_DIGITS = :leading_digits.freeze
12
+ MAIN_COUNTRY_FOR_CODE = :main_country_for_code.freeze
13
+ MOBILE = :mobile.freeze
14
+ MOBILE_TOKEN_COUNTRIES = { AR: '9' }.freeze
15
+ NATIONAL_PREFIX = :national_prefix.freeze
16
+ NATIONAL_PREFIX_FOR_PARSING = :national_prefix_for_parsing.freeze
17
+ NATIONAL_PREFIX_FORMATTING_RULE = :national_prefix_formatting_rule.freeze
18
+ NO_INTERNATIONAL_DIALING = :no_international_dialling.freeze
19
+ PATTERN = :pattern
20
+ PERSONAL_NUMBER = :personal_number.freeze
21
+ POSSIBLE_PATTERN = :possible_number_pattern.freeze
6
22
  PREMIUM_RATE = :premium_rate.freeze
7
- TOLL_FREE = :toll_free.freeze
8
23
  SHARED_COST = :shared_cost.freeze
9
- VOIP = :voip.freeze
10
- PERSONAL_NUMBER = :personal_number.freeze
24
+ TOLL_FREE = :toll_free.freeze
11
25
  UAN = :uan.freeze
12
- VOICEMAIL = :voicemail.freeze
13
- FIXED_LINE = :fixed_line.freeze
14
- MOBILE = :mobile.freeze
15
- NO_INTERNATIONAL_DIALING = :no_international_dialling.freeze
16
- AREA_CODE_OPTIONAL = :area_code_optional.freeze
26
+ VALIDATIONS = :validations.freeze
17
27
  VALID_PATTERN = :national_number_pattern.freeze
18
- POSSIBLE_PATTERN = :possible_number_pattern.freeze
19
- NATIONAL_PREFIX = :national_prefix.freeze
20
- COUNTRY_CODE = :country_code.freeze
21
- LEADING_DIGITS = :leading_digits.freeze
22
- INTERNATIONAL_PREFIX = :international_prefix.freeze
28
+ VOICEMAIL = :voicemail.freeze
29
+ VOIP = :voip.freeze
23
30
 
24
31
  def self.phone_data
25
32
  @@phone_data ||= load_data
@@ -0,0 +1,91 @@
1
+ module TelephoneNumber
2
+ require 'nokogiri'
3
+ require 'httparty'
4
+ require 'yaml'
5
+
6
+ class TestDataGenerator
7
+ URL = "https://libphonenumber.appspot.com/phonenumberparser?number=%s&country=%s".freeze
8
+ COUNTRIES = {
9
+ AR: %w(111512345678 380151234567 299154104587 01112345678),
10
+ AU: %w(0467703037),
11
+ BR: %w(011992339376 1123456789 11961234567),
12
+ BY: %w(80152450911 294911911 152450911),
13
+ CA: %w(16135550119 16135550171 16135550112 16135550194
14
+ 16135550122 16135550131 15146708700 14169158200),
15
+ CL: %w(961234567 221234567),
16
+ CN: %w(15694876068 13910503766 15845989469 05523245954 04717158875 03748086894),
17
+ CO: %w(3211234567 12345678),
18
+ CR: %w(22123456 83123456),
19
+ DE: %w(15222503070),
20
+ FR: %w(0607114556),
21
+ GB: %w(448444156790 442079308181 442076139800 442076361000
22
+ 07780991912 442076299400 442072227888),
23
+ HK: %w(64636251),
24
+ IE: %w(0863634875),
25
+ MX: %w(4423593227 14423593227),
26
+ IN: %w(915622231515 912942433300 912912510101 911126779191
27
+ 912224818000 917462223999 912266653366 912266325757
28
+ 914066298585 911242451234 911166566162 911123890606
29
+ 911123583754 5622231515 2942433300 2912510101
30
+ 1126779191 2224818000 7462223999 2266653366
31
+ 2266325757 4066298585 1242451234 1166566162
32
+ 1123890606 1123583754 09176642499),
33
+ JP: %w(312345678 9012345678),
34
+ SG: %w(96924755),
35
+ US: %w(16502530000 14044879000 15123435283 13032450086 16175751300
36
+ 3175083345 13128404100 12485934000 19497941600 14257395600 13103106000
37
+ 16086699600 12125650000 14123456700 14157360000 12068761800
38
+ 12023461100 3175082333)
39
+ }.freeze
40
+
41
+ def self.import!
42
+ output_hash = {}
43
+ fetch_data(output_hash)
44
+ write_file(output_hash)
45
+ true
46
+ end
47
+
48
+ def self.fetch_data(output_hash)
49
+ COUNTRIES.each do |key, value|
50
+ output_hash[key] = {}
51
+
52
+ value.each_with_index do |num, counter|
53
+ page = HTTParty.get(sprintf(URL, num, key.to_s))
54
+ parsed_page = Nokogiri::HTML.parse(page)
55
+ body = parsed_page.elements.first.elements.css('body').first
56
+ parsed_data = parse_remote_data(counter, body.elements.css('table')[2])
57
+ output_hash[key].merge!(parsed_data)
58
+ end
59
+ end
60
+ return output_hash
61
+ end
62
+
63
+ def self.parse_remote_data(counter, table)
64
+ output = { counter.to_s => {} }
65
+ table.elements.each do |row|
66
+ next if row.elements.one?
67
+ key = case row.elements.css('th').text
68
+ when 'E164 format'
69
+ :e164_formatted
70
+ when 'National format'
71
+ :national_formatted
72
+ when 'International format'
73
+ :international_formatted
74
+ end
75
+
76
+ output[counter.to_s][key] = row.elements.css('td').text if key
77
+ end
78
+ return output
79
+ end
80
+
81
+ def self.write_file(data)
82
+ File.open('test/valid_numbers.yml', 'w') do |file|
83
+ file.write "# This file is generated automatically by TestDataGenerator. \n" \
84
+ "# Any changes made to this file will be overridden next time test data is generated. \n" \
85
+ "# Please edit TestDataGenerator if you need to add test cases. \n"
86
+
87
+ file.write data.to_yaml
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,3 +1,3 @@
1
1
  module TelephoneNumber
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -2,8 +2,10 @@ require "telephone_number/version"
2
2
 
3
3
  module TelephoneNumber
4
4
  autoload :DataImporter, 'telephone_number/data_importer'
5
+ autoload :TestDataGenerator, 'telephone_number/test_data_generator'
5
6
  autoload :Parser, 'telephone_number/parser'
6
7
  autoload :Number, 'telephone_number/number'
8
+ autoload :Formatter, 'telephone_number/formatter'
7
9
  autoload :PhoneData, 'telephone_number/phone_data'
8
10
  autoload :ClassMethods, 'telephone_number/class_methods'
9
11
 
@@ -26,5 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "pry"
27
27
  spec.add_development_dependency "pry-byebug"
28
28
  spec.add_development_dependency "nokogiri"
29
+ spec.add_development_dependency "httparty"
29
30
  spec.add_development_dependency "minitest-focus"
31
+ spec.add_development_dependency 'coveralls'
30
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: telephone_number
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - MOBI Wireless Management
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-01-20 00:00:00.000000000 Z
11
+ date: 2017-02-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: httparty
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: minitest-focus
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +122,20 @@ dependencies:
108
122
  - - ">="
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: coveralls
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
111
139
  description:
112
140
  email:
113
141
  - adam.fernung@mobiwm.com
@@ -118,12 +146,13 @@ extra_rdoc_files: []
118
146
  files:
119
147
  - ".gitignore"
120
148
  - ".hound.yml"
149
+ - ".rubocop.default.yml"
150
+ - ".rubocop.yml"
121
151
  - ".ruby-version"
122
- - ".ruby.yml"
123
152
  - ".travis.yml"
124
153
  - CODE_OF_CONDUCT.md
125
154
  - Gemfile
126
- - LICENSE.txt
155
+ - LICENSE
127
156
  - README.md
128
157
  - Rakefile
129
158
  - bin/console
@@ -133,12 +162,13 @@ files:
133
162
  - lib/telephone_number.rb
134
163
  - lib/telephone_number/class_methods.rb
135
164
  - lib/telephone_number/data_importer.rb
165
+ - lib/telephone_number/formatter.rb
136
166
  - lib/telephone_number/number.rb
137
167
  - lib/telephone_number/parser.rb
138
168
  - lib/telephone_number/phone_data.rb
169
+ - lib/telephone_number/test_data_generator.rb
139
170
  - lib/telephone_number/version.rb
140
171
  - telephone_number.gemspec
141
- - telephone_number_data_file.xml
142
172
  homepage: https://github.com/mobi/telephone_number
143
173
  licenses:
144
174
  - MIT