phonelib 0.6.21 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 340a5c0487adffe92db6fbba129f1d79d669e3ca
4
- data.tar.gz: 58b742b98ed74a4a8b47573f30b0eb4e4963e4ab
2
+ SHA256:
3
+ metadata.gz: d9a96a615d554195979c6dc886188eb4f8638e89f616fb5ce5aa6cb74ee91ca0
4
+ data.tar.gz: e8b51d76743373fd9e2913eb5eddcea65e35e57673f29e47a361398dab662a16
5
5
  SHA512:
6
- metadata.gz: 935c0feb67ce471cba7f9e2c4046deba98ea3800e5bbd864ef227ec5503a4fd1b8317d8a00cac190e02c8f161c9c72191f44b4c79129f1436ee2d4d7a7136cf7
7
- data.tar.gz: 5377cf7fa504d3538163f7eac8142e8729620c0f558a48f3488e93a4a5ddd786061e2366028b21d525f16d68145b3c295be1dc8a243e11b068c1c3b5555a71f9
6
+ metadata.gz: 7710f2ab513e1bb53bb6bec2fe78de93fa742c18c85f2393f05ebf862cd245c7b3b01a4055403af287c4e225a58c26fd03507f0f08bf9e4d1468ad1279623ecd
7
+ data.tar.gz: e44959fbc033b27ac6b2974941c2ce010ba788bf371a4048a45c7d3dba78bb982b08c9201019b4bd2f958b139e46f7f29dcf0c0ae6c406a94d21490129df68de
data/README.md CHANGED
@@ -2,16 +2,20 @@
2
2
 
3
3
  [![Built in integration with JetBrains RubyMine](https://github.com/daddyz/phonelib/blob/master/icon_RubyMine.png?raw=true)](https://www.jetbrains.com/ruby/)
4
4
  [![Gem Version](https://badge.fury.io/rb/phonelib.svg)](http://badge.fury.io/rb/phonelib)
5
- [![Build Status](https://travis-ci.org/daddyz/phonelib.png?branch=master)](http://travis-ci.org/daddyz/phonelib)
5
+ [![CircleCI](https://circleci.com/gh/daddyz/phonelib/tree/master.svg?style=shield)](https://circleci.com/gh/daddyz/phonelib/tree/master)
6
6
  [![](https://codeclimate.com/github/daddyz/phonelib/badges/coverage.svg)](https://codeclimate.com/github/daddyz/phonelib/coverage)
7
7
  [![](https://codeclimate.com/github/daddyz/phonelib/badges/gpa.svg)](https://codeclimate.com/github/daddyz/phonelib)
8
- [![Dependency Status](https://gemnasium.com/daddyz/phonelib.svg)](https://gemnasium.com/daddyz/phonelib)
9
8
  [![Inline docs](http://inch-ci.org/github/daddyz/phonelib.svg?branch=master)](http://inch-ci.org/github/daddyz/phonelib)
10
9
 
11
10
  Phonelib is a gem allowing you to validate phone number. All validations are based on [Google libphonenumber](https://github.com/googlei18n/libphonenumber).
12
11
  Currently it can make basic validations and formatting to e164 international number format and national number format with prefix.
13
12
  But it still doesn't include all Google's library functionality.
14
13
 
14
+ ## Incorrect parsing or validation
15
+
16
+ In case your phone number is incorrectly parsed, you can check original libphonenumber for result [here](https://rawgit.com/googlei18n/libphonenumber/master/javascript/i18n/phonenumbers/demo-compiled.html) and in case of same parse result [open an issue for them](https://issuetracker.google.com/issues/new?component=192347&template=829703). This gem's data is based on it.
17
+ If you can't wait for libphonenumber to resolve the issue, try to use ```Phonelib.add_additional_regex``` and ```Phonelib.additional_regexes``` methods.
18
+
15
19
  ## Information
16
20
 
17
21
  ### Change Log
@@ -62,6 +66,18 @@ To disable sanitizing of passed phone number (keeping digits only)
62
66
  Phonelib.strict_check = true
63
67
  ```
64
68
 
69
+ To change sanitized symbols on parsed number, so non-specified symbols won't be wiped and will fail the parsing
70
+
71
+ ``` ruby
72
+ Phonelib.sanitize_regex = '[\.\-\(\) \;\+]'
73
+ ```
74
+
75
+ To disable sanitizing of double prefix on passed phone number
76
+
77
+ ```ruby
78
+ Phonelib.strict_double_prefix_check = true
79
+ ```
80
+
65
81
  To set different extension separator on formatting, this setting doesn't affect parsing. Default setting is ';'
66
82
 
67
83
  ``` ruby
@@ -72,7 +88,7 @@ To set symbols that are used for separating extension from phone number for pars
72
88
 
73
89
  ``` ruby
74
90
  Phonelib.extension_separate_symbols = '#;' # for single symbol separator
75
- Phonelib.extension_separator = %w(ext # ; extension) # each string will be treated as separator
91
+ Phonelib.extension_separate_symbols = %w(ext # ; extension) # each string will be treated as separator
76
92
  ```
77
93
 
78
94
  In case you need to overwrite some Google's libphonenumber library data, you need to assign file path to this setter. File should be Marshal.dump'ed with existing structure like in ```Phonelib.phone_data```. Gem is simply doing ```merge``` between hashes.
@@ -81,6 +97,21 @@ In case you need to overwrite some Google's libphonenumber library data, you nee
81
97
  Phonelib.override_phone_data = '/path/to/override_phone_data.dat'
82
98
  ```
83
99
 
100
+ In case you want to add some custom or still not updated regex patterns for certain type you can use additional regexes feature in a following way:
101
+
102
+ ``` ruby
103
+ Phonelib.add_additional_regex :us, Phonelib::Core::MOBILE, '[5]{10}' # this will add number 1-555-555-5555 to be valid
104
+ Phonelib.add_additional_regex :gb, Phonelib::Core::MOBILE, '[1]{5}' # this will add number 44-11-111 to be valid
105
+ # you can also specify all regexes using this method
106
+ Phonelib.additional_regexes = [[:us, :mobile, "[5]{10}"], [:gb, :mobile, "[1]{5}"]]
107
+ # or just use dump method to keep them altogether
108
+ Phonelib.dump_additional_regexes # => [["US", :mobile, "[5]{10}"], ["GB", :mobile, "[1]{5}"]
109
+ ```
110
+
111
+ (!) For a list of available types refer to this readme.
112
+
113
+ (!) Please note that regex should be added as string
114
+
84
115
  In case phone number that was passed for parsing has "+" sign in the beginning, library will try to detect a country regarding the provided one.
85
116
 
86
117
  ### ActiveRecord Integration
@@ -109,7 +140,10 @@ Refer to [Google libphonenumber](http://code.google.com/p/libphonenumber/) for m
109
140
  <tt>types: :mobile</tt> or <tt>types: [:voip, :mobile]</tt> - allows to validate against specific phone types patterns,
110
141
  if mixed with <tt>possible</tt> will check if number is possible for specified type
111
142
 
112
- <tt>country_specifier: -> phone { phone.country.try(:upcase) }</tt> - allows to specify country for validation dynamically for each validation.
143
+ <tt>countries: :us</tt> or <tt>countries: [:us, :ca]</tt> - allows to validate against specific countries,
144
+ if mixed with <tt>possible</tt> will check if number is possible for specified countries
145
+
146
+ <tt>country_specifier: :method_name</tt> or <tt>country_specifier: -> instance { instance.country.try(:upcase) }</tt> - allows to specify country for validation dynamically for each validation. Usefull when phone is stored as national number without country prefix.
113
147
 
114
148
  <tt>extensions: false</tt> - set to perform check for phone extension to be blank
115
149
 
@@ -143,6 +177,7 @@ Additionally you can run:
143
177
 
144
178
  ``` ruby
145
179
  phone = Phonelib.parse('123456789')
180
+ phone = Phonelib.parse('+1 (972) 123-4567', 'US')
146
181
  ```
147
182
 
148
183
  You can pass phone number with extension, it should be separated with <tt>;</tt> or <tt>#</tt> signs from the phone number.
@@ -222,7 +257,7 @@ phone.full_e164 # returns e164 phone representation with extension
222
257
  phone.full_international # returns formatted international number with extension
223
258
  ```
224
259
 
225
- You can pass <tt>false</tt> to <tt>national</tt> and <tt>international</tt> methods in order to get unformatted representaions
260
+ You can pass <tt>false</tt> to <tt>national</tt> and <tt>international</tt> methods in order to get unformatted representations
226
261
 
227
262
  ``` ruby
228
263
  phone.international(false) # returns unformatted international phone
@@ -235,12 +270,33 @@ You can get E164 formatted number
235
270
  phone.e164 # returns number in E164 format
236
271
  ```
237
272
 
273
+ You can define prefix for ```international``` and ```e164``` related methods to get formatted number prefixed with anything you need.
274
+
275
+ ``` ruby
276
+ phone.international('00') # returns formatted international number prefixed by 00 instead of +
277
+ phone.e164('00') # returns e164 represantation of a number prefixed by 00 instead of +
278
+ phone.full_international('00') # returns formatted international number with extension prefixed by 00 instead of +
279
+ phone.full_e164('00') # returns e164 represantation of a number with extension prefixed by 00 instead of +
280
+ phone.international_00 # same as phone.international('00'). 00 can be replaced with whatever you need
281
+ phone.e164_00 # same as phone.international('00')
282
+ ```
283
+
238
284
  There is a ```to_s``` method, it will return ```e164``` in case number is valid and ```original``` otherwise
239
285
 
240
286
  ``` ruby
241
287
  phone.to_s # returns number in E164 format if number is valid or original otherwise
242
288
  ```
243
289
 
290
+ You can compare 2 instances of ```Phonelib::Phone``` with ```==``` method or just use it with string
291
+
292
+ ```ruby
293
+ phone1 = Phonelib.parse('+12125551234') # Phonelib::Phone instance
294
+ phone2 = Phonelib.parse('+12125551234') # Phonelib::Phone instance
295
+ phone1 == phone2 # returns true
296
+ phone1 == '+12125551234' # returns true
297
+ phone1 == '12125551234;123' # returns true
298
+ ```
299
+
244
300
  There is extended data available for numbers. It will return <tt>nil</tt> in case there is no data or phone is impossible.
245
301
  Can return array of values in case there are some results for specified number
246
302
 
data/Rakefile CHANGED
@@ -22,6 +22,13 @@ end
22
22
  Bundler::GemHelper.install_tasks
23
23
 
24
24
  require 'rspec/core/rake_task'
25
+ module TempFixForRakeLastComment
26
+ def last_comment
27
+ last_description
28
+ end
29
+ end
30
+ Rake::Application.send :include, TempFixForRakeLastComment
31
+
25
32
  RSpec::Core::RakeTask.new(:spec) do |t|
26
33
  if defined? Rails
27
34
  puts 'Rails found! Running tests with Rails'
Binary file
data/data/phone_data.dat CHANGED
Binary file
data/lib/phonelib/core.rb CHANGED
@@ -107,6 +107,38 @@ module Phonelib
107
107
  @@strict_check = strict
108
108
  end
109
109
 
110
+ # @private sanitizing regex, matching symbols will get removed from parsed number, must be string
111
+ @@sanitize_regex = '[^0-9]+'
112
+
113
+ # getter for sanitize regex
114
+ # @return [String] regex of symbols to wipe from parsed number
115
+ def sanitize_regex
116
+ @@sanitize_regex
117
+ end
118
+
119
+ # setter for sanitize regex
120
+ # @param regex [String] symbols to wipe from parsed number
121
+ # @return [String] regex of symbols to wipe from parsed number
122
+ def sanitize_regex=(regex)
123
+ @@sanitize_regex = regex.is_a?(String) ? regex : regex.to_s
124
+ end
125
+
126
+ # @private strict double prefix check for validator, doesn't sanitize number
127
+ @@strict_double_prefix_check = false
128
+
129
+ # getter for strict double prefix check flag
130
+ # @return [Boolean] Flag defines whether to do strict double prefix parsing check
131
+ def strict_double_prefix_check
132
+ @@strict_double_prefix_check
133
+ end
134
+
135
+ # setter for strict double prefix check flag
136
+ # @param strict [Boolean] make a strict double prefix parsing or not
137
+ # @return [Boolean] Flag defines whether to do strict double prefix parsing check
138
+ def strict_double_prefix_check=(strict)
139
+ @@strict_double_prefix_check = strict
140
+ end
141
+
110
142
  @@override_phone_data = nil
111
143
  # setter for data file to use
112
144
  def override_phone_data=(file_path)
@@ -117,6 +149,42 @@ module Phonelib
117
149
  @@override_phone_data
118
150
  end
119
151
 
152
+ @@additional_regexes = {}
153
+ # setter for data file to use
154
+ def additional_regexes=(data)
155
+ return unless data.is_a?(Array)
156
+ @@additional_regexes = {}
157
+ data.each do |row|
158
+ next if row.size != 3
159
+ add_additional_regex(*row)
160
+ end
161
+ end
162
+
163
+ def add_additional_regex(country, type, national_regex)
164
+ return unless Phonelib::Core::TYPES_DESC.keys.include?(type.to_sym)
165
+ return unless national_regex.is_a?(String)
166
+ @@phone_data = nil
167
+ @@additional_regexes[country.to_s.upcase] ||= {}
168
+ @@additional_regexes[country.to_s.upcase][type] ||= []
169
+ @@additional_regexes[country.to_s.upcase][type] << national_regex
170
+ end
171
+
172
+ def dump_additional_regexes
173
+ rows = []
174
+ @@additional_regexes.each do |country, types|
175
+ types.each do |type, regexes|
176
+ regexes.each do |regex|
177
+ rows << [country, type, regex]
178
+ end
179
+ end
180
+ end
181
+ rows
182
+ end
183
+
184
+ def additional_regexes
185
+ @@additional_regexes
186
+ end
187
+
120
188
  @@vanity_conversion = false
121
189
  # setter for vanity phone numbers chars replacement
122
190
  def vanity_conversion=(value)
@@ -285,6 +353,8 @@ module Phonelib
285
353
  EXT_PREFIXES = :prefixes
286
354
  # @private Extended data geo names array key
287
355
  EXT_GEO_NAMES = :geo_names
356
+ # @private Extended data country names array key
357
+ EXT_COUNTRY_NAMES = :country_names
288
358
  # @private Extended data timezones array key
289
359
  EXT_TIMEZONES = :timezones
290
360
  # @private Extended data carriers array key
@@ -359,6 +429,21 @@ module Phonelib
359
429
  override_data_file = Marshal.load(File.binread(override_phone_data))
360
430
  default_data.merge!(override_data_file)
361
431
  end
432
+ additional_regexes.each do |country, types|
433
+ types.each do |type, regex|
434
+ default_data[country][Core::TYPES][type] ||= {}
435
+ [Core::VALID_PATTERN, Core::POSSIBLE_PATTERN].each do |key|
436
+ if default_data[country][Core::TYPES][type][key]
437
+ default_data[country][Core::TYPES][type][key] << "|#{regex.join('|')}"
438
+ else
439
+ default_data[country][Core::TYPES][type][key] = regex.join('|')
440
+ end
441
+ if type != Core::GENERAL
442
+ default_data[country][Core::TYPES][Core::GENERAL][key] << "|#{regex.join('|')}"
443
+ end
444
+ end
445
+ end
446
+ end
362
447
  default_data
363
448
  end
364
449
 
@@ -18,7 +18,48 @@ module Phonelib
18
18
  include Phonelib::DataImporterHelper
19
19
 
20
20
  # countries that can have double country prefix in number
21
- DOUBLE_COUNTRY_CODES_COUNTRIES = %w(IN DE BR IT NO)
21
+ DOUBLE_COUNTRY_CODES_COUNTRIES = %w(IN DE BR IT NO PL CU VN)
22
+ FORMAT_SHARING = {
23
+ 'CA' => 'US',
24
+ 'CC' => 'AU',
25
+ 'CX' => 'AU',
26
+ 'DM' => 'US',
27
+ 'DO' => 'US',
28
+ 'EH' => 'MA',
29
+ 'GD' => 'US',
30
+ 'GG' => 'GB',
31
+ 'GU' => 'US',
32
+ 'IM' => 'GB',
33
+ 'JE' => 'GB',
34
+ 'JM' => 'US',
35
+ 'KN' => 'US',
36
+ 'KY' => 'US',
37
+ 'KZ' => 'RU',
38
+ 'LC' => 'US',
39
+ 'MF' => 'GP',
40
+ 'MP' => 'US',
41
+ 'MS' => 'US',
42
+ 'PR' => 'US',
43
+ 'SJ' => 'NO',
44
+ 'SX' => 'US',
45
+ 'TA' => 'SH',
46
+ 'TC' => 'US',
47
+ 'TT' => 'US',
48
+ 'VA' => 'IT',
49
+ 'VC' => 'US',
50
+ 'VG' => 'US',
51
+ 'VI' => 'US',
52
+ 'YT' => 'RE',
53
+ 'AG' => 'US',
54
+ 'AI' => 'US',
55
+ 'AS' => 'US',
56
+ 'AX' => 'FI',
57
+ 'BB' => 'US',
58
+ 'BL' => 'GP',
59
+ 'BM' => 'US',
60
+ 'BQ' => 'CW',
61
+ 'BS' => 'US',
62
+ }
22
63
 
23
64
  # main data file in repo
24
65
  MAIN_FILE = 'resources/PhoneNumberMetadata.xml'
@@ -42,6 +83,7 @@ module Phonelib
42
83
  @geo_names = []
43
84
  @timezones = []
44
85
  @carriers = []
86
+ @countries = {}
45
87
 
46
88
  run_import
47
89
  end
@@ -54,9 +96,11 @@ module Phonelib
54
96
  import_main_data
55
97
  import_short_data
56
98
  import_alternate_formats
99
+ process_format_links
57
100
  import_geocoding_data
58
101
  import_timezone_data
59
102
  import_carrier_data
103
+ import_country_names
60
104
  save_data_file
61
105
  save_extended_data_file
62
106
  end
@@ -113,6 +157,14 @@ module Phonelib
113
157
  end
114
158
  end
115
159
 
160
+ # some countries missing formats, and are linking them to another countries
161
+ def process_format_links
162
+ FORMAT_SHARING.each do |destination, source|
163
+ @data[destination][:formats] ||= []
164
+ @data[destination][:formats] = @data[destination][:formats] + @data[source][:formats]
165
+ end
166
+ end
167
+
116
168
  # method parses geocoding data dir
117
169
  def import_geocoding_data
118
170
  puts 'IMPORTING GEOCODING DATA'
@@ -137,6 +189,21 @@ module Phonelib
137
189
  :c)
138
190
  end
139
191
 
192
+ # import country names
193
+ def import_country_names
194
+ puts 'IMPORTING COUNTRY NAMES'
195
+
196
+ require 'open-uri'
197
+ require 'csv'
198
+ io = open('http://api.geonames.org/countryInfoCSV?username=demo&style=full')
199
+ csv = CSV.new(io, {col_sep: "\t"})
200
+ csv.each do |row|
201
+ next if row[0].nil? || row[0].start_with?('#') || row[0].empty? || row[0].size != 2
202
+
203
+ @countries[row[0]] = row[4]
204
+ end
205
+ end
206
+
140
207
  # adds double country code flag in case country allows
141
208
  def add_double_country_flag(country)
142
209
  if DOUBLE_COUNTRY_CODES_COUNTRIES.include?(country[:id])
@@ -22,6 +22,7 @@ module Phonelib
22
22
  extended = {
23
23
  Phonelib::Core::EXT_PREFIXES => @prefixes,
24
24
  Phonelib::Core::EXT_GEO_NAMES => @geo_names,
25
+ Phonelib::Core::EXT_COUNTRY_NAMES => @countries,
25
26
  Phonelib::Core::EXT_TIMEZONES => @timezones,
26
27
  Phonelib::Core::EXT_CARRIERS => @carriers
27
28
  }
@@ -7,6 +7,9 @@ module Phonelib
7
7
  # @!attribute [r] extension
8
8
  # @return [String] phone extension passed for parsing after a number
9
9
  attr_reader :extension
10
+ # @!attribute [r] national_number
11
+ # @return [String] phone national number
12
+ attr_reader :national_number
10
13
 
11
14
  # including module that has all phone analyzing methods
12
15
  include Phonelib::PhoneAnalyzer
@@ -37,12 +40,21 @@ module Phonelib
37
40
  valid? ? e164 : original
38
41
  end
39
42
 
43
+ # Compare a phone number against a string or other parsed number
44
+ # @param other [String|Phonelib::Phone] Phone number to compare against
45
+ # @return [Boolean] result of equality comparison
46
+ def ==(other)
47
+ other = Phonelib.parse(other) unless other.is_a?(Phonelib::Phone)
48
+ return (e164 == other.e164) if valid? && other.valid?
49
+ original == other.original
50
+ end
51
+
40
52
  # method to get sanitized phone number (only numbers)
41
53
  # @return [String] Sanitized phone number
42
54
  def sanitized
43
55
  @sanitized ||=
44
56
  vanity_converted(@original).gsub(
45
- Phonelib.strict_check ? cr('^\+') : cr('[^0-9]+'),
57
+ Phonelib.strict_check ? cr('^\+') : cr(Phonelib.sanitize_regex),
46
58
  '')
47
59
  end
48
60
 
@@ -16,15 +16,38 @@ module Phonelib
16
16
  # * +passed_country+ - Country provided for parsing. Must be ISO code of
17
17
  # country (2 letters) like 'US', 'us' or :us for United States
18
18
  def analyze(phone, passed_country)
19
- country = country_or_default_country passed_country
19
+ countries = country_or_default_country passed_country
20
20
 
21
+ return analyze_single_country(phone, countries.first, passed_country) if countries.size == 1
22
+
23
+ results = {}
24
+ countries.map do |country|
25
+ results.merge! analyze_single_country(phone, country, passed_country)
26
+ end
27
+
28
+ pick_results(results)
29
+ end
30
+
31
+ private
32
+
33
+ # pick best result when several countries specified
34
+ def pick_results(results)
35
+ [:valid, :possible].each do |key|
36
+ final = results.select { |_k, v| v[key].any? }
37
+ return decorate_analyze_result(final) if final.size > 0
38
+ end
39
+
40
+ decorate_analyze_result(results)
41
+ end
42
+
43
+ def analyze_single_country(phone, country, passed_country)
21
44
  result = parse_country(phone, country)
22
45
  d_result = case
23
46
  when result && result.values.find { |e| e[:valid].any? }
24
47
  # all is good, return result
25
48
  when passed_country.nil?
26
49
  # trying for all countries if no country was passed
27
- detect_and_parse phone
50
+ detect_and_parse(phone, country)
28
51
  when country_can_dp?(country)
29
52
  # if country allows double prefix trying modified phone
30
53
  parse_country(changed_dp_phone(country, phone), country)
@@ -32,8 +55,6 @@ module Phonelib
32
55
  better_result(result, d_result)
33
56
  end
34
57
 
35
- private
36
-
37
58
  # method checks which result is better to return
38
59
  def better_result(base_result, result = nil)
39
60
  base_result ||= {}
@@ -102,11 +123,11 @@ module Phonelib
102
123
  # ==== Attributes
103
124
  #
104
125
  # * +phone+ - phone number for parsing
105
- def detect_and_parse(phone)
126
+ def detect_and_parse(phone, country)
106
127
  result = {}
107
128
  Phonelib.phone_data.each do |key, data|
108
129
  parsed = parse_single_country(phone, data)
109
- if double_prefix_allowed?(data, phone, parsed && parsed[key])
130
+ if (!Phonelib.strict_double_prefix_check || key == country) && double_prefix_allowed?(data, phone, parsed && parsed[key])
110
131
  parsed = parse_single_country(changed_dp_phone(key, phone), data)
111
132
  end
112
133
  result.merge!(parsed) unless parsed.nil?
@@ -3,6 +3,14 @@ module Phonelib
3
3
  module PhoneAnalyzerHelper
4
4
  private
5
5
 
6
+ def decorate_analyze_result(result)
7
+ if result.size == 1
8
+ result
9
+ else
10
+ Hash[result.take(1)]
11
+ end
12
+ end
13
+
6
14
  def original_starts_with_plus?
7
15
  original_s[0] == Core::PLUS_SIGN
8
16
  end
@@ -90,7 +98,11 @@ module Phonelib
90
98
  # * +country+ - country passed for parsing
91
99
  def country_or_default_country(country)
92
100
  country ||= (original_starts_with_plus? ? nil : Phonelib.default_country)
93
- country && country.to_s.upcase
101
+ if country.is_a?(Array)
102
+ country.compact.map { |e| e.to_s.upcase }
103
+ else
104
+ [country && country.to_s.upcase]
105
+ end
94
106
  end
95
107
 
96
108
  # constructs full regex for phone validation for provided phone data
@@ -102,6 +114,7 @@ module Phonelib
102
114
  # * +country_optional+ - whether to put country code as optional group
103
115
  def full_regex_for_data(data, type, country_optional = true)
104
116
  regex = []
117
+ regex << '0{2}?'
105
118
  regex << "(#{data[Core::INTERNATIONAL_PREFIX]})?"
106
119
  regex << "(#{data[Core::COUNTRY_CODE]})#{country_optional ? '?' : ''}"
107
120
  regex << "(#{data[Core::NATIONAL_PREFIX_FOR_PARSING] || data[Core::NATIONAL_PREFIX]})?"
@@ -133,7 +146,7 @@ module Phonelib
133
146
  def phone_match_data?(phone, data, possible = false)
134
147
  country_code = "#{data[Core::COUNTRY_CODE]}"
135
148
  inter_prefix = "(#{data[Core::INTERNATIONAL_PREFIX]})?"
136
- return unless phone.match cr("^#{inter_prefix}#{country_code}")
149
+ return unless phone.match cr("^0{2}?#{inter_prefix}#{country_code}")
137
150
 
138
151
  type = possible ? Core::POSSIBLE_PATTERN : Core::VALID_PATTERN
139
152
  phone.match full_regex_for_data(data, type, false)
@@ -32,6 +32,13 @@ module Phonelib
32
32
  Phonelib::Core::EXT_CARRIER_KEY
33
33
  end
34
34
 
35
+ # returns valid country name
36
+ def valid_country_name
37
+ return unless valid?
38
+
39
+ Phonelib.phone_ext_data[Phonelib::Core::EXT_COUNTRY_NAMES][valid_country]
40
+ end
41
+
35
42
  private
36
43
 
37
44
  # @private get name from extended phone data by keys
@@ -42,7 +49,7 @@ module Phonelib
42
49
  # * +id_key+ - parameter id key in resolved extended data for number
43
50
  #
44
51
  def get_ext_name(names_key, id_key)
45
- return nil unless ext_data[id_key] > 0
52
+ return nil unless ext_data[id_key]
46
53
 
47
54
  res = Phonelib.phone_ext_data[names_key][ext_data[id_key]]
48
55
  return nil unless res
@@ -51,7 +58,7 @@ module Phonelib
51
58
 
52
59
  # @private returns extended data ids for current number
53
60
  def ext_data
54
- return @ext_data if @ext_data
61
+ return @ext_data if defined?(@ext_data) && @ext_data
55
62
 
56
63
  result = default_ext_data
57
64
  return result unless possible?
@@ -72,7 +79,7 @@ module Phonelib
72
79
  # @private default extended data
73
80
  def default_ext_data
74
81
  result = {}
75
- EXT_KEYS.each { |key| result[key] = 0 }
82
+ EXT_KEYS.each { |key| result[key] = nil }
76
83
  result
77
84
  end
78
85
  end
@@ -36,22 +36,24 @@ module Phonelib
36
36
  Phonelib.phone_data[country][Core::COUNTRY_CODE]
37
37
  end
38
38
 
39
- # Returns e164 formatted phone number
39
+ # Returns e164 formatted phone number. Method can receive single string parameter that will be defined as prefix
40
40
  # @param formatted [Boolean] whether to return numbers only or formatted
41
+ # @param prefix [String] prefix to be placed before the number, "+" by default
41
42
  # @return [String] formatted international number
42
- def international(formatted = true)
43
+ def international(formatted = true, prefix = '+')
44
+ prefix = formatted if formatted.is_a?(String)
43
45
  return nil if sanitized.empty?
44
- return "+#{country_prefix_or_not}#{sanitized}" unless valid?
45
- return country_code + @national_number unless formatted
46
+ return "#{prefix}#{country_prefix_or_not}#{sanitized}" unless valid?
47
+ return "#{prefix}#{country_code}#{@national_number}" unless formatted
46
48
 
47
49
  fmt = @data[country][:format]
48
50
  national = @national_number
49
51
  if (matches = @national_number.match(cr(fmt[Core::PATTERN])))
50
52
  fmt = fmt[:intl_format] || fmt[:format]
51
- national = fmt.gsub(/\$\d/) { |el| matches[el[1].to_i] }
53
+ national = fmt.gsub(/\$\d/) { |el| matches[el[1].to_i] } unless fmt == 'NA'
52
54
  end
53
55
 
54
- "+#{country_code} #{national}"
56
+ "#{prefix}#{country_code} #{national}"
55
57
  end
56
58
 
57
59
  # returns national formatted number with extension added
@@ -61,22 +63,25 @@ module Phonelib
61
63
  end
62
64
 
63
65
  # returns international formatted number with extension added
66
+ # @param prefix [String] prefix to be placed before the number, "+" by default
64
67
  # @return [String] formatted internation phone number with extension
65
- def full_international
66
- "#{international}#{formatted_extension}"
68
+ def full_international(prefix = '+')
69
+ "#{international(true, prefix)}#{formatted_extension}"
67
70
  end
68
71
 
69
72
  # returns e164 format of phone with extension added
73
+ # @param prefix [String] prefix to be placed before the number, "+" by default
70
74
  # @return [String] phone formatted in E164 format with extension
71
- def full_e164
72
- "#{e164}#{formatted_extension}"
75
+ def full_e164(prefix = '+')
76
+ "#{e164(prefix)}#{formatted_extension}"
73
77
  end
74
78
 
75
79
  # Returns e164 unformatted phone number
80
+ # @param prefix [String] prefix to be placed before the number, "+" by default
76
81
  # @return [String] phone formatted in E164 format
77
- def e164
78
- international = self.international
79
- international && international.gsub(/[^+0-9]/, '')
82
+ def e164(prefix = '+')
83
+ international = self.international(false, '')
84
+ international && "#{prefix}#{international}"
80
85
  end
81
86
 
82
87
  # returns area code of parsed number
@@ -93,6 +98,15 @@ module Phonelib
93
98
  format_match[take_group]
94
99
  end
95
100
 
101
+ def method_missing(method, *args)
102
+ prefix_methods = %w(international_ full_international_ e164_ full_e164_)
103
+ method_s = method.to_s
104
+ prefix_methods.each do |key|
105
+ return send(key[0..-2], method_s.gsub(key, '')) if method_s.start_with?(key)
106
+ end
107
+ super
108
+ end
109
+
96
110
  private
97
111
 
98
112
  # @private defines if phone can have area code
@@ -135,7 +149,12 @@ module Phonelib
135
149
  rule.gsub!(/(\$NP|\$FG)/, '$NP' => prefix, '$FG' => '$1')
136
150
 
137
151
  # add space to format groups, change first group to rule,
138
- format_string = format[:format].gsub(/(\d)\$/, '\\1 $').gsub('$1', rule)
152
+ format_string = format[:format].gsub(/(\d)\$/, '\\1 $')
153
+ if format_string.include? '$1'
154
+ format_string.gsub! '$1', rule
155
+ else
156
+ format_string = rule.gsub('$1', '') + format_string
157
+ end
139
158
 
140
159
  @formatting_data =
141
160
  [@national_number.match(/#{format[Core::PATTERN]}/), format_string]
@@ -1,4 +1,4 @@
1
1
  module Phonelib
2
2
  # @private
3
- VERSION = '0.6.21'
3
+ VERSION = '0.7.0'
4
4
  end
data/lib/phonelib.rb CHANGED
@@ -12,5 +12,9 @@ module Phonelib
12
12
  end
13
13
 
14
14
  if defined?(ActiveModel) || defined?(Rails)
15
- autoload :PhoneValidator, 'validators/phone_validator'
15
+ if RUBY_VERSION >= '3.0.0'
16
+ autoload :PhoneValidator, 'validators/phone_validator3'
17
+ else
18
+ autoload :PhoneValidator, 'validators/phone_validator'
19
+ end
16
20
  end
@@ -36,6 +36,13 @@
36
36
  # validates :mobile, phone: { possible: true, types: :mobile }
37
37
  # end
38
38
  #
39
+ # validates that phone is valid and it is from specified country or countries
40
+ #
41
+ # class Phone < ActiveRecord::Base
42
+ # attr_accessible :number
43
+ # validates :number, phone: { countries: [:us, :ca] }
44
+ # end
45
+ #
39
46
  # Validates that attribute does not include an extension.
40
47
  # The default setting is to allow extensions
41
48
  #
@@ -51,21 +58,11 @@ class PhoneValidator < ActiveModel::EachValidator
51
58
  # Validation method
52
59
  def validate_each(record, attribute, value)
53
60
  return if options[:allow_blank] && value.blank?
54
- allowed_extensions = options.has_key?(:extensions) ? options[:extensions] : true
55
61
 
56
- phone = parse(value, specified_country(record))
57
- valid = if simple_validation?
58
- phone.send(validate_method)
59
- else
60
- (phone_types(phone) & types).size > 0
61
- end
62
+ @phone = parse(value, specified_country(record))
63
+ valid = phone_valid? && valid_types? && valid_country? && valid_extensions?
62
64
 
63
- # We default to not-allowing extensions for fax numbers
64
- if valid && !allowed_extensions && !phone.extension.empty?
65
- valid = false
66
- end
67
-
68
- record.errors.add(attribute, message, options) unless valid
65
+ record.errors.add(attribute, message, **options) unless valid
69
66
  end
70
67
 
71
68
  private
@@ -74,34 +71,60 @@ class PhoneValidator < ActiveModel::EachValidator
74
71
  options[:message] || :invalid
75
72
  end
76
73
 
77
- def validate_method
78
- options[:possible] ? :possible? : :valid?
74
+ def phone_valid?
75
+ @phone.send(options[:possible] ? :possible? : :valid?)
76
+ end
77
+
78
+ def valid_types?
79
+ return true unless options[:types]
80
+ (phone_types & types).size > 0
81
+ end
82
+
83
+ def valid_country?
84
+ return true unless options[:countries]
85
+ (phone_countries & countries).size > 0
86
+ end
87
+
88
+ def valid_extensions?
89
+ return true if !options.has_key?(:extensions) || options[:extensions]
90
+ @phone.extension.empty?
79
91
  end
80
92
 
81
93
  def specified_country(record)
82
94
  return unless options[:country_specifier]
83
- options[:country_specifier].call(record)
84
- end
85
95
 
86
- # @private
87
- def simple_validation?
88
- options[:types].nil?
96
+ if options[:country_specifier].is_a?(Symbol)
97
+ record.send(options[:country_specifier])
98
+ else
99
+ options[:country_specifier].call(record)
100
+ end
89
101
  end
90
102
 
91
103
  # @private
92
- # @param phone [Phonelib::Phone] parsed phone
93
- def phone_types(phone)
104
+ def phone_types
94
105
  method = options[:possible] ? :possible_types : :types
95
- phone_types = phone.send(method)
106
+ phone_types = @phone.send(method)
96
107
  if (phone_types & [Phonelib::Core::FIXED_OR_MOBILE]).size > 0
97
108
  phone_types += [Phonelib::Core::FIXED_LINE, Phonelib::Core::MOBILE]
98
109
  end
99
110
  phone_types
100
111
  end
101
112
 
113
+ # @private
114
+ def phone_countries
115
+ method = options[:possible] ? :countries : :valid_countries
116
+ @phone.send(method)
117
+ end
118
+
102
119
  # @private
103
120
  def types
104
121
  types = options[:types].is_a?(Array) ? options[:types] : [options[:types]]
105
122
  types.map(&:to_sym)
106
123
  end
124
+
125
+ # @private
126
+ def countries
127
+ countries = options[:countries].is_a?(Array) ? options[:countries] : [options[:countries]]
128
+ countries.map { |c| c.to_s.upcase }
129
+ end
107
130
  end
@@ -0,0 +1,130 @@
1
+ # Validator class for phone validations
2
+ #
3
+ # ==== Examples
4
+ #
5
+ # Validates that attribute is a valid phone number.
6
+ # If empty value passed for attribute it fails.
7
+ #
8
+ # class Phone < ActiveRecord::Base
9
+ # attr_accessible :number
10
+ # validates :number, phone: true
11
+ # end
12
+ #
13
+ # Validates that attribute is a possible phone number.
14
+ # If empty value passed for attribute it fails.
15
+ #
16
+ # class Phone < ActiveRecord::Base
17
+ # attr_accessible :number
18
+ # validates :number, phone: { possible: true }
19
+ # end
20
+ #
21
+ # Validates that attribute is a valid phone number.
22
+ # Empty value is allowed to be passed.
23
+ #
24
+ # class Phone < ActiveRecord::Base
25
+ # attr_accessible :number
26
+ # validates :number, phone: { allow_blank: true }
27
+ # end
28
+ #
29
+ # Validates that attribute is a valid phone number of specified type(s).
30
+ # It is also possible to check that attribute is a possible number of specified
31
+ # type(s). Symbol or array accepted.
32
+ #
33
+ # class Phone < ActiveRecord::Base
34
+ # attr_accessible :number, :mobile
35
+ # validates :number, phone: { types: [:mobile, :fixed], allow_blank: true }
36
+ # validates :mobile, phone: { possible: true, types: :mobile }
37
+ # end
38
+ #
39
+ # validates that phone is valid and it is from specified country or countries
40
+ #
41
+ # class Phone < ActiveRecord::Base
42
+ # attr_accessible :number
43
+ # validates :number, phone: { countries: [:us, :ca] }
44
+ # end
45
+ #
46
+ # Validates that attribute does not include an extension.
47
+ # The default setting is to allow extensions
48
+ #
49
+ # class Phone < ActiveRecord::Base
50
+ # attr_accessible :number
51
+ # validates :number, phone: { extensions: false }
52
+ # end
53
+ #
54
+ class PhoneValidator < ActiveModel::EachValidator
55
+ # Include all core methods
56
+ include Phonelib::Core
57
+
58
+ # Validation method
59
+ def validate_each(record, attribute, value)
60
+ return if options[:allow_blank] && value.blank?
61
+
62
+ @phone = parse(value, specified_country(record))
63
+ valid = phone_valid? && valid_types? && valid_country? && valid_extensions?
64
+
65
+ record.errors.add(attribute, message, **options) unless valid
66
+ end
67
+
68
+ private
69
+
70
+ def message
71
+ options[:message] || :invalid
72
+ end
73
+
74
+ def phone_valid?
75
+ @phone.send(options[:possible] ? :possible? : :valid?)
76
+ end
77
+
78
+ def valid_types?
79
+ return true unless options[:types]
80
+ (phone_types & types).size > 0
81
+ end
82
+
83
+ def valid_country?
84
+ return true unless options[:countries]
85
+ (phone_countries & countries).size > 0
86
+ end
87
+
88
+ def valid_extensions?
89
+ return true if !options.has_key?(:extensions) || options[:extensions]
90
+ @phone.extension.empty?
91
+ end
92
+
93
+ def specified_country(record)
94
+ return unless options[:country_specifier]
95
+
96
+ if options[:country_specifier].is_a?(Symbol)
97
+ record.send(options[:country_specifier])
98
+ else
99
+ options[:country_specifier].call(record)
100
+ end
101
+ end
102
+
103
+ # @private
104
+ def phone_types
105
+ method = options[:possible] ? :possible_types : :types
106
+ phone_types = @phone.send(method)
107
+ if (phone_types & [Phonelib::Core::FIXED_OR_MOBILE]).size > 0
108
+ phone_types += [Phonelib::Core::FIXED_LINE, Phonelib::Core::MOBILE]
109
+ end
110
+ phone_types
111
+ end
112
+
113
+ # @private
114
+ def phone_countries
115
+ method = options[:possible] ? :countries : :valid_countries
116
+ @phone.send(method)
117
+ end
118
+
119
+ # @private
120
+ def types
121
+ types = options[:types].is_a?(Array) ? options[:types] : [options[:types]]
122
+ types.map(&:to_sym)
123
+ end
124
+
125
+ # @private
126
+ def countries
127
+ countries = options[:countries].is_a?(Array) ? options[:countries] : [options[:countries]]
128
+ countries.map { |c| c.to_s.upcase }
129
+ end
130
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phonelib
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.21
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vadim Senderovich
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-12 00:00:00.000000000 Z
11
+ date: 2022-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "<"
18
18
  - !ruby/object:Gem::Version
19
- version: '11.0'
19
+ version: '14.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "<"
25
25
  - !ruby/object:Gem::Version
26
- version: '11.0'
26
+ version: '14.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: nokogiri
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 1.8.1
33
+ version: 1.13.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 1.8.1
40
+ version: 1.13.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: pry
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -114,14 +114,14 @@ dependencies:
114
114
  requirements:
115
115
  - - '='
116
116
  - !ruby/object:Gem::Version
117
- version: 1.8.6
117
+ version: 2.3.1
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - '='
123
123
  - !ruby/object:Gem::Version
124
- version: 1.8.6
124
+ version: 2.3.1
125
125
  description: |2
126
126
  Google libphonenumber library was taken as a basis for
127
127
  this gem. Gem uses its data file for validations and number formatting.
@@ -148,6 +148,7 @@ files:
148
148
  - lib/phonelib/version.rb
149
149
  - lib/tasks/phonelib_tasks.rake
150
150
  - lib/validators/phone_validator.rb
151
+ - lib/validators/phone_validator3.rb
151
152
  homepage: https://github.com/daddyz/phonelib
152
153
  licenses:
153
154
  - MIT
@@ -168,8 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
169
  - !ruby/object:Gem::Version
169
170
  version: '0'
170
171
  requirements: []
171
- rubyforge_project:
172
- rubygems_version: 2.6.11
172
+ rubygems_version: 3.0.8
173
173
  signing_key:
174
174
  specification_version: 4
175
175
  summary: Gem validates phone numbers with Google libphonenumber database