phonie 2.1.2 → 3.0.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.
data/.travis.yml CHANGED
@@ -4,12 +4,10 @@ before_install: gem install bundler
4
4
  before_script: export RUBY_GC_MALLOC_LIMIT=90000000
5
5
 
6
6
  rvm:
7
- - 1.8.7
8
- - 1.9.2
9
7
  - 1.9.3
10
8
  - 2.0.0
11
- - rbx-18mode
12
- - rbx-19mode
13
- - ruby-head
9
+ - 2.1.0
10
+ - jruby-19mode
11
+ - rbx-2.2.3
14
12
 
15
13
  script: rake
data/Changelog.md ADDED
@@ -0,0 +1,14 @@
1
+ 3.0.0
2
+ =====
3
+
4
+ * ActiveModel validator no longer modifies phone numbers on validation. Use a before_save to do that!
5
+ * configuration changes from Phonie::Phone.default_country_code to Phonie.configuration.default_country_code
6
+ * adds Phone#possible_valid_number? and Phone#is_valid_number?
7
+ * removes Phone#matches_full_number? Phone#matches_local_number_with_area_code? and Phone#matches_local_number?
8
+
9
+ 2.1.2
10
+ =====
11
+
12
+ * fixes Mauritius parsing
13
+ * Support new Ecuador mobile numbers
14
+ * fix for Russian mobile number detection
data/Readme.rdoc CHANGED
@@ -3,7 +3,6 @@ Parsing, validating and creating phone numbers
3
3
 
4
4
  == Install
5
5
  You can install the phone library as a gem
6
- gem sources -a http://gemcutter.org
7
6
  gem install phonie
8
7
 
9
8
  == Initializing
@@ -11,7 +10,7 @@ You can initialize a new phone object with the number, area code, country code a
11
10
 
12
11
  Phonie::Phone.new('5125486', '91', '385')
13
12
  or
14
- Phonie::Phone.new(:number => '5125486', :area_code => '91', :country_code => '385', :extension => '143')
13
+ Phonie::Phone.new(number: '5125486', area_code: '91', country_code: '385', extension: '143')
15
14
 
16
15
  == Parsing
17
16
  You can create a new phone object by parsing from a string. Phonie::Phone does it's best to detect the country and area codes:
@@ -19,31 +18,29 @@ You can create a new phone object by parsing from a string. Phonie::Phone does i
19
18
  Phonie::Phone.parse '00385915125486'
20
19
 
21
20
  If the country or area code isn't given in the string, you must set it, otherwise it doesn't work:
22
- Phonie::Phone.parse '091/512-5486', :country_code => '385'
23
- Phonie::Phone.parse '(091) 512 5486', :country_code => '385'
21
+ Phonie::Phone.parse '091/512-5486', country_code: '385'
22
+ Phonie::Phone.parse '(091) 512 5486', country_code: '385'
24
23
 
25
24
  If you feel that it's tedious, set the default country code once (in your config/environment.rb):
26
- Phonie::Phone.default_country_code = '385'
25
+ Phonie.configuration.default_country_code = '385'
27
26
  Phonie::Phone.parse '091/512-5486'
28
27
  Phonie::Phone.parse '(091) 512 5486'
29
28
 
30
29
  Same goes for the area code:
31
- Phonie::Phone.parse '451-588', :country_code => '385', :area_code => '47'
32
- or
33
- Phonie::Phone.default_country_code = '385'
34
- Phonie::Phone.default_area_code = '47'
30
+ Phonie::Phone.parse '451-588', country_code: '385', area_code: '47'
31
+
32
+ Alternatively Phonie can be configured via a block
33
+ Phonie.configure do |config|
34
+ config.default_country_code = '385'
35
+ config.default_area_code = '47'
36
+ end
35
37
 
36
38
  Phonie::Phone.parse '451-588'
37
39
 
38
40
  === Automatic country and area code detection
39
- Like it's stated above, Phone does it's best to automatically detect the country and area code while parsing. Do do this,
40
- phone uses data stored in <tt>data/countries.yml</tt>.
41
-
42
- Each country code can have a regular expression named <tt>area_code</tt> that describes how the area code for that
43
- particular country looks like.
41
+ Like it's stated above, Phone does it's best to automatically detect the country and area code while parsing. To do this, phone uses data stored in <tt>data/countries.yml</tt>.
44
42
 
45
- If an <tt>area_code</tt> regular expression isn't specified, the default, <tt>Phonie::Phone::DEFAULT_AREA_CODE</tt> (correct for
46
- the US) is used.
43
+ Each country code can have a regular expression named <tt>area_code</tt> that describes how the area code for that particular country looks like.
47
44
 
48
45
  == Validating
49
46
  Validating is very relaxed, basically it strips out everything that's not a number or '+' character:
@@ -58,7 +55,7 @@ When given a string, it interpolates the string with the following fields:
58
55
  * %a - area_code (91)
59
56
  * %A - area_code with leading zero (091)
60
57
  * %n - number (5125486)
61
- * %f - first @@n1_length characters of number (configured through Phonie::Phone.n1_length), default is 3 (512)
58
+ * %f - first @@n1_length characters of number (configured through Phonie.n1_length), default is 3 (512)
62
59
  * %l - last characters of number (5486)
63
60
  * %x - the extension number
64
61
 
@@ -76,8 +73,25 @@ You can add your own custom named formats like so:
76
73
  Phonie::Phone.named_formats[:short] = '%A/%n1-%n2'
77
74
  pn.format(:short) # => 091/512-5486
78
75
 
76
+ == ActiveModel validator
77
+
78
+ Phonie includes an ActiveModel validator. If you are using ActiveModel you can validate phone numbers like so:
79
+
80
+ class SomeModel
81
+ include ActiveModel::Validations
82
+
83
+ validates :phone_number, phone: true
84
+ end
85
+
86
+ model = SomeModel.new(phone_number: '')
87
+ model.valid? # false
88
+
89
+ model = SomeModel.new(phone_number: '+1 251 123 4567')
90
+ model.valid? # true
91
+
92
+
79
93
  = TODO
80
- Parse testing for different countries.
94
+ Add definitions for more countries
81
95
 
82
96
  Currently tested on:
83
97
  [AE] UAE
@@ -176,9 +190,7 @@ More testing is needed to add support for missing countries, and improve support
176
190
  The best places to start is to read through the country tests and data/phone_countries.rb
177
191
 
178
192
  = Other libraries
179
- This is based off a fork of the Phone gem (https://github.com/carr/phone), and was extensively modified for better support of country detection
180
-
181
- Another fork based off this one is available at https://github.com/capitainetrain/phoner It explictly requires ActiveSupport.
193
+ This is based off a fork of the Phone gem (https://github.com/carr/phone), and was extensively modified for better support of country detection, and supports far more countries.
182
194
 
183
195
  = Contributors
184
196
  Tomislav Carr, Don Morrison, Michael Squires, Todd Eichel (Fooala, Inc.), chipiga, Etienne Samson, Luke Randall, Wesley Moxam
data/lib/phonie.rb CHANGED
@@ -1,10 +1,7 @@
1
1
  require "phonie/version"
2
- require "phonie/support" unless defined? ActiveSupport
2
+ require "phonie/configuration"
3
+ require "phonie/formatter"
3
4
  require "phonie/phone"
5
+ require "phonie/parser"
4
6
  require "phonie/country"
5
- require "phonie/railties/validator"
6
-
7
- module Phonie
8
- end
9
-
10
- Phonie::Country.load
7
+ require "phonie/railties/validator" if defined? ActiveModel
@@ -0,0 +1,22 @@
1
+ require 'singleton'
2
+
3
+ module Phonie
4
+ class Configuration
5
+ include Singleton
6
+
7
+ attr_accessor :data_file_path, :default_area_code, :default_country_code, :n1_length
8
+
9
+ def initialize
10
+ @data_file_path = File.join(File.dirname(__FILE__), 'data', 'phone_countries.yml')
11
+ @n1_length = 3
12
+ end
13
+ end
14
+
15
+ def self.configure(&block)
16
+ yield configuration
17
+ end
18
+
19
+ def self.configuration
20
+ Configuration.instance
21
+ end
22
+ end
@@ -1,115 +1,79 @@
1
+ require 'forwardable'
2
+ require 'yaml'
3
+
1
4
  module Phonie
2
- class Country < Struct.new(:name, :country_code, :char_2_code, :iso_3166_code, :area_code, :local_number_format, :mobile_format, :full_number_length, :number_format, :national_dialing_prefix)
3
- def self.load
4
- data_file = File.join(File.dirname(__FILE__), 'data', 'phone_countries.yml')
5
-
6
- all = []
7
- YAML.load(File.read(data_file)).each do |c|
8
- next unless c[:area_code] && c[:local_number_format]
9
- all << Country.new(c[:name], c[:country_code], c[:char_2_code], c[:iso_3166_code], c[:area_code], c[:local_number_format], c[:mobile_format], c[:full_number_length], c[:number_format], c[:national_dialing_prefix])
5
+ class Country
6
+ extend Forwardable
7
+
8
+ attr_reader :name, :country_code, :char_2_code, :iso_3166_code, :parser
9
+
10
+ def initialize(params)
11
+ @name = params[:name]
12
+ @country_code = params[:country_code]
13
+ @char_2_code = params[:char_2_code]
14
+ @iso_3166_code = params[:iso_3166_code]
15
+ @parser = Phonie::Parser.new(params)
16
+ @national_dialing_prefix = params[:national_dialing_prefix]
17
+ end
18
+
19
+ def_delegators :parser, :is_mobile?, :possible_valid_number?,
20
+ :is_valid_number?, :parse
21
+
22
+ def self.all
23
+ @@all ||= begin
24
+ YAML.load_file(Phonie.configuration.data_file_path).collect do |country_params|
25
+ Country.new(country_params)
26
+ end.select {|country| country.valid? }
10
27
  end
11
- all
12
28
  end
13
29
 
14
- COUNTRIES = self.load
15
- COUNTRIES_BY_PHONE_CODE = COUNTRIES.inject(Hash.new){|h, c| (h[c.country_code] ||= []) << c; h }
16
- COUNTRIES_BY_COUNTRY_CODE = Hash[*COUNTRIES.map{|c| [c.iso_3166_code.downcase, c] }.flatten]
17
- COUNTRIES_BY_NAME = Hash[*COUNTRIES.map{|c| [c.name.downcase, c] }.flatten]
30
+ def self.all_by_phone_code
31
+ @@all_by_phone_code ||= all.inject(Hash.new){|h, c| (h[c.country_code] ||= []) << c; h }
32
+ end
33
+
34
+ def self.all_by_country_code
35
+ @@all_by_country_name ||= Hash[*all.map{|c| [c.iso_3166_code.downcase, c] }.flatten]
36
+ end
37
+
38
+ def self.all_by_name
39
+ @@all_by_name ||= Hash[*all.map{|c| [c.name.downcase, c] }.flatten]
40
+ end
18
41
 
19
42
  def self.find_all_by_phone_code(code)
20
- COUNTRIES_BY_PHONE_CODE[code] || []
43
+ all_by_phone_code[code] || []
21
44
  end
22
45
 
23
46
  def self.find_by_country_code(code)
24
- COUNTRIES_BY_COUNTRY_CODE[code.downcase] if code
47
+ all_by_country_code[code.downcase] if code
25
48
  end
26
49
 
27
50
  def self.find_by_name(name)
28
- COUNTRIES_BY_NAME[name.downcase] if name
51
+ all_by_name[name.downcase] if name
29
52
  end
30
53
 
31
- # detect country from the string entered
32
- def self.detect(string, default_country_code, default_area_code)
54
+ # detect country from the passed phone number
55
+ def self.detect(phone_number, default_country_code, default_area_code)
33
56
  # use the default_country_code to try for a quick match
34
- country = find_all_by_phone_code(default_country_code).find do |country|
35
- country.matches_full_number?(string) ||
36
- country.matches_local_number_with_area_code?(string) ||
37
- country.matches_local_number?(string, default_area_code)
57
+ country = find_all_by_phone_code(default_country_code).find do |c|
58
+ c.possible_valid_number?(phone_number, default_area_code)
38
59
  end
39
60
 
40
61
  # then search all for a full match
41
- country || COUNTRIES.find {|country| country.matches_full_number?(string) }
62
+ country || all.find {|country| country.is_valid_number?(phone_number) }
42
63
  end
43
64
 
44
65
  def to_s
45
66
  name
46
67
  end
47
68
 
48
- def is_mobile?(number)
49
- return true if mobile_format.nil?
50
- number =~ mobile_number_regex ? true : false
51
- end
52
-
53
- # true if string contains country_code + area_code + local_number
54
- def matches_full_number?(string)
55
- string =~ full_number_regex && string =~ number_format_regex
56
- end
57
-
58
- # true if string contains area_code + local_number
59
- def matches_local_number_with_area_code?(string)
60
- string =~ area_code_number_regex && string =~ number_format_regex
61
- end
62
-
63
- # true if string contains only the local_number, but the default_area_code is valid
64
- def matches_local_number?(string, default_area_code)
65
- string =~ number_regex && default_area_code =~ area_code_regex
66
- end
67
-
68
- def parse(number, default_area_code)
69
- if md = number.match(full_number_regex)
70
- {:area_code => md[2], :number => md[-1]}
71
- elsif md = number.match(area_code_number_regex)
72
- {:area_code => md[1], :number => md[-1]}
73
- elsif md = number.match(number_regex)
74
- {:area_code => default_area_code, :number => md[1]}
75
- else
76
- {}
77
- end
78
- end
79
-
80
69
  def national_dialing_prefix
81
- prefix = super
82
- if prefix == "None"
83
- nil
84
- else
85
- prefix
86
- end
87
- end
88
-
89
- private
90
-
91
- def number_format_regex
92
- @number_format_regex ||= Regexp.new("^[+0]?(#{country_code})?(#{number_format})$")
93
- end
94
-
95
- def full_number_regex
96
- @full_number_regex ||= Regexp.new("^[+]?(#{country_code})(#{area_code})(#{local_number_format})$")
97
- end
98
-
99
- def area_code_number_regex
100
- @area_code_number_regex ||= Regexp.new("^0?(#{area_code})(#{local_number_format})$")
101
- end
102
-
103
- def area_code_regex
104
- @area_code_regex ||= Regexp.new("^0?(#{area_code})$")
105
- end
70
+ return nil if @national_dialing_prefix == "None"
106
71
 
107
- def mobile_number_regex
108
- @mobile_number_regex ||= Regexp.new("^(#{mobile_format})$")
72
+ @national_dialing_prefix
109
73
  end
110
74
 
111
- def number_regex
112
- @number_regex ||= Regexp.new("^(#{local_number_format})$")
75
+ def valid?
76
+ !!(name && parser.valid?)
113
77
  end
114
78
  end
115
79
  end
@@ -0,0 +1,54 @@
1
+ # Formats the phone number.
2
+ #
3
+ # if the method argument is a String, it is used as a format string, with the following fields being interpolated:
4
+ #
5
+ # * %c - country_code (385)
6
+ # * %a - area_code (91)
7
+ # * %A - area_code with leading zero (091)
8
+ # * %n - number (5125486)
9
+ # * %f - first n1_length characters of number (configured through Phone.n1_length), default is 3 (512)
10
+ # * %l - last characters of number (5486)
11
+ # * %x - entire extension
12
+ #
13
+ # if the method argument is a Symbol, it is used as a lookup key for a format String in Phone.named_formats
14
+ # pn.format(:europe)
15
+ module Phonie
16
+ class Formatter
17
+ attr_reader :format, :phone_number
18
+
19
+ def initialize(params)
20
+ @phone_number = params[:phone_number]
21
+
22
+ format = params[:format]
23
+ @format = if format.respond_to?(:gsub)
24
+ format
25
+ else
26
+ self.class.named_formats[format]
27
+ end
28
+
29
+ raise ArgumentError.new("No valid format provided") if @format.nil?
30
+ raise ArgumentError.new("No valid phone number provided") if @phone_number.nil?
31
+ end
32
+
33
+ def self.named_formats
34
+ {
35
+ default: "+%c%a%n",
36
+ default_with_extension: "+%c%a%nx%x",
37
+ europe: '+%c (0) %a %f %l',
38
+ us: "(%a) %f-%l"
39
+ }
40
+ end
41
+
42
+ def to_s
43
+ pn = phone_number
44
+
45
+ format.gsub("%c", pn.country_code.to_s).
46
+ gsub("%a", pn.area_code.to_s).
47
+ gsub("%A", pn.area_code_long.to_s).
48
+ gsub("%n", pn.number.to_s).
49
+ gsub("%f", pn.number1.to_s).
50
+ gsub("%l", pn.number2.to_s).
51
+ gsub("%x", pn.extension.to_s)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,125 @@
1
+ module Phonie
2
+ # Parser is responsible for parsing and verifying phone numbers
3
+ class Parser
4
+ attr_reader :area_code, :country_code, :local_number_format, :mobile_format, :number_format
5
+
6
+ # Creates a new phone number parser based on a hash of +params+
7
+ # The +params+ hash requires
8
+ # * area_code
9
+ # * country_code - a number representing the International Subscriber Dialling code
10
+ # * local_number_format - a regex describing a local phone number (minus country and area code)
11
+ # * number_format - a regex describing a valid phone number
12
+ # * mobile_format (optional) - a regex describing a mobile phone number
13
+ def initialize(params)
14
+ @area_code = params[:area_code]
15
+ @country_code = params[:country_code]
16
+ @local_number_format = params[:local_number_format]
17
+ @mobile_format = params[:mobile_format]
18
+ @number_format = params[:number_format]
19
+ end
20
+
21
+ # Test if a phone +number+ might be for a mobile phone
22
+ # Can produce false positives, and some countries have
23
+ # portable numbers between landlines and mobile phones
24
+ # in which case this will always be true
25
+ def is_mobile?(number)
26
+ return true if mobile_format.nil?
27
+ number =~ mobile_number_regex ? true : false
28
+ end
29
+
30
+ # Test if +phone_number+ is valid
31
+ def is_valid_number?(phone_number)
32
+ matches_full_number?(phone_number)
33
+ end
34
+
35
+ # Parse a pass phone +number+ returning it's area_code & number as a
36
+ # hash if valid.
37
+ # Optionally a +default_area_code+ may be passed in order to
38
+ # allow parsing local number numbers for a known area code
39
+ def parse(number, default_area_code = nil)
40
+ parse_full_match(number) ||
41
+ parse_area_code_match(number) ||
42
+ parse_with_default(number, default_area_code) ||
43
+ {}
44
+ end
45
+
46
+ # Test if a phone number could possibly be valid by testing
47
+ # if it's matches a number without a country code, and optionally
48
+ # testing for a local number against a default_area_code
49
+ def possible_valid_number?(phone_number, default_area_code = nil)
50
+ matches_full_number?(phone_number) ||
51
+ matches_local_number_with_area_code?(phone_number) ||
52
+ matches_local_number?(phone_number, default_area_code)
53
+ end
54
+
55
+ # Test that a parser is valid and is capable of parsing phone numbers
56
+ def valid?
57
+ !!(country_code && area_code && local_number_format && number_format)
58
+ end
59
+
60
+ private
61
+
62
+ # true if string contains country_code + area_code + local_number
63
+ def matches_full_number?(string)
64
+ string =~ full_number_regex && string =~ number_format_regex
65
+ end
66
+
67
+ # true if string contains area_code + local_number
68
+ def matches_local_number_with_area_code?(string)
69
+ string =~ area_code_number_regex && string =~ number_format_regex
70
+ end
71
+
72
+ # true if string contains only the local_number, but the default_area_code is valid
73
+ def matches_local_number?(string, default_area_code)
74
+ string =~ number_regex && default_area_code =~ area_code_regex
75
+ end
76
+
77
+ def parse_full_match(number)
78
+ match = number.match(full_number_regex)
79
+ return nil unless match
80
+
81
+ { area_code: match[2],
82
+ number: match[-1] }
83
+ end
84
+
85
+ def parse_area_code_match(number)
86
+ match = number.match(area_code_number_regex)
87
+ return nil unless match
88
+
89
+ { area_code: match[1],
90
+ number: match[-1] }
91
+ end
92
+
93
+ def parse_with_default(number, default_area_code)
94
+ match = number.match(number_regex)
95
+ return nil unless match
96
+
97
+ { area_code: default_area_code,
98
+ number: match[1] }
99
+ end
100
+
101
+ def number_format_regex
102
+ @number_format_regex ||= Regexp.new("^[+0]?(#{country_code})?(#{number_format})$")
103
+ end
104
+
105
+ def full_number_regex
106
+ @full_number_regex ||= Regexp.new("^[+]?(#{country_code})(#{area_code})(#{local_number_format})$")
107
+ end
108
+
109
+ def area_code_number_regex
110
+ @area_code_number_regex ||= Regexp.new("^0?(#{area_code})(#{local_number_format})$")
111
+ end
112
+
113
+ def area_code_regex
114
+ @area_code_regex ||= Regexp.new("^0?(#{area_code})$")
115
+ end
116
+
117
+ def mobile_number_regex
118
+ @mobile_number_regex ||= Regexp.new("^(#{mobile_format})$")
119
+ end
120
+
121
+ def number_regex
122
+ @number_regex ||= Regexp.new("^(#{local_number_format})$")
123
+ end
124
+ end
125
+ end
data/lib/phonie/phone.rb CHANGED
@@ -1,5 +1,3 @@
1
- require 'active_model'
2
-
3
1
  # An object representing a phone number.
4
2
  #
5
3
  # The phone number is recorded in 3 separate parts:
@@ -8,64 +6,40 @@ require 'active_model'
8
6
  # * number - e.g. '5125486', '451588'
9
7
  #
10
8
  # All parts are mandatory, but country code and area code can be set for all phone numbers using
11
- # Phone.default_country_code
12
- # Phone.default_area_code
9
+ # Phonie.configuration.default_country_code
10
+ # Phonie.configuration.default_area_code
13
11
  #
12
+
14
13
  module Phonie
15
14
  class Phone
16
15
  EXTENSION = /[ ]*(ext|ex|x|xt|#|:)+[^0-9]*\(*([-0-9]{1,})\)*#?$/i
17
16
 
18
- attr_accessor :country_code, :area_code, :number, :extension, :country
19
-
20
- cattr_accessor :default_country_code
21
- cattr_accessor :default_area_code
22
- cattr_accessor :named_formats
23
-
24
- # length of first number part (using multi number format)
25
- cattr_accessor :n1_length
26
- # default length of first number part
27
- @@n1_length = 3
28
-
29
- @@named_formats = {
30
- :default => "+%c%a%n",
31
- :default_with_extension => "+%c%a%nx%x",
32
- :europe => '+%c (0) %a %f %l',
33
- :us => "(%a) %f-%l"
34
- }
35
-
36
- include ActiveModel::Validations
37
- validates :country_code, :presence => true
38
- validates :area_code, :presence => true
39
- validates :number, :presence => true
40
-
41
- def initialize(*hash_or_args)
42
- if hash_or_args.first.is_a?(Hash)
43
- hash_or_args = hash_or_args.first
44
- keys = {:country => :country, :number => :number, :area_code => :area_code, :country_code => :country_code, :extension => :extension}
45
- else
46
- keys = {:number => 0, :area_code => 1, :country_code => 2, :extension => 3, :country => 4}
47
- end
17
+ attr_reader :country_code, :area_code, :errors, :number, :extension, :country
18
+
19
+ def initialize(*args)
20
+ params = normalize_args(args)
48
21
 
49
- self.number = hash_or_args[ keys[:number] ]
50
- self.area_code = hash_or_args[ keys[:area_code] ] || self.default_area_code
51
- self.country_code = hash_or_args[ keys[:country_code] ] || self.default_country_code
52
- self.extension = hash_or_args[ keys[:extension] ]
53
- self.country = hash_or_args[ keys[:country] ]
22
+ @number = params[:number]
23
+ @area_code = params[:area_code]
24
+ @country_code = params[:country_code]
25
+ @extension = params[:extension]
26
+ @country = params[:country]
27
+ @errors = {}
54
28
  end
55
29
 
56
30
  def self.parse!(string, options = {})
57
- pn = parse(string, options)
58
- raise ArgumentError.new("#{string} is not a valid phone number") unless pn && pn.valid?
59
- pn
31
+ phone_number = parse(string, options)
32
+ raise ArgumentError.new("#{string} is not a valid phone number") unless phone_number && phone_number.valid?
33
+ phone_number
60
34
  end
61
35
 
62
36
  # create a new phone number by parsing a string
63
37
  # the format of the string is detect automatically (from FORMATS)
64
38
  def self.parse(string, options = {})
65
- return unless string.present?
39
+ return if string.nil?
66
40
 
67
- options[:country_code] ||= self.default_country_code
68
- options[:area_code] ||= self.default_area_code
41
+ options[:country_code] ||= Phonie.configuration.default_country_code
42
+ options[:area_code] ||= Phonie.configuration.default_area_code
69
43
 
70
44
  extension = extract_extension(string)
71
45
  normalized = normalize(string)
@@ -101,36 +75,17 @@ module Phonie
101
75
 
102
76
  # first n characters of :number
103
77
  def number1
104
- number[0...self.class.n1_length]
78
+ number[0...Phonie.configuration.n1_length]
105
79
  end
106
80
 
107
81
  # everything left from number after the first n characters (see number1)
108
82
  def number2
109
- n2_length = number.size - self.class.n1_length
83
+ n2_length = number.size - Phonie.configuration.n1_length
110
84
  number[-n2_length, n2_length]
111
85
  end
112
86
 
113
- # Formats the phone number.
114
- #
115
- # if the method argument is a String, it is used as a format string, with the following fields being interpolated:
116
- #
117
- # * %c - country_code (385)
118
- # * %a - area_code (91)
119
- # * %A - area_code with leading zero (091)
120
- # * %n - number (5125486)
121
- # * %f - first @@n1_length characters of number (configured through Phone.n1_length), default is 3 (512)
122
- # * %l - last characters of number (5486)
123
- # * %x - entire extension
124
- #
125
- # if the method argument is a Symbol, it is used as a lookup key for a format String in Phone.named_formats
126
- # pn.format(:europe)
127
87
  def format(fmt)
128
- if fmt.is_a?(Symbol)
129
- raise ArgumentError.new("The format #{fmt} doesn't exist") unless named_formats.has_key?(fmt)
130
- format_number named_formats[fmt]
131
- else
132
- format_number(fmt)
133
- end
88
+ Formatter.new(format: fmt, phone_number: self).to_s
134
89
  end
135
90
 
136
91
  # the default format is "+%c%a%n"
@@ -140,12 +95,17 @@ module Phonie
140
95
 
141
96
  # does this number belong to the default country code?
142
97
  def has_default_country_code?
143
- country_code == self.class.default_country_code
98
+ country_code == Phonie.configuration.default_country_code
144
99
  end
145
100
 
146
101
  # does this number belong to the default area code?
147
102
  def has_default_area_code?
148
- area_code == self.class.default_area_code
103
+ area_code == Phonie.configuration.default_area_code
104
+ end
105
+
106
+ def valid?
107
+ validate
108
+ errors.empty?
149
109
  end
150
110
 
151
111
  # comparison of 2 phone objects
@@ -156,6 +116,33 @@ module Phonie
156
116
 
157
117
  private
158
118
 
119
+ def defaults
120
+ { area_code: Phonie.configuration.default_area_code,
121
+ country_code: Phonie.configuration.default_country_code }
122
+ end
123
+
124
+ def normalize_args(args)
125
+ hash = if args.first.respond_to?(:key?)
126
+ args.first
127
+ else
128
+ {}.tap do |h|
129
+ h[:number] = args[0] if args.length > 0
130
+ h[:area_code] = args[1] if args.length > 1
131
+ h[:country_code] = args[2] if args.length > 2
132
+ h[:extension] = args[3] if args.length > 3
133
+ h[:country] = args[4] if args.length > 4
134
+ end
135
+ end
136
+
137
+ defaults.merge(hash)
138
+ end
139
+
140
+ def validate
141
+ [:country_code, :area_code, :number].each do |field|
142
+ errors[field] = ["can't be blank"] if send(field).to_s == ''
143
+ end
144
+ end
145
+
159
146
  # split string into hash with keys :country_code, :area_code and :number
160
147
  def self.split_to_parts(string, options = {})
161
148
  country = Country.detect(string, options[:country_code], options[:area_code])
@@ -172,15 +159,5 @@ module Phonie
172
159
  return unless string && string.match(EXTENSION)
173
160
  Regexp.last_match[2]
174
161
  end
175
-
176
- def format_number(fmt)
177
- fmt.gsub("%c", country_code || "").
178
- gsub("%a", area_code || "").
179
- gsub("%A", area_code_long || "").
180
- gsub("%n", number || "").
181
- gsub("%f", number1 || "").
182
- gsub("%l", number2 || "").
183
- gsub("%x", extension || "")
184
- end
185
162
  end
186
163
  end
@@ -2,14 +2,6 @@ I18n.load_path += Dir.glob( File.expand_path('../locales/*.{rb,yml}', __FILE__)
2
2
 
3
3
  class PhoneValidator < ActiveModel::EachValidator
4
4
  def validate_each(object, attribute, value)
5
- return if value.blank?
6
- phone = Phonie::Phone.parse(value)
7
-
8
- if phone.nil?
9
- object.errors.add(attribute, :invalid_phone_number)
10
- else
11
- formated = phone.format( phone.extension ? :default_with_extension : :default)
12
- object.send("#{attribute}=", formated) if object.respond_to?("#{attribute}=")
13
- end
5
+ object.errors.add(attribute, :invalid_phone_number) unless Phonie::Phone.valid?(value)
14
6
  end
15
- end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Phonie
2
- VERSION = '2.1.2'
2
+ VERSION = '3.0.0'
3
3
  end
data/phonie.gemspec CHANGED
@@ -16,8 +16,7 @@ Gem::Specification.new do |s|
16
16
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
17
  s.require_paths = ["lib"]
18
18
 
19
- s.add_dependency 'activemodel'
20
-
19
+ s.add_development_dependency 'activemodel'
21
20
  s.add_development_dependency 'rake'
22
21
  s.add_development_dependency 'nokogiri'
23
22
  end
@@ -9,7 +9,7 @@ class BDTest < Phonie::TestCase
9
9
  end
10
10
 
11
11
  def test_with_default_country
12
- Phonie::Phone.default_country_code = '880'
12
+ Phonie.configuration.default_country_code = '880'
13
13
  parse_test('1715379982', '880', '171', '5379982', 'Bangladesh', true)
14
14
  parse_test('1191573647', '880', '119', '1573647', 'Bangladesh', true)
15
15
  parse_test('21915736', '880', '2', '1915736', 'Bangladesh', false)
@@ -8,13 +8,13 @@ class CATest < Phonie::TestCase
8
8
  end
9
9
 
10
10
  def test_long_with_default_country_code
11
- Phonie::Phone.default_country_code = '1'
11
+ Phonie.configuration.default_country_code = '1'
12
12
  parse_test('9059735100', '1', '905', '9735100', 'Canada')
13
13
  end
14
14
 
15
15
  def test_short_with_default_country_code_and_area_code
16
- Phonie::Phone.default_country_code = '1'
17
- Phonie::Phone.default_area_code = '416'
16
+ Phonie.configuration.default_country_code = '1'
17
+ Phonie.configuration.default_area_code = '416'
18
18
  parse_test('9735100', '1', '416', '9735100', 'Canada')
19
19
  end
20
20
  end
@@ -29,43 +29,42 @@ class HRTest < Phonie::TestCase
29
29
  end
30
30
 
31
31
  def test_short_without_special_characters_with_country
32
- Phonie::Phone.default_country_code = '385'
32
+ Phonie.configuration.default_country_code = '385'
33
33
  parse_test('044885047', '385', '44', '885047')
34
34
  end
35
35
 
36
36
  def test_zagreb_short_without_special_characters_with_country
37
- Phonie::Phone.default_country_code = '385'
37
+ Phonie.configuration.default_country_code = '385'
38
38
  parse_test('013668734', '385', '1', '3668734')
39
39
  end
40
40
 
41
41
  def test_long_with_zero_in_brackets
42
- Phonie::Phone.default_country_code = nil
42
+ Phonie.configuration.default_country_code = nil
43
43
  parse_test('+385 (0)1 366 8111', '385', '1', '3668111')
44
44
  end
45
45
 
46
46
  def test_has_default_country_code
47
- Phonie::Phone.default_country_code = '385'
47
+ Phonie.configuration.default_country_code = '385'
48
48
 
49
49
  assert_equal Phonie::Phone.parse('+38547451588').has_default_country_code?, true
50
50
  assert_equal Phonie::Phone.parse('+16472228242').has_default_country_code?, false
51
51
  end
52
52
 
53
53
  def test_has_default_area_code
54
- Phonie::Phone.default_country_code = '385'
55
- Phonie::Phone.default_area_code = '47'
54
+ Phonie.configuration.default_country_code = '385'
55
+ Phonie.configuration.default_area_code = '47'
56
56
 
57
57
  assert_equal Phonie::Phone.parse('047/451-588').has_default_area_code?, true
58
58
  assert_equal Phonie::Phone.parse('032/336-1456').has_default_area_code?, false
59
59
  end
60
60
 
61
61
  def test_validates
62
- Phonie::Phone.default_country_code = nil
63
62
  assert_equal Phonie::Phone.valid?('00385915125486'), true
64
63
  assert_equal Phonie::Phone.valid?('+385915125486'), true
65
64
  assert_equal Phonie::Phone.valid?('+385 (91) 512 5486'), true
66
65
  assert_equal Phonie::Phone.valid?('+38547451588'), true
67
66
 
68
- Phonie::Phone.default_country_code = '385'
67
+ Phonie.configuration.default_country_code = '385'
69
68
  assert_equal Phonie::Phone.valid?('0915125486'), true
70
69
  assert_equal Phonie::Phone.valid?('091/512-5486'), true
71
70
  assert_equal Phonie::Phone.valid?('091/512-5486'), true
@@ -14,18 +14,18 @@ class INTest < Phonie::TestCase
14
14
  end
15
15
 
16
16
  def test_long_with_default_country_code
17
- Phonie::Phone.default_country_code = '91'
17
+ Phonie.configuration.default_country_code = '91'
18
18
  parse_test('9124459000', '91', '9124', '459000')
19
19
  end
20
20
 
21
21
  def test_short_with_default_country_code_and_area_code
22
- Phonie::Phone.default_country_code = '91'
23
- Phonie::Phone.default_area_code = '9124'
22
+ Phonie.configuration.default_country_code = '91'
23
+ Phonie.configuration.default_area_code = '9124'
24
24
  parse_test('4529000', '91', '9124', '4529000')
25
25
  end
26
26
 
27
27
  def test_lengths
28
- Phonie::Phone.default_country_code = '91'
28
+ Phonie.configuration.default_country_code = '91'
29
29
 
30
30
  phone = Phonie::Phone.parse("919812344")
31
31
  assert_nil phone
@@ -51,7 +51,7 @@ class PTTest < Phonie::TestCase
51
51
 
52
52
  # 707-708: Premium Numbers
53
53
  def test_707
54
- Phonie::Phone.default_country_code = '351'
54
+ Phonie.configuration.default_country_code = '351'
55
55
  parse_test('707 123 456', '351', '707', '123456')
56
56
  end
57
57
 
@@ -59,17 +59,17 @@ class PTTest < Phonie::TestCase
59
59
 
60
60
  # 800: Numero verde ("Green Number")
61
61
  def test_800
62
- Phonie::Phone.default_country_code = '351'
62
+ Phonie.configuration.default_country_code = '351'
63
63
  parse_test('800 123 456', '351', '800', '123456')
64
64
  end
65
65
  # 808: Numero azul ("Blue Number")
66
66
  def test_808
67
- Phonie::Phone.default_country_code = '351'
67
+ Phonie.configuration.default_country_code = '351'
68
68
  parse_test('808 123 456', '351', '808', '123456')
69
69
  end
70
70
  # 809: Custo partilhado ("Shared cost")
71
71
  def test_809
72
- Phonie::Phone.default_country_code = '351'
72
+ Phonie.configuration.default_country_code = '351'
73
73
  parse_test('809 123 456', '351', '809', '123456', 'Portugal', false)
74
74
  end
75
75
 
@@ -108,14 +108,13 @@ class PTTest < Phonie::TestCase
108
108
  end
109
109
 
110
110
  def test_validates
111
- Phonie::Phone.default_country_code = nil
112
111
  assert_equal Phonie::Phone.valid?('00351211234567'), true
113
112
  assert_equal Phonie::Phone.valid?('00351911234567'), true
114
113
  assert_equal Phonie::Phone.valid?('+351931234567'), true
115
114
  assert_equal Phonie::Phone.valid?('+351 (911) 123 456'), true
116
115
  assert_equal Phonie::Phone.valid?('+351921123456'), true
117
116
 
118
- Phonie::Phone.default_country_code = '351'
117
+ Phonie.configuration.default_country_code = '351'
119
118
  assert_equal Phonie::Phone.valid?('(931) 234-567'), true
120
119
  assert_equal Phonie::Phone.valid?('(211) 234 567'), true
121
120
  assert_equal Phonie::Phone.valid?('232-123-456'), true
@@ -12,13 +12,15 @@ class USTest < Phonie::TestCase
12
12
  end
13
13
 
14
14
  def test_long_with_default_country_code
15
- Phonie::Phone.default_country_code = '1'
15
+ Phonie.configuration.default_country_code = '1'
16
16
  parse_test('2069735100', '1', '206', '9735100', 'United States')
17
17
  end
18
18
 
19
19
  def test_short_with_default_country_code_and_area_code
20
- Phonie::Phone.default_country_code = '1'
21
- Phonie::Phone.default_area_code = '206'
20
+ Phonie.configure do |config|
21
+ config.default_country_code = '1'
22
+ config.default_area_code = '206'
23
+ end
22
24
  parse_test('9735100', '1', '206', '9735100', 'United States')
23
25
  end
24
26
  end
data/test/phone_test.rb CHANGED
@@ -7,16 +7,13 @@ class PhoneTest < Phonie::TestCase
7
7
  end
8
8
 
9
9
  def test_number_without_country_code_initialize
10
- Phonie::Phone.default_country_code = nil
11
-
12
10
  pn = Phonie::Phone.new '5125486', '91'
13
11
  assert !pn.valid?
14
12
  assert_equal ["can't be blank"], pn.errors[:country_code]
15
13
  end
16
14
 
17
15
  def test_number_without_area_code_initialize
18
- Phonie::Phone.default_country_code = '1'
19
- Phonie::Phone.default_area_code = nil
16
+ Phonie.configuration.default_country_code = '1'
20
17
 
21
18
  pn = Phonie::Phone.new '451588'
22
19
  assert !pn.valid?
@@ -24,8 +21,8 @@ class PhoneTest < Phonie::TestCase
24
21
  end
25
22
 
26
23
  def test_number_with_default_area_code_initialize
27
- Phonie::Phone.default_country_code = '385'
28
- Phonie::Phone.default_area_code = '47'
24
+ Phonie.configuration.default_country_code = '385'
25
+ Phonie.configuration.default_area_code = '47'
29
26
 
30
27
  pn = Phonie::Phone.new '451588'
31
28
  assert pn.number == '451588'
@@ -34,7 +31,7 @@ class PhoneTest < Phonie::TestCase
34
31
  end
35
32
 
36
33
  def test_number_with_default_country_code_initialize
37
- Phonie::Phone.default_country_code = '386'
34
+ Phonie.configuration.default_country_code = '386'
38
35
 
39
36
  pn = Phonie::Phone.new '5125486', '91'
40
37
  assert pn.number == '5125486'
@@ -43,7 +40,7 @@ class PhoneTest < Phonie::TestCase
43
40
  end
44
41
 
45
42
  def test_number_with_country_code_initialize
46
- Phonie::Phone.default_country_code = '387'
43
+ Phonie.configuration.default_country_code = '387'
47
44
 
48
45
  pn = Phonie::Phone.new '5125486', '91', '385'
49
46
  assert pn.number == '5125486'
@@ -57,8 +54,6 @@ class PhoneTest < Phonie::TestCase
57
54
  end
58
55
 
59
56
  def test_parse_short_without_special_characters_without_country
60
- Phonie::Phone.default_country_code = nil
61
-
62
57
  assert_nil Phonie::Phone.parse "0915125486"
63
58
 
64
59
  assert_raise ArgumentError do
@@ -67,8 +62,6 @@ class PhoneTest < Phonie::TestCase
67
62
  end
68
63
 
69
64
  def test_parse_short_with_special_characters_without_country
70
- Phonie::Phone.default_country_code = nil
71
-
72
65
  assert_nil Phonie::Phone.parse "091/512-5486"
73
66
 
74
67
  assert_raise ArgumentError do
@@ -77,31 +70,28 @@ class PhoneTest < Phonie::TestCase
77
70
  end
78
71
 
79
72
  def test_to_s
80
- Phonie::Phone.default_country_code = nil
81
73
  pn = Phonie::Phone.new '5125486', '91', '385'
82
74
  assert pn.to_s == '+385915125486'
83
75
  end
84
76
 
85
77
  def test_to_s_without_country_code
86
- Phonie::Phone.default_country_code = '385'
78
+ Phonie.configuration.default_country_code = '385'
87
79
  pn = Phonie::Phone.new '5125486', '91'
88
80
  assert pn.format("0%a%n") == '0915125486'
89
81
  end
90
82
 
91
83
  def test_format_special_with_country_code
92
- Phonie::Phone.default_country_code = nil
93
84
  pn = Phonie::Phone.new '5125486', '91', '385'
94
85
  assert pn.format("+ %c (%a) %n") == '+ 385 (91) 5125486'
95
86
  end
96
87
 
97
88
  def test_format_special_without_country_code
98
- Phonie::Phone.default_country_code = '385'
89
+ Phonie.configuration.default_country_code = '385'
99
90
  pn = Phonie::Phone.new '5125486', '91'
100
91
  assert_equal '091/512-5486', pn.format("%A/%f-%l")
101
92
  end
102
93
 
103
94
  def test_format_with_symbol_specifier
104
- Phonie::Phone.default_country_code = nil
105
95
  pn = Phonie::Phone.new '5125486', '91', '385'
106
96
  assert_equal '+385 (0) 91 512 5486', pn.format(:europe)
107
97
  end
@@ -1,37 +1,39 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
2
-
3
2
  require 'active_model'
4
3
  require 'phonie/railties/validator'
5
- class SomeModel < Struct.new(:phone)
4
+
5
+ class SomeModel < Struct.new(:phone_number)
6
6
  include ActiveModel::Validations
7
- validates :phone, phone: true
7
+ validates :phone_number, phone: true
8
8
  end
9
9
 
10
+ class SomeOtherModel < Struct.new(:phone_number)
11
+ include ActiveModel::Validations
12
+ validates :phone_number, phone: true, allow_blank: true
13
+ end
10
14
 
11
15
  class PhoneValidatorTest < Phonie::TestCase
12
16
  def test_blank_phone
13
- assert SomeModel.new(nil).valid?
14
- assert SomeModel.new('').valid?
17
+ assert SomeModel.new(nil).invalid?
18
+ assert SomeModel.new('').invalid?
19
+ assert SomeOtherModel.new(nil).valid?
20
+ assert SomeOtherModel.new('').valid?
15
21
  end
16
22
 
17
23
  def test_valid_model
18
24
  model = SomeModel.new('+1 251 123 4567')
19
25
  assert model.valid?
20
-
21
- assert model.phone == '+12511234567'
22
26
  end
23
27
 
24
28
  def test_valid_number_with_extension
25
29
  model = SomeModel.new('+1 251 123 4567 ex 1234')
26
30
  assert model.valid?
27
-
28
- assert model.phone == '+12511234567x1234'
29
31
  end
30
32
 
31
33
  def test_invalid_model
32
34
  model = SomeModel.new('+1 251 123 456')
33
35
  assert model.invalid?
34
36
 
35
- assert !model.errors[:phone].empty?
37
+ assert !model.errors[:phone_number].empty?
36
38
  end
37
39
  end
data/test/test_helper.rb CHANGED
@@ -23,8 +23,10 @@ end
23
23
  class Phonie::TestCase < Test::Unit::TestCase
24
24
 
25
25
  def setup
26
- Phonie::Phone.default_country_code = nil
27
- Phonie::Phone.default_area_code = nil
26
+ Phonie.configure do |config|
27
+ config.default_country_code = nil
28
+ config.default_area_code = nil
29
+ end
28
30
  end
29
31
 
30
32
  def default_test
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phonie
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.2
4
+ version: 3.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2013-09-27 00:00:00.000000000 Z
16
+ date: 2014-01-23 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: activemodel
@@ -23,7 +23,7 @@ dependencies:
23
23
  - - ! '>='
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
- type: :runtime
26
+ type: :development
27
27
  prerelease: false
28
28
  version_requirements: !ruby/object:Gem::Requirement
29
29
  none: false
@@ -76,18 +76,21 @@ extra_rdoc_files: []
76
76
  files:
77
77
  - .gitignore
78
78
  - .travis.yml
79
+ - Changelog.md
79
80
  - Gemfile
80
81
  - LICENSE
81
82
  - Rakefile
82
83
  - Readme.rdoc
83
84
  - lib/phonie.rb
85
+ - lib/phonie/configuration.rb
84
86
  - lib/phonie/country.rb
85
87
  - lib/phonie/data/phone_countries.yml
88
+ - lib/phonie/formatter.rb
89
+ - lib/phonie/parser.rb
86
90
  - lib/phonie/phone.rb
87
91
  - lib/phonie/railties/locales/en.yml
88
92
  - lib/phonie/railties/locales/es.yml
89
93
  - lib/phonie/railties/validator.rb
90
- - lib/phonie/support.rb
91
94
  - lib/phonie/version.rb
92
95
  - phonie.gemspec
93
96
  - tasks/tests.rake
@@ -214,7 +217,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
214
217
  version: '0'
215
218
  segments:
216
219
  - 0
217
- hash: 453458853538770825
220
+ hash: 2987221699007223305
218
221
  required_rubygems_version: !ruby/object:Gem::Requirement
219
222
  none: false
220
223
  requirements:
@@ -223,7 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
223
226
  version: '0'
224
227
  segments:
225
228
  - 0
226
- hash: 453458853538770825
229
+ hash: 2987221699007223305
227
230
  requirements: []
228
231
  rubyforge_project:
229
232
  rubygems_version: 1.8.23
@@ -1,78 +0,0 @@
1
- require 'yaml'
2
- # support methods to remove dependencies on ActiveSupport
3
- class String
4
- def present?
5
- !blank?
6
- end
7
-
8
- def blank?
9
- if respond_to?(:empty?) && respond_to?(:strip)
10
- empty? or strip.empty?
11
- elsif respond_to?(:empty?)
12
- empty?
13
- else
14
- !self
15
- end
16
- end
17
- end
18
-
19
- class Hash
20
- alias_method :blank?, :empty?
21
-
22
- def present?
23
- !blank?
24
- end
25
- end
26
-
27
- class Object
28
- def present?
29
- self.class!=NilClass
30
- end
31
- end
32
-
33
- class NilClass #:nodoc:
34
- def blank?
35
- true
36
- end
37
-
38
- def present?
39
- false
40
- end
41
- end
42
-
43
- module Accessorize
44
- module ClassMethods
45
- def cattr_accessor(*syms)
46
- syms.flatten.each do |sym|
47
- class_eval(<<-EOS, __FILE__, __LINE__)
48
- unless defined? @@#{sym}
49
- @@#{sym} = nil
50
- end
51
-
52
- def self.#{sym}
53
- @@#{sym}
54
- end
55
-
56
- def #{sym}=(value)
57
- @@#{sym} = value
58
- end
59
-
60
- def self.#{sym}=(value)
61
- @@#{sym} = value
62
- end
63
-
64
- def #{sym}
65
- @@#{sym}
66
- end
67
- EOS
68
- end
69
- end
70
- end
71
-
72
-
73
- def self.included(receiver)
74
- receiver.extend ClassMethods
75
- end
76
- end
77
-
78
- Object.send(:include, Accessorize)