phoney 0.1.3 → 0.2.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.
@@ -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