phonie 2.1.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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)