telephone_number 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -1
- data/lib/telephone_number/class_methods.rb +7 -2
- data/lib/telephone_number/data_importer.rb +14 -7
- data/lib/telephone_number/formatter.rb +56 -12
- data/lib/telephone_number/number.rb +8 -51
- data/lib/telephone_number/parser.rb +39 -10
- data/lib/telephone_number/phone_data.rb +8 -1
- data/lib/telephone_number/version.rb +1 -1
- data/lib/telephone_number.rb +1 -7
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9176d842a4931a224fbc4e89bcf4f643e6bb4ab2
|
4
|
+
data.tar.gz: 88a4b6d3439017b5a9b68a15ae752c790373a475
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c1edb3e9c8f23ff5b8b43bf803995aac9c4c189c86ae453fe03c19d664af3bd8379be03c6aeaea8ad7f4a838efb1d0ac19dafb13bef84c90607a9af36bc51b4
|
7
|
+
data.tar.gz: c078804473a1a25106b6911b1bf379ca7727f5387a9dcfe8c34947c2f9b478bc1d868b083796943380497c631221355ef5f15f5ee0f6444bb518d01c909f9251
|
data/README.md
CHANGED
@@ -2,7 +2,13 @@
|
|
2
2
|
|
3
3
|
# What is it?
|
4
4
|
|
5
|
-
TelephoneNumber is global phone number validation gem based on Google's [libphonenumber](https://github.com/googlei18n/libphonenumber) library.
|
5
|
+
TelephoneNumber is global phone number validation gem based on Google's [libphonenumber](https://github.com/googlei18n/libphonenumber) library.
|
6
|
+
|
7
|
+
## Demo
|
8
|
+
|
9
|
+
Feel free to check out our demo!
|
10
|
+
|
11
|
+
[Numberjack](http://numberjack.io)
|
6
12
|
|
7
13
|
## Installation
|
8
14
|
|
@@ -1,15 +1,20 @@
|
|
1
1
|
module TelephoneNumber
|
2
2
|
module ClassMethods
|
3
|
-
def parse(number, country)
|
3
|
+
def parse(number, country = Parser.detect_country(number))
|
4
4
|
TelephoneNumber::Number.new(number, country)
|
5
5
|
end
|
6
6
|
|
7
|
-
def valid?(number, country, keys = [])
|
7
|
+
def valid?(number, country = Parser.detect_country(number), keys = [])
|
8
8
|
parse(number, country).valid?(keys)
|
9
9
|
end
|
10
10
|
|
11
11
|
def invalid?(*args)
|
12
12
|
!valid?(*args)
|
13
13
|
end
|
14
|
+
|
15
|
+
# generates binary file from xml that user gives us
|
16
|
+
def generate_override_file(file)
|
17
|
+
DataImporter.new(file, override: true).import!
|
18
|
+
end
|
14
19
|
end
|
15
20
|
end
|
@@ -1,16 +1,17 @@
|
|
1
1
|
module TelephoneNumber
|
2
2
|
require 'nokogiri'
|
3
3
|
class DataImporter
|
4
|
-
attr_reader :data, :file
|
4
|
+
attr_reader :data, :file, :override
|
5
5
|
|
6
|
-
def initialize(file_name)
|
6
|
+
def initialize(file_name, override: false)
|
7
7
|
@data = {}
|
8
8
|
@file = File.open(file_name) { |f| Nokogiri::XML(f) }
|
9
|
+
@override = override
|
9
10
|
end
|
10
11
|
|
11
|
-
def import!
|
12
|
+
def import!
|
12
13
|
parse_main_data
|
13
|
-
save_data_file
|
14
|
+
save_data_file
|
14
15
|
end
|
15
16
|
|
16
17
|
def parse_main_data
|
@@ -28,7 +29,7 @@ module TelephoneNumber
|
|
28
29
|
private
|
29
30
|
|
30
31
|
def load_formats(country_data, territory)
|
31
|
-
|
32
|
+
formats_arr = territory.css('availableFormats numberFormat').map do |format|
|
32
33
|
{}.tap do |fhash|
|
33
34
|
format.attributes.values.each do |attr|
|
34
35
|
key = underscore(attr.name).to_sym
|
@@ -44,6 +45,9 @@ module TelephoneNumber
|
|
44
45
|
end
|
45
46
|
end
|
46
47
|
end
|
48
|
+
|
49
|
+
return if override && formats_arr.empty?
|
50
|
+
country_data[PhoneData::FORMATS] = formats_arr
|
47
51
|
end
|
48
52
|
|
49
53
|
def load_validations(country_data, territory)
|
@@ -54,6 +58,7 @@ module TelephoneNumber
|
|
54
58
|
element.elements.each { |child| validation_hash[underscore(child.name).to_sym] = child.text.delete("\n ") }
|
55
59
|
end
|
56
60
|
end
|
61
|
+
country_data.delete(PhoneData::VALIDATIONS) if country_data[PhoneData::VALIDATIONS].empty? && override
|
57
62
|
end
|
58
63
|
|
59
64
|
def load_base_attributes(country_data, territory)
|
@@ -68,7 +73,9 @@ module TelephoneNumber
|
|
68
73
|
end
|
69
74
|
|
70
75
|
def load_references(country_data, territory)
|
71
|
-
|
76
|
+
ref_arr = territory.css('references sourceUrl').map(&:text)
|
77
|
+
return if override && ref_arr.empty?
|
78
|
+
country_data[:references] = ref_arr
|
72
79
|
end
|
73
80
|
|
74
81
|
def underscore(camel_cased_word)
|
@@ -81,7 +88,7 @@ module TelephoneNumber
|
|
81
88
|
word
|
82
89
|
end
|
83
90
|
|
84
|
-
def save_data_file
|
91
|
+
def save_data_file
|
85
92
|
data_file = override ? 'telephone_number_data_override_file.dat' : "#{File.dirname(__FILE__)}/../../data/telephone_number_data_file.dat"
|
86
93
|
File.open(data_file, 'wb+') { |f| Marshal.dump(@data, f) }
|
87
94
|
end
|
@@ -1,13 +1,56 @@
|
|
1
1
|
module TelephoneNumber
|
2
|
-
|
2
|
+
class Formatter
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
attr_reader :normalized_number, :phone_data, :valid
|
6
|
+
|
7
|
+
delegate [:country_data, :country] => :phone_data
|
8
|
+
|
9
|
+
def initialize(number_obj, phone_data)
|
10
|
+
@normalized_number = number_obj.normalized_number
|
11
|
+
@phone_data = phone_data
|
12
|
+
@valid = number_obj.valid?
|
13
|
+
end
|
14
|
+
|
15
|
+
def national_number(formatted: true)
|
16
|
+
if formatted
|
17
|
+
@formatted_national_number ||= build_national_number
|
18
|
+
else
|
19
|
+
@national_number ||= build_national_number(formatted: false)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def e164_number(formatted: true)
|
24
|
+
if formatted
|
25
|
+
@formatted_e164_number ||= build_e164_number
|
26
|
+
else
|
27
|
+
@e164_number ||= build_e164_number(formatted: false)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def international_number(formatted: true)
|
32
|
+
if formatted
|
33
|
+
@formatted_international_number ||= build_international_number
|
34
|
+
else
|
35
|
+
@international_number ||= build_international_number(formatted: false)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
alias_method :valid?, :valid
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def number_format
|
44
|
+
@number_format ||= extract_number_format
|
45
|
+
end
|
46
|
+
|
3
47
|
def build_national_number(formatted: true)
|
4
48
|
return normalized_or_default if !valid? || number_format.nil?
|
5
49
|
captures = normalized_number.match(Regexp.new(number_format[PhoneData::PATTERN])).captures
|
6
50
|
national_prefix_formatting_rule = number_format[PhoneData::NATIONAL_PREFIX_FORMATTING_RULE] \
|
7
51
|
|| country_data[PhoneData::NATIONAL_PREFIX_FORMATTING_RULE]
|
8
52
|
|
9
|
-
|
10
|
-
formatted_string = format(format_string, *captures)
|
53
|
+
formatted_string = format(ruby_format_string(number_format[PhoneData::FORMAT]), *captures)
|
11
54
|
captures.delete(PhoneData::MOBILE_TOKEN_COUNTRIES[country])
|
12
55
|
|
13
56
|
if national_prefix_formatting_rule
|
@@ -17,30 +60,31 @@ module TelephoneNumber
|
|
17
60
|
formatted_string.sub!(captures[0], national_prefix_string)
|
18
61
|
end
|
19
62
|
|
20
|
-
formatted ? formatted_string : sanitize(formatted_string)
|
63
|
+
formatted ? formatted_string : Parser.sanitize(formatted_string)
|
21
64
|
end
|
22
65
|
|
23
66
|
def build_e164_number(formatted: true)
|
67
|
+
return normalized_or_default if !country_data || normalized_number.empty?
|
24
68
|
formatted_string = "+#{country_data[PhoneData::COUNTRY_CODE]}#{normalized_number}"
|
25
|
-
formatted ? formatted_string : sanitize(formatted_string)
|
69
|
+
formatted ? formatted_string : Parser.sanitize(formatted_string)
|
26
70
|
end
|
27
71
|
|
28
72
|
def build_international_number(formatted: true)
|
29
73
|
return normalized_or_default if !valid? || number_format.nil?
|
30
74
|
captures = normalized_number.match(Regexp.new(number_format[PhoneData::PATTERN])).captures
|
31
75
|
key = number_format.fetch(PhoneData::INTL_FORMAT, 'NA') != 'NA' ? PhoneData::INTL_FORMAT : PhoneData::FORMAT
|
32
|
-
|
33
|
-
formatted_string
|
34
|
-
formatted ? formatted_string : sanitize(formatted_string)
|
76
|
+
formatted_string = "+#{country_data[PhoneData::COUNTRY_CODE]} #{format(ruby_format_string(number_format[key]), *captures)}"
|
77
|
+
formatted ? formatted_string : Parser.sanitize(formatted_string)
|
35
78
|
end
|
36
79
|
|
37
|
-
|
80
|
+
def ruby_format_string(format_string)
|
81
|
+
format_string.gsub(/(\$\d)/) { |cap| "%#{cap.reverse}s" }
|
82
|
+
end
|
38
83
|
|
39
84
|
def normalized_or_default
|
40
|
-
return normalized_number
|
85
|
+
return normalized_number unless TelephoneNumber.default_format_string && TelephoneNumber.default_format_pattern
|
41
86
|
captures = normalized_number.match(TelephoneNumber.default_format_pattern).captures
|
42
|
-
|
43
|
-
format(format_string, *captures)
|
87
|
+
format(ruby_format_string(TelephoneNumber.default_format_string), *captures)
|
44
88
|
end
|
45
89
|
|
46
90
|
def extract_number_format
|
@@ -1,59 +1,16 @@
|
|
1
1
|
module TelephoneNumber
|
2
2
|
class Number
|
3
|
-
|
4
|
-
include TelephoneNumber::Formatter
|
3
|
+
extend Forwardable
|
5
4
|
|
6
|
-
attr_reader :
|
5
|
+
attr_reader :phone_data, :parser, :formatter
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@original_number = sanitize(number).freeze
|
12
|
-
@country = country.upcase.to_sym
|
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
|
19
|
-
end
|
20
|
-
|
21
|
-
def valid_types
|
22
|
-
@valid_types ||= validate
|
23
|
-
end
|
24
|
-
|
25
|
-
def valid?(keys = [])
|
26
|
-
keys.empty? ? !valid_types.empty? : !(valid_types & keys.map(&:to_sym)).empty?
|
27
|
-
end
|
7
|
+
delegate [:valid?, :valid_types, :normalized_number] => :parser
|
8
|
+
delegate [:national_number, :e164_number, :international_number] => :formatter
|
28
9
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
55
|
-
def number_format
|
56
|
-
@number_format ||= extract_number_format
|
10
|
+
def initialize(number, country)
|
11
|
+
@phone_data = PhoneData.new(country)
|
12
|
+
@parser = Parser.new(number, @phone_data)
|
13
|
+
@formatter = Formatter.new(self, @phone_data)
|
57
14
|
end
|
58
15
|
end
|
59
16
|
end
|
@@ -1,11 +1,39 @@
|
|
1
1
|
module TelephoneNumber
|
2
|
-
|
2
|
+
class Parser
|
3
3
|
KEYS_TO_SKIP = [PhoneData::GENERAL, PhoneData::AREA_CODE_OPTIONAL]
|
4
|
+
extend Forwardable
|
4
5
|
|
5
|
-
|
6
|
-
|
6
|
+
delegate [:country_data, :country] => :phone_data
|
7
|
+
attr_reader :sanitized_number, :original_number, :normalized_number, :phone_data
|
8
|
+
|
9
|
+
def initialize(original_number, phone_data)
|
10
|
+
@sanitized_number = self.class.sanitize(original_number)
|
11
|
+
@phone_data = phone_data
|
12
|
+
@original_number = original_number
|
13
|
+
@normalized_number = build_normalized_number
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid_types
|
17
|
+
@valid_types ||= validate
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?(keys = [])
|
21
|
+
keys.empty? ? !valid_types.empty? : !(valid_types & keys.map(&:to_sym)).empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.detect_country(number)
|
25
|
+
sanitized_number = sanitize(number)
|
26
|
+
detected_country = PhoneData.phone_data.detect(->{[]}) do |two_letter, value|
|
27
|
+
sanitized_number =~ Regexp.new("^#{value[:country_code]}") && new(sanitized_number, PhoneData.new(two_letter)).valid?
|
28
|
+
end.first
|
7
29
|
end
|
8
30
|
|
31
|
+
def self.sanitize(input_number)
|
32
|
+
return input_number.to_s.gsub(/\D/, '')
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
9
37
|
# returns an array of valid types for the normalized number
|
10
38
|
# if array is empty, we can assume that the number is invalid
|
11
39
|
def validate
|
@@ -17,10 +45,11 @@ module TelephoneNumber
|
|
17
45
|
end.compact
|
18
46
|
end
|
19
47
|
|
20
|
-
|
21
|
-
|
48
|
+
# normalized_number is basically a "best effort" at national number without
|
49
|
+
# any formatting. This is what we will use to derive formats, validations and
|
50
|
+
# basically anything else that uses google data
|
22
51
|
def build_normalized_number
|
23
|
-
return
|
52
|
+
return sanitized_number unless country_data
|
24
53
|
country_code = country_data[PhoneData::COUNTRY_CODE]
|
25
54
|
|
26
55
|
number_with_correct_prefix = parse_prefix
|
@@ -30,20 +59,20 @@ module TelephoneNumber
|
|
30
59
|
reg_string << "(#{country_data[PhoneData::VALIDATIONS][PhoneData::GENERAL][PhoneData::VALID_PATTERN]})$"
|
31
60
|
|
32
61
|
match_result = number_with_correct_prefix.match(Regexp.new(reg_string))
|
33
|
-
return
|
62
|
+
return sanitized_number unless match_result
|
34
63
|
prefix_results = [match_result[1], match_result[2]]
|
35
64
|
number_with_correct_prefix.sub(prefix_results.join, '')
|
36
65
|
end
|
37
66
|
|
38
67
|
def parse_prefix
|
39
|
-
return
|
40
|
-
duped =
|
68
|
+
return sanitized_number unless country_data[:national_prefix_for_parsing]
|
69
|
+
duped = sanitized_number.dup
|
41
70
|
match_object = duped.match(Regexp.new(country_data[:national_prefix_for_parsing]))
|
42
71
|
|
43
72
|
# we need to do the "start_with?" here because we need to make sure it's not finding
|
44
73
|
# something in the middle of the number. However, we can't modify the regex to do this
|
45
74
|
# for us because it will offset the match groups that are referenced in the transform rules
|
46
|
-
return
|
75
|
+
return sanitized_number unless match_object && duped.start_with?(match_object[0])
|
47
76
|
if country_data[:national_prefix_transform_rule]
|
48
77
|
transform_national_prefix(duped, match_object)
|
49
78
|
else
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module TelephoneNumber
|
2
|
-
|
2
|
+
class PhoneData
|
3
3
|
AREA_CODE_OPTIONAL = :area_code_optional
|
4
4
|
COUNTRY_CODE = :country_code
|
5
5
|
FIXED_LINE = :fixed_line
|
@@ -28,6 +28,8 @@ module TelephoneNumber
|
|
28
28
|
VOICEMAIL = :voicemail
|
29
29
|
VOIP = :voip
|
30
30
|
|
31
|
+
attr_reader :country_data, :country
|
32
|
+
|
31
33
|
def self.phone_data
|
32
34
|
@@phone_data ||= load_data
|
33
35
|
end
|
@@ -39,6 +41,11 @@ module TelephoneNumber
|
|
39
41
|
override_data = Marshal.load(File.binread(TelephoneNumber.override_file)) if TelephoneNumber.override_file
|
40
42
|
return main_data.deep_deep_merge!(override_data)
|
41
43
|
end
|
44
|
+
|
45
|
+
def initialize(country)
|
46
|
+
@country = country.to_s.upcase.to_sym
|
47
|
+
@country_data = self.class.phone_data[@country]
|
48
|
+
end
|
42
49
|
end
|
43
50
|
end
|
44
51
|
|
data/lib/telephone_number.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'telephone_number/version'
|
2
2
|
require 'utilities/hash'
|
3
|
+
require 'forwardable'
|
3
4
|
|
4
5
|
module TelephoneNumber
|
5
6
|
autoload :DataImporter, 'telephone_number/data_importer'
|
@@ -44,11 +45,4 @@ module TelephoneNumber
|
|
44
45
|
def self.default_format_pattern
|
45
46
|
@@default_format_pattern
|
46
47
|
end
|
47
|
-
|
48
|
-
|
49
|
-
# generates binary file from xml that user gives us
|
50
|
-
def self.generate_override_file(file)
|
51
|
-
DataImporter.new(file).import!(override: true)
|
52
|
-
end
|
53
|
-
|
54
48
|
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.
|
4
|
+
version: 1.0.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-
|
11
|
+
date: 2017-04-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|