phoney 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,39 +1,30 @@
1
- require 'yaml'
1
+ require 'set'
2
2
 
3
- class PhoneNumber
4
-
3
+ module Phoney
5
4
  class Region
6
- @@regions = []
5
+ REGION_FILE = File.join(File.dirname(__FILE__), '..', 'data', 'regions.bin')
7
6
 
8
7
  attr_reader :country_code, :country_abbr
9
- attr_reader :national_prefix, :dialout_prefixes
8
+ attr_reader :trunk_prefixes, :dialout_prefixes
10
9
  attr_reader :rule_sets
11
10
 
12
11
  class << self
13
12
  def load
14
- data_file = File.join(File.dirname(__FILE__), '..', 'data', 'regions.yml')
15
-
16
13
  @@regions = []
17
- YAML.load(File.read(data_file)).each_pair do |key, region_hash|
18
- new_region = Region.new(region_hash)
19
- @@regions.push(new_region)
14
+ Marshal.load(File.read(REGION_FILE)).each_pair do |key, region_hash|
15
+ new_region = Region.new region_hash
16
+ @@regions.push new_region
20
17
  end
21
18
  @@regions
22
19
  end
23
20
 
24
21
  def all
25
- return @@regions unless @@regions.empty?
26
-
27
- load
22
+ @@regions.empty? ? load : @@regions
28
23
  end
29
24
 
30
- def find(param)
31
- return nil unless param
32
-
33
- param = param.to_sym
34
-
25
+ def find(param)
35
26
  all.detect do |region|
36
- region.country_code == param || region.country_abbr == param
27
+ region.country_code.to_s == param.to_s || region.country_abbr.to_s == param.to_s
37
28
  end
38
29
  end
39
30
 
@@ -42,29 +33,22 @@ class PhoneNumber
42
33
  end
43
34
  end
44
35
 
45
- def initialize(hash)
46
- @country_abbr = hash[:country_abbr]
47
- @country_code = hash[:country_code]
36
+ def initialize(options={})
37
+ @country_abbr = options[:country_abbr]
38
+ @country_code = options[:country_code]
48
39
 
49
- @national_prefix = hash[:national_prefix]
50
- @dialout_prefixes = hash[:dialout_prefixes]
40
+ @trunk_prefixes = options[:trunk_prefixes]
41
+ @dialout_prefixes = options[:dialout_prefixes]
51
42
 
52
- @rule_sets = hash[:rule_sets]
43
+ @rule_sets = SortedSet.new
53
44
 
54
- if(@rule_sets)
55
- for rule_set in @rule_sets do
56
- if(rule_set[:rules])
57
- rule_set[:rules].each_with_index do |rule,index|
58
- rule.merge!(:index => index)
59
- end
60
- end
61
- end
45
+ (options[:rule_sets]||[]).each do |rule_group|
46
+ @rule_sets.add RuleGroup.new(rule_group[:significant_digits], rule_group[:rules])
62
47
  end
63
48
  end
64
49
 
65
50
  def to_s
66
- "#{@country_abbr.to_s} [+#{@country_code.to_s}]"
51
+ country_abbr
67
52
  end
68
53
  end
69
-
70
54
  end
@@ -0,0 +1,56 @@
1
+ module Phoney
2
+ class RuleGroup
3
+ attr_reader :significant_digits, :rules
4
+
5
+ def initialize(significant_digits, rules=[])
6
+ @significant_digits = significant_digits
7
+ @rules = []
8
+
9
+ rules.each do |rule|
10
+ add_rule Rule.new(rule)
11
+ end
12
+ end
13
+
14
+ def add_rule(rule)
15
+ @rules.push rule
16
+ end
17
+
18
+ def delete_rule(rule)
19
+ @rules.delete rule
20
+ end
21
+
22
+ def <=>(other)
23
+ other.significant_digits <=> significant_digits
24
+ end
25
+ end
26
+
27
+ class Rule
28
+ attr_reader :max_digits, :min_value, :max_value, :areacode_length, :areacode_offset, :pattern
29
+
30
+ def initialize(options={})
31
+ @max_digits = options[:max_digits]
32
+
33
+ @min_value = options[:min_value]
34
+ @max_value = options[:max_value]
35
+
36
+ @areacode_length = options[:areacode_length]
37
+ @areacode_offset = options[:areacode_offset]
38
+
39
+ @pattern = options[:pattern]
40
+ @flags = options[:flags]
41
+ end
42
+
43
+ def <=>(other)
44
+ max_digits <=> other.max_digits
45
+ end
46
+
47
+ def matches?(number)
48
+ value = number.to_s[0, max_value.to_s.length].to_i
49
+ (min_value..max_value).member?(value) && number.length <= max_digits
50
+ end
51
+
52
+ def flags
53
+ @flags || []
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,31 @@
1
+ module Phoney
2
+ # Helper module that maps vanity numbers to digit numbers.
3
+ module Vanity
4
+ VANITY_REGEXP = /\A\d{3}[a-zA-Z]{6,12}\Z/
5
+ VANITY_NORMALIZING_REGEXP = /^0*|[^\d\w]/
6
+
7
+ # Returns a char to number mapping string for the String#tr method.
8
+ def self.mapping
9
+ @@mapping ||= [
10
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.freeze,
11
+ '2223334445556667777888999922233344455566677778889999'.freeze
12
+ ]
13
+ end
14
+
15
+ # Replaces (and normalizes) vanity characters of passed number with correct digits.
16
+ def self.replace number
17
+ number.tr *mapping
18
+ end
19
+
20
+ # Returns true if there is a character in the number
21
+ # after the first four numbers.
22
+ def self.vanity? number
23
+ !(normalized(number) =~ VANITY_REGEXP).nil?
24
+ end
25
+
26
+ # Vanity-Normalized.
27
+ def self.normalized number
28
+ number.gsub VANITY_NORMALIZING_REGEXP, ''
29
+ end
30
+ end
31
+ end
@@ -1,11 +1,3 @@
1
- class PhoneNumber
2
-
3
- module VERSION #:nodoc:
4
- MAJOR = 0
5
- MINOR = 1
6
- TINY = 3
7
-
8
- STRING = [MAJOR, MINOR, TINY].join('.')
9
- end
10
-
1
+ module Phoney
2
+ VERSION = "0.2.0"
11
3
  end
@@ -1,48 +1,20 @@
1
- # -*- encoding: utf-8 -*-
2
-
3
- $:.push File.expand_path("../lib", __FILE__)
4
-
5
- # Maintain your gem's version:
1
+ $:.unshift File.expand_path("../lib", __FILE__)
6
2
  require "phoney/version"
7
3
 
8
4
  Gem::Specification.new do |s|
9
- s.name = %q{phoney}
10
- s.version = PhoneNumber::VERSION::STRING
11
-
12
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
13
- s.authors = ["Jan Habermann"]
14
- s.date = %q{2011-12-01}
15
- s.description = %q{Ruby library that parses a phone number and automatically formats it correctly, depending on the country/locale you set.}
16
- s.email = %q{jan@habermann24.com}
17
- s.extra_rdoc_files = [
18
- "README.rdoc"
19
- ]
20
- s.files = [
21
- "Gemfile",
22
- "Gemfile.lock",
23
- "LICENCE",
24
- "README.rdoc",
25
- "Rakefile",
26
- "lib/data/regions.yml",
27
- "lib/phoney.rb",
28
- "lib/phoney/base.rb",
29
- "lib/phoney/parser.rb",
30
- "lib/phoney/region.rb",
31
- "lib/phoney/utils.rb",
32
- "lib/phoney/version.rb",
33
- "phoney.gemspec",
34
- "spec/parser/br_spec.rb",
35
- "spec/parser/de_spec.rb",
36
- "spec/parser/us_spec.rb",
37
- "spec/phone_number_spec.rb",
38
- "spec/spec.opts",
39
- "spec/spec_helper.rb"
40
- ]
41
- s.homepage = %q{http://github.com/habermann24/phoney}
42
- s.require_paths = ["lib"]
43
- s.summary = %q{Ruby library that formats phone numbers.}
44
-
45
- s.add_development_dependency(%q<rake>, [">= 0"])
46
- s.add_development_dependency(%q<rspec>, ["= 2.11.0"])
5
+ s.name = 'phoney'
6
+ s.version = Phoney::VERSION
7
+ s.author = 'Jan Habermann'
8
+ s.email = 'jan@habermann24.com'
9
+ s.homepage = 'http://github.com/habermann24/phoney'
10
+ s.summary = 'Ruby library that formats phone numbers.'
11
+ s.description = 'Ruby library that parses a phone number and automatically formats it correctly, depending on the country/locale you set.'
12
+
13
+ s.required_rubygems_version = '>= 1.9'
14
+
15
+ s.add_development_dependency 'rake'
16
+ s.add_development_dependency 'minitest'
17
+
18
+ s.files = Dir["#{File.dirname(__FILE__)}/**/*"]
47
19
  end
48
20
 
@@ -0,0 +1,32 @@
1
+ require 'bundler/setup'
2
+ require 'phoney'
3
+
4
+ require 'io/console'
5
+
6
+ Phoney.region = ARGV.first||'us'
7
+
8
+ input = ''
9
+ prompt = "(Ctrl-C to exit) #[#{Phoney.region.country_abbr}]: "
10
+ print prompt
11
+
12
+ while ch = STDIN.getch do
13
+ case ch.ord
14
+ when 3 # Ctrl-C
15
+ break
16
+ when 127 # Backspace
17
+ input = input[0..-2]
18
+ print "\b"
19
+ else
20
+ input << ch
21
+ end
22
+
23
+ output = Phoney::Parser.parse input
24
+
25
+ print "\r#{prompt}"
26
+ print output
27
+
28
+ print " "*100
29
+ print "\b"*100
30
+ end
31
+
32
+ puts "\n^C Exiting - Goodbye"
@@ -0,0 +1,122 @@
1
+ require 'yaml'
2
+
3
+ module PhoneformatConverter
4
+ class Country
5
+ attr_accessor :international_prefix, :code, :offset
6
+ attr_accessor :desc_len, :desc_count, :disp_scheme_offset, :rules_sets_count
7
+ attr_accessor :trunk_prefixes
8
+ attr_accessor :dialout_prefixes
9
+ end
10
+
11
+ def self.convert(file)
12
+ io = File.open(file, "rb")
13
+ num_countries = io.read(4).unpack("V")[0]
14
+ countries = []
15
+
16
+ (1..num_countries).each do
17
+ country = Country.new
18
+ country.international_prefix, country.code, country.offset = io.read(12).unpack("a4/a4/V")
19
+
20
+ country.international_prefix = country.international_prefix.delete("\000")
21
+ country.code = country.code.delete("\000")
22
+
23
+ countries << country
24
+ end
25
+
26
+ data = io.read
27
+
28
+ countries.each do |c|
29
+ c.desc_len, c.desc_count, c.disp_scheme_offset, c.rules_sets_count = data[c.offset, 12].unpack("v/v/V/V")
30
+
31
+ local_nat_raw = data[c.offset+12, c.desc_len-12].unpack("A*")
32
+
33
+ c.trunk_prefixes = local_nat_raw.to_s.delete('"[]').split('\0\0').first.split('\0').reject(&:empty?) rescue []
34
+ c.dialout_prefixes = local_nat_raw.to_s.delete('"[]').split('\0\0').last.split('\0').reject(&:empty?) rescue []
35
+
36
+ temp_dialout_prefixes = c.dialout_prefixes
37
+ c.dialout_prefixes = []
38
+
39
+ temp_dialout_prefixes.each do |p|
40
+ if p.start_with?('->')
41
+ c.dialout_prefixes.last << p
42
+ else
43
+ c.dialout_prefixes << p
44
+ end
45
+ end
46
+
47
+ rules_offset = c.offset+c.desc_len # points to the rules section
48
+ scheme_base = rules_offset+c.disp_scheme_offset # points to the scheme section
49
+
50
+ puts "#{c.code}:"
51
+ puts " :country_code: #{c.international_prefix}"
52
+ puts " :country_abbr: #{c.code}"
53
+ puts " :trunk_prefixes: #{c.trunk_prefixes}"
54
+ puts " :dialout_prefixes: #{c.dialout_prefixes}"
55
+ puts " :rule_sets:"
56
+
57
+ smallest_scheme_offset = nil
58
+
59
+ (0..(c.rules_sets_count-1)).each do |rule_set_idx|
60
+ digits, rules_count = data[rules_offset, 4].unpack("v/v")
61
+
62
+ puts " - :significant_digits: #{digits}"
63
+ puts " :rules:"
64
+
65
+ # iterate over individual rules
66
+ (0..(rules_count-1)).each do |rule_idx|
67
+ # each rule is 16 bytes
68
+ prefix_min,prefix_max,unkn1,total_digits,areacode_offset,areacode_length,type,unkn2,scheme_offset = data[rules_offset+4+(rule_idx*16), 16].unpack("V/V/C/C/C/C/C/C/v")
69
+
70
+ scheme_length = data[(scheme_base+scheme_offset)..-1].index("\x00")
71
+ comment_length = data[(scheme_base+scheme_offset+scheme_length+1)..-1].index("\x00")
72
+
73
+ # now extract the rule scheme and comment
74
+ scheme = data[scheme_base+scheme_offset, scheme_length]
75
+ comment = data[scheme_base+scheme_offset+scheme_length+1, comment_length]
76
+
77
+ smallest_scheme_offset ||= scheme_offset
78
+ smallest_scheme_offset = [smallest_scheme_offset, scheme_offset].min
79
+
80
+ flags = []
81
+
82
+ types_bin = ("%08b" % type).split('').reverse
83
+ types_bin.each_with_index do |t,i|
84
+ if t.to_i == 1
85
+ case 2**i
86
+ when 1
87
+ flags << :n
88
+ when 2
89
+ flags << :c
90
+ when 4
91
+ flags << :toll_free
92
+ when 8
93
+ flags << :local
94
+ when 16
95
+ flags << :unknown_16
96
+ when 32
97
+ flags << :unknown_32
98
+ end
99
+ end
100
+ end
101
+
102
+ puts " - :min_value: #{prefix_min}"
103
+ puts " :max_value: #{prefix_max}"
104
+ puts " :max_digits: #{total_digits}"
105
+ puts " :areacode_length: #{areacode_length}"
106
+ puts " :areacode_offset: #{areacode_offset}"
107
+ puts " :unknown1: #{unkn1}"
108
+ puts " :unknown2: #{unkn2}"
109
+ puts " :pattern: \"#{scheme}\""
110
+ puts " :flags:"
111
+ flags.each do |flag|
112
+ puts " - :#{flag}"
113
+ end
114
+ end
115
+
116
+ rules_offset += (4 + rules_count*16) # move to next rules-set
117
+ end
118
+
119
+ pre_scheme = data[scheme_base, smallest_scheme_offset||0]
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,23 @@
1
+ require 'yaml'
2
+ require './resources/phoneformat_converter'
3
+
4
+ namespace :phoney do
5
+ desc 'Generate a regions YAML file from a .phoneformat binary'
6
+ task :generate_regions_yml do
7
+ raise 'You must provide .phoneformat FILE for the task to run' unless ENV['FILE']
8
+ PhoneformatConverter.convert(ENV['FILE'])
9
+ end
10
+
11
+ desc 'Generate a new regions data file from a .phoneformat binary'
12
+ task :generate_regions_bin do
13
+ raise 'You must provide .phoneformat FILE for the task to run' unless ENV['FILE']
14
+
15
+ output = StringIO.new
16
+ $stdout = output
17
+
18
+ PhoneformatConverter.convert(ENV['FILE'])
19
+
20
+ $stdout = STDOUT
21
+ puts Marshal.dump(YAML.load(output.string))
22
+ end
23
+ end
@@ -0,0 +1,91 @@
1
+ require 'phoney/test_helper'
2
+
3
+ class FormatterTest < MiniTest::Unit::TestCase
4
+ include Phoney::Formatter
5
+
6
+ def setup
7
+ Phoney.region = 'us'
8
+ end
9
+
10
+ def test_international_call_prefix_for_us_region
11
+ assert_equal "0", international_call_prefix_for("0")
12
+ assert_equal "01", international_call_prefix_for("01")
13
+ assert_equal "011", international_call_prefix_for("011")
14
+ assert_equal "011", international_call_prefix_for("011999")
15
+ assert_equal nil, international_call_prefix_for("123")
16
+ end
17
+
18
+ def test_international_call_prefix_for_br_region
19
+ assert_equal "+00", international_call_prefix_for("+0055123", region: Phoney::Region["br"])
20
+ assert_equal "00 55", international_call_prefix_for("0055123", region: Phoney::Region["br"])
21
+ assert_equal "00 12", international_call_prefix_for("0012456", region: Phoney::Region["br"])
22
+ assert_equal nil, international_call_prefix_for("03001234567", region: Phoney::Region["br"])
23
+ end
24
+
25
+ def test_international_call_prefix_with_plus_sign
26
+ assert_equal "+011", international_call_prefix_for("+011")
27
+ assert_equal "+", international_call_prefix_for("+123")
28
+ assert_equal "+", international_call_prefix_for("+")
29
+ end
30
+
31
+ def test_formatting_international_call_with_non_exiting_country
32
+ assert_equal "+99", format("+99", "###")
33
+
34
+ assert_equal "+99", Phoney::Parser.parse("+99")
35
+ assert_equal "+999999999", Phoney::Parser.parse("+999999999")
36
+
37
+ assert_equal "011 99", Phoney::Parser.parse("01199")
38
+ assert_equal "011 999999999", Phoney::Parser.parse("011999999999")
39
+ end
40
+
41
+ def test_international_calling_prefix_for_empty_number
42
+ assert_equal nil, international_call_prefix_for("")
43
+ end
44
+
45
+ def test_country_code_extraction
46
+ assert_equal "49", extract_country_code("+49")
47
+ assert_equal "49", extract_country_code("01149")
48
+ assert_equal "49", extract_country_code("01149123456")
49
+ assert_equal "1", extract_country_code("0111234567")
50
+ assert_equal nil, extract_country_code("03001234567", region: Phoney::Region["br"])
51
+ end
52
+
53
+ def test_nonexisting_country_code
54
+ assert_equal nil, extract_country_code("+99")
55
+ assert_equal nil, extract_country_code("01199")
56
+ end
57
+
58
+ def test_trunk_prefix_extraction
59
+ assert_equal "1", extract_trunk_prefix("+117041234567")
60
+ assert_equal "0", extract_trunk_prefix("+49040")
61
+ assert_equal nil, extract_trunk_prefix("+1705")
62
+ assert_equal nil, extract_trunk_prefix("+1")
63
+ assert_equal nil, extract_trunk_prefix("+4940")
64
+
65
+ assert_equal "1", extract_trunk_prefix("011117041234567")
66
+ assert_equal nil, extract_trunk_prefix("011705")
67
+ assert_equal "0", extract_trunk_prefix("01149040")
68
+ end
69
+
70
+ def test_format_international_number_with_trunk_prefix
71
+ assert_equal "+1 (1) (704) 205-1234", Phoney::Parser.parse("+117042051234")
72
+ assert_equal "+49 (0) 40 1234567", Phoney::Parser.parse("+490401234567")
73
+ assert_equal "011 1 (1) (704) 205-1234", Phoney::Parser.parse("011117042051234")
74
+ assert_equal "011 49 (0) 40 1234567", Phoney::Parser.parse("011490401234567")
75
+ end
76
+
77
+ def test_format_string_without_prefixes
78
+ assert_equal "123", format("123", "n ###")
79
+ assert_equal "123", format("123", "c ###")
80
+ assert_equal "123", format("123", "c n ###")
81
+ end
82
+
83
+ def test_format_number_with_double_international_prefix
84
+ assert_equal "+011 49 40", Phoney::Parser.parse("+0114940")
85
+ end
86
+
87
+ def test_international_prefix_with_plus_and_trunk_prefix_start
88
+ assert_equal "+0", international_call_prefix_for("+0", region: Phoney::Region["de"])
89
+ assert_equal "+00", international_call_prefix_for("+00", region: Phoney::Region["de"])
90
+ end
91
+ end