telephone_number 0.1.0 → 0.2.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.
@@ -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