dialable 0.6.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.gitignore +2 -0
- data/.travis.yml +8 -13
- data/Gemfile +0 -2
- data/Gemfile.lock +77 -18
- data/Guardfile +57 -0
- data/README.md +55 -0
- data/Rakefile +13 -1
- data/certs/chorn.pem +55 -0
- data/data/dialable/nanpa.yaml +919 -398
- data/dialable.gemspec +29 -18
- data/lib/dialable.rb +52 -123
- data/lib/dialable/area_codes.rb +13 -0
- data/lib/dialable/parsers.rb +19 -0
- data/lib/dialable/patterns.rb +15 -0
- data/lib/dialable/service_codes.rb +17 -0
- data/lib/dialable/version.rb +1 -1
- data/spec/dialable_spec.rb +74 -35
- data/spec/spec_helper.rb +2 -4
- data/support/make_yaml_nanpa.rb +123 -23
- metadata +147 -31
- metadata.gz.sig +0 -0
- data/README.rdoc +0 -45
- data/TODO +0 -2
- data/data/nanpa.yaml +0 -1404
data/dialable.gemspec
CHANGED
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
|
data/lib/dialable.rb
CHANGED
@@ -1,141 +1,48 @@
|
|
1
|
-
|
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
|
-
|
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
|
56
|
-
|
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
|
-
|
63
|
-
|
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
|
-
|
22
|
+
fail InvalidNANPError, "Not a valid NANP Phone Number."
|
110
23
|
end
|
111
24
|
end
|
112
25
|
|
113
|
-
def
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
@
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/dialable/version.rb
CHANGED
data/spec/dialable_spec.rb
CHANGED
@@ -1,48 +1,87 @@
|
|
1
|
-
require
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Dialable do
|
4
|
-
|
5
|
-
subject { Dialable::NANP.parse(
|
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
|
8
|
-
|
9
|
-
|
10
|
-
it
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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') +
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|