dialable 0.6.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,24 +3,35 @@ lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'dialable/version'
5
5
 
6
- spec = Gem::Specification.new do |gem|
7
- gem.licenses = ['MIT', 'LGPL-2']
8
- gem.required_ruby_version = '>= 1.9.0'
9
- gem.name = "dialable"
10
- gem.version = Dialable::VERSION
11
- gem.authors = ["chorn"]
12
- gem.email = ["chorn@chorn.com"]
13
- gem.description = %q{A gem that provides parsing and output of phone numbers according to NANPA (North American Numbering Plan Administration) standards. If possible, time zones are populated by abbreviation as well as offset relative to the local timezone.}
14
- gem.summary = %q{Provides parsing and output of phone numbers according to NANPA standards.}
15
- gem.homepage = "http://github.com/chorn/dialable"
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'dialable'
8
+ spec.version = Dialable::VERSION
9
+ spec.authors = ['Chris Horn']
10
+ spec.email = ['chorn@chorn.com']
11
+ spec.summary = 'Provides parsing and output of phone numbers according to NANPA standards.'
12
+ spec.description = 'A gem that provides parsing and output of phone numbers according to NANPA (North American Numbering Plan Administration) standards. If possible, time zones are populated by abbreviation as well as offset relative to the local timezone.'
13
+ spec.homepage = 'http://github.com/chorn/dialable'
14
+ spec.licenses = ['MIT', 'LGPL-2']
16
15
 
17
- gem.files = `git ls-files`.split($/)
18
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
19
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
- gem.require_paths = ["lib"]
21
- gem.add_dependency "tzinfo"
22
- gem.add_development_dependency "rake"
23
- gem.add_development_dependency "rspec"
24
- gem.add_development_dependency "tzinfo"
16
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
+ spec.executables = spec.files.grep(%r{!^bin/}).map { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ signing_key = File.expand_path("~/.certs/chorn@chorn.com-rubygems.key")
22
+ if File.file?(signing_key)
23
+ spec.signing_key = signing_key
24
+ spec.cert_chain = ['certs/chorn.pem']
25
+ end
26
+
27
+ spec.required_ruby_version = '>= 1.9.0'
28
+ spec.add_runtime_dependency 'tzinfo', '~> 1.0'
29
+ spec.add_development_dependency 'bundler', '~> 1.6'
30
+ spec.add_development_dependency 'codeclimate-test-reporter', '~> 0.4'
31
+ spec.add_development_dependency 'guard', '~> 2.12'
32
+ spec.add_development_dependency 'guard-bundler', '~> 2.1'
33
+ spec.add_development_dependency 'guard-rspec', '~> 4.5'
34
+ spec.add_development_dependency 'rake', '~> 10.4'
35
+ spec.add_development_dependency 'rspec', '~> 3.3'
25
36
  end
26
37
 
@@ -1,141 +1,48 @@
1
- # Copyright (c) 2008-2011 Chris Horn http://chorn.com/
2
- # See MIT-LICENSE.txt
3
-
4
- require "dialable/version"
5
- require "yaml"
6
- require "tzinfo"
1
+ require 'dialable/version'
7
2
  require 'rubygems'
3
+ require 'tzinfo'
4
+ require 'dialable/area_codes'
5
+ require 'dialable/service_codes'
6
+ require 'dialable/patterns'
7
+ require 'dialable/parsers'
8
8
 
9
9
  module Dialable
10
10
  class NANP
11
+ class InvalidNANPError < StandardError; end
11
12
 
12
- ##
13
- # Raised if something other than a valid NANP is supplied
14
- class InvalidNANPError < StandardError
15
- end
16
-
17
- ##
18
- # ERC, Easily Recognizable Codes
19
- module ServiceCodes
20
- ERC = {
21
- 211 => "Community Information and Referral Services",
22
- 311 => "Non-Emergency Police and Other Governmental Services",
23
- 411 => "Local Directory Assistance",
24
- 511 => "Traffic and Transportation Information (US); Provision of Weather and Traveller Information Services (Canada)",
25
- 611 => "Repair Service",
26
- 711 => "Telecommunications Relay Service (TRS)",
27
- 811 => "Access to One Call Services to Protect Pipeline and Utilities from Excavation Damage (US); Non-Urgent Health Teletriage Services (Canada)",
28
- 911 => "Emergency"
29
- }
30
- end
31
-
32
- ##
33
- # Regexs to match valid phone numbers
34
- module Patterns
35
- VALID = [
36
- /^\D*1?\D*([2-9]\d\d)\D*(\d{3})\D*(\d{4})\D*[ex]+\D*(\d{1,5})\D*$/i,
37
- /^\D*1?\D*([2-9]\d\d)[ $\\\.-]*(\d{3})[ $\\\.-]*(\d{4})[ $\\\.\*-]*(\d{1,5})\D*$/i,
38
- /^\D*1?\D*([2-9]\d\d)\D*(\d{3})\D*(\d{4})\D*$/,
39
- /^(\D*)(\d{3})\D*(\d{4})\D*$/,
40
- /^\D*([2-9]11)\D*$/,
41
- /^\D*1?\D*([2-9]\d\d)\D*(\d{3})\D*(\d{4})\D.*/ # Last ditch, just find a number
42
- ]
43
- end
44
-
45
- ##
46
- # Valid area codes per nanpa.com
47
- module AreaCodes
48
- data_path = Gem::datadir('dialable')
49
- data_path ||= File.join(File.dirname(__FILE__), '..', 'data', 'dialable')
50
- NANP = YAML.load_file(File.join(data_path, "nanpa.yaml"))
51
- end
52
-
53
- attr_accessor :areacode, :prefix, :line, :extension, :location, :country, :timezones, :relative_timezones, :raw_timezone
13
+ attr_accessor :areacode, :prefix, :line, :extension, :location, :country, :timezones, :relative_timezones
14
+ attr_reader :service_codes, :pattern
54
15
 
55
- def initialize(parts={})
56
- self.areacode = parts[:areacode] ? parts[:areacode] : nil
57
- self.prefix = parts[:prefix] ? parts[:prefix] : nil
58
- self.line = parts[:line] ? parts[:line] : nil
59
- self.extension = parts[:extension] ? parts[:extension] : nil
60
- end
16
+ def self.parse(string, parser = Dialable::Parsers::NANP)
17
+ parsed = parser.call(string)
61
18
 
62
- def areacode=(raw_code)
63
- code = raw_code.to_i
64
- if AreaCodes::NANP[code]
65
- @areacode = raw_code
66
- self.location = AreaCodes::NANP[code][:location] if AreaCodes::NANP[code][:location]
67
- self.country = AreaCodes::NANP[code][:country] if AreaCodes::NANP[code][:country]
68
- self.raw_timezone = AreaCodes::NANP[code][:timezone] if AreaCodes::NANP[code][:timezone]
69
-
70
- if AreaCodes::NANP[code][:timezone]
71
- self.timezones = []
72
- self.relative_timezones = []
73
- tz = AreaCodes::NANP[code][:timezone]
74
- now = Time.now
75
- local_utc_offset = now.utc_offset/3600
76
-
77
- if tz =~ /UTC(-\d+)/
78
- self.timezones << tz
79
- utc_offset = $1.to_i
80
- self.relative_timezones << (utc_offset) - local_utc_offset
81
- else
82
- tz.split(//).each do |zone| # http://www.timeanddate.com/library/abbreviations/timezones/na/
83
- zone = "HA" if zone == "H"
84
- zone = "AK" if zone == "K"
85
- tz = zone + (now.dst? ? "D" : "S") + "T" # This is cludgey
86
- self.timezones << tz
87
- delta = nil
88
- if Time.zone_offset(tz)
89
- delta = Time.zone_offset(tz)/3600 - local_utc_offset
90
- else
91
- case zone
92
- when /N[SD]T/
93
- delta = -3.5 - local_utc_offset
94
- when /A[SD]T/
95
- delta = -4 - local_utc_offset
96
- when /A?K[SD]T/
97
- delta = -9 - local_utc_offset
98
- when /HA?[SD]T/
99
- delta = -10 - local_utc_offset
100
- end
101
- delta = delta - 1 if delta and now.dst?
102
- end
103
- # puts "#{delta} // #{Time.zone_offset(tz)} // #{tz} // #{local_utc_offset}"
104
- self.relative_timezones << delta if delta
105
- end
106
- end
107
- end
19
+ if parsed
20
+ return Dialable::NANP.new(parsed)
108
21
  else
109
- raise InvalidNANPError, "#{code} is not a valid NANP Area Code."
22
+ fail InvalidNANPError, "Not a valid NANP Phone Number."
110
23
  end
111
24
  end
112
25
 
113
- def timezone
114
- @timezones.first if @timezones
115
- end
116
-
117
- def timezone=(tz)
118
- @timezones = [tz] if tz
26
+ def initialize(parts = {}, options = {})
27
+ self.areacode = parts.fetch(:areacode)
28
+ self.prefix = parts.fetch(:prefix)
29
+ self.line = parts.fetch(:line)
30
+ self.extension = parts.fetch(:extension)
31
+ @service_codes = options.fetch(:service_codes) { Dialable::ServiceCodes::NANP }
32
+ @patterns = options.fetch(:patterns) { Dialable::Patterns::NANP }
119
33
  end
120
34
 
121
- # def relative_timezones
122
- # rt = []
123
- # @timezones.each do |tz|
124
- # rt <<
125
- # end
126
- # rt
127
- # end
128
-
129
- def self.parse(number)
130
- Patterns::VALID.each do |pattern|
131
- return Dialable::NANP.new(:areacode => $1, :prefix => $2, :line => $3, :extension => $4) if number =~ pattern
132
- end
133
-
134
- raise InvalidNANPError, "Not a valid NANP Phone Number."
35
+ def areacode=(raw_code)
36
+ code = AreaCodes::NANP.fetch(raw_code.to_i) { fail InvalidNANPError, "#{code} is not a valid NANP Area Code." }
37
+ @areacode = raw_code
38
+ @location = code.fetch(:location) { nil }
39
+ @country = code.fetch(:country) { nil }
40
+ @timezones = code.fetch(:timezones) { [] }
41
+ @relative_timezones = @timezones.map { |timezone| timezone_offset(timezone) }
135
42
  end
136
43
 
137
44
  def erc?
138
- return ServiceCodes::ERC[@areacode].nil?
45
+ @service_codes.key?(@areacode)
139
46
  end
140
47
 
141
48
  def to_s
@@ -159,12 +66,34 @@ module Dialable
159
66
  end
160
67
 
161
68
  def to_hash
162
- return {
69
+ {
163
70
  :areacode => @areacode,
164
71
  :prefix => @prefix,
165
72
  :line => @line,
166
73
  :extension => @extension
167
74
  }
168
75
  end
76
+
77
+ def timezone
78
+ @timezones.first if @timezones
79
+ end
80
+
81
+ def timezone=(tz)
82
+ @timezones = [tz] if tz
83
+ end
84
+
85
+ def raw_timezone
86
+ @raw_timezones.first if @raw_timezones
87
+ end
88
+
89
+ def relative_timezone
90
+ @relative_timezones.first if @relative_timezones
91
+ end
92
+
93
+ private
94
+
95
+ def timezone_offset(raw)
96
+ TZInfo::Timezone.get(raw).offsets_up_to(0).first.utc_total_offset / 3600
97
+ end
169
98
  end
170
99
  end
@@ -0,0 +1,13 @@
1
+ require 'yaml'
2
+
3
+ module Dialable
4
+ module AreaCodes
5
+
6
+ # Valid area codes per nanpa.com
7
+ data_path = Gem.datadir('dialable')
8
+ data_path ||= File.join(File.dirname(__FILE__), '..', 'data', 'dialable')
9
+
10
+ NANP = YAML.load_file(File.join(data_path, 'nanpa.yaml'))
11
+
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ require 'dialable/patterns'
2
+
3
+ module Dialable
4
+ module Parsers
5
+
6
+ NANP = lambda do |string, patterns = Dialable::Patterns::NANP|
7
+ patterns.each do |pattern|
8
+ pattern.match(string) do |m|
9
+ return { areacode: m[1],
10
+ prefix: m[2],
11
+ line: m[3],
12
+ extension: m[4],
13
+ }
14
+ end
15
+ end
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Dialable
2
+ module Patterns
3
+
4
+ # Regexs to match valid phone numbers
5
+ NANP = [
6
+ Regexp.new('^\D*1?\D*([2-9]\d\d)\D*(\d{3})\D*(\d{4})\D*[ex]+\D*(\d{1,5})\D*$', Regexp::IGNORECASE),
7
+ Regexp.new('^\D*1?\D*([2-9]\d\d)[ $\\\.-]*(\d{3})[ $\\\.-]*(\d{4})[ $\\\.\*-]*(\d{1,5})\D*$', Regexp::IGNORECASE),
8
+ Regexp.new('^\D*1?\D*([2-9]\d\d)\D*(\d{3})\D*(\d{4})\D*$'),
9
+ Regexp.new('^(\D*)(\d{3})\D*(\d{4})\D*$'),
10
+ Regexp.new('^\D*([2-9]11)\D*$'),
11
+ Regexp.new('^\D*1?\D*([2-9]\d\d)\D*(\d{3})\D*(\d{4})\D.*') # Last ditch, just find a number
12
+ ]
13
+
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Dialable
2
+ module ServiceCodes
3
+
4
+ # Easily Recognizable Codes
5
+ NANP = {
6
+ 211 => 'Community Information and Referral Services',
7
+ 311 => 'Non-Emergency Police and Other Governmental Services',
8
+ 411 => 'Local Directory Assistance',
9
+ 511 => 'Traffic and Transportation Information (US); Provision of Weather and Traveller Information Services (Canada)',
10
+ 611 => 'Repair Service',
11
+ 711 => 'Telecommunications Relay Service (TRS)',
12
+ 811 => 'Access to One Call Services to Protect Pipeline and Utilities from Excavation Damage (US); Non-Urgent Health Teletriage Services (Canada)',
13
+ 911 => 'Emergency'
14
+ }
15
+
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module Dialable
2
- VERSION = "0.6.3"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,48 +1,87 @@
1
- require File.dirname(__FILE__) + '/spec_helper'
1
+ require 'spec_helper'
2
2
 
3
3
  describe Dialable do
4
- describe "with a full NANP number with extension" do
5
- subject { Dialable::NANP.parse("+1(307)555-1212 ext 1234") }
4
+ context 'with a full NANP number with extension' do
5
+ subject { Dialable::NANP.parse('+1(307)555-1212 ext 1234') }
6
6
 
7
- it("should extract the area code") { subject.areacode.should == "307" }
8
- it("should extract the prefix") { subject.prefix.should == "555" }
9
- it("should extract the line number") { subject.line.should == "1212" }
10
- it("should extract the extension") { subject.extension.should == "1234" }
7
+ it 'will not raise an exception' do
8
+ end
9
+
10
+ it 'will parse the area code' do
11
+ expect(subject.areacode).to eq '307'
12
+ end
13
+
14
+ it 'will parse the prefix' do
15
+ expect(subject.prefix).to eq '555'
16
+ end
17
+
18
+ it 'will parse the line number' do
19
+ expect(subject.line).to eq '1212'
20
+ end
11
21
 
22
+ it 'will parse the extension' do
23
+ expect(subject.extension).to eq '1234'
24
+ end
12
25
  end
13
26
 
14
- describe "with a full NANP number with extension and appropriate daylight/standard time" do
15
- subject { Dialable::NANP.parse("+1(307)555-1212 ext 1234") }
16
- it("should determine the time zone during daylight savings or standard time") {
17
- target_zone = Time.new.dst? ? "MDT" : "MST"
18
- subject.timezones.should == [target_zone]
19
- }
27
+ context 'with a full NANP number with extension and appropriate daylight/standard time' do
28
+ let(:target_zone) { 'US/Mountain' }
29
+ subject { Dialable::NANP.parse('+1(307)555-1212 ext 1234') }
30
+
31
+ it 'will determine the time zone during daylight savings or standard time' do
32
+ expect(subject.timezones).to eq [target_zone]
33
+ end
20
34
  end
21
35
 
22
- NANP = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'data') + "/nanpa.yaml")
36
+ NANP = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'data', 'dialable') + '/nanpa.yaml')
23
37
  NANP.delete(:created)
24
38
  NANP.each do |nanp|
25
39
  areacode = nanp[0]
26
- describe "recognize the area code #{areacode} in ()'s" do
27
- subject { Dialable::NANP.parse("(#{areacode})5551212") }
28
- it("should extract the area code") { subject.areacode.should == areacode.to_s }
29
- end
30
- describe "recognize the area code #{areacode} with garbage" do
31
- subject { Dialable::NANP.parse("noise #{areacode}noise5551212") }
32
- it("should extract the area code") { subject.areacode.should == areacode.to_s }
33
- end
34
- describe "with a sloppy areacode of #{areacode} and number" do
35
- subject { Dialable::NANP.parse("+1-#{areacode}555 1212") }
36
- it("should match the location") { subject.location.should == nanp[1][:location] }
37
- end
38
- describe "with areacode of #{areacode} and a sloppy extension" do
39
- subject { Dialable::NANP.parse("1 #{areacode} 867 5309 xAAA0001") }
40
- it("should match the country") { subject.country.should == nanp[1][:country] }
41
- end
42
- describe "with areacode of #{areacode} and lots of garbage" do
43
- subject { Dialable::NANP.parse("1 #{areacode}yadayada867BLAH5309....derp") }
44
- it("should match the timezone") { subject.raw_timezone.should == nanp[1][:timezone] }
40
+ country = nanp[1].fetch(:country) { nil }
41
+ location = nanp[1].fetch(:location) { nil }
42
+ timezones = nanp[1].fetch(:timezones) { nil }
43
+ timezone = timezones.first if timezones
44
+
45
+ context "with area code #{areacode}" do
46
+ describe "when the areacode is in ()" do
47
+ subject { Dialable::NANP.parse("(#{areacode})5551212") }
48
+
49
+ it 'will parse the area code' do
50
+ expect(subject.areacode).to eq areacode.to_s
51
+ end
52
+ end
53
+
54
+ describe 'when there is text noise in the number' do
55
+ subject { Dialable::NANP.parse("noise #{areacode}noise5551212") }
56
+
57
+ it 'will parse area code' do
58
+ expect(subject.areacode).to eq areacode.to_s
59
+ end
60
+ end
61
+
62
+ describe 'when the areacode has extra noise' do
63
+ subject { Dialable::NANP.parse("+1-#{areacode}555 1212") }
64
+
65
+ it 'will find the location' do
66
+ expect(subject.location).to eq location
67
+ end
68
+ end
69
+
70
+ describe 'when the extension has extra noise' do
71
+ subject { Dialable::NANP.parse("1 #{areacode} 867 5309 xAAA0001") }
72
+
73
+ it 'will find the country' do
74
+ expect(subject.country).to eq country
75
+ end
76
+ end
77
+
78
+ describe 'when there is lots of garbage in the string' do
79
+ subject { Dialable::NANP.parse("1 #{areacode}yadayada867BLAH5309....derp") }
80
+
81
+ it 'will find the timezone' do
82
+ expect(subject.timezone).to eq timezone
83
+ end
84
+ end
45
85
  end
46
86
  end
47
-
48
- end
87
+ end