postal_code 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +140 -0
- data/Rakefile +18 -0
- data/db/us.csv +43628 -0
- data/lib/postal_code.rb +78 -0
- data/lib/postal_code/server.rb +56 -0
- data/postal_code.gemspec +12 -0
- data/test/data/us.csv +4 -0
- data/test/test_postal_code.rb +87 -0
- metadata +54 -0
data/lib/postal_code.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module PostalCode
|
2
|
+
|
3
|
+
VERSION = '0.0.1'
|
4
|
+
|
5
|
+
CityOffset = 0
|
6
|
+
StateOffset = 1
|
7
|
+
DB = File.join File.dirname(__FILE__), '..', 'db', 'us.csv'
|
8
|
+
FS = "\t" # field separator
|
9
|
+
RS = "\n" # record separator
|
10
|
+
|
11
|
+
@cache = {}
|
12
|
+
|
13
|
+
#
|
14
|
+
# Load the entire postal code database into memory.
|
15
|
+
#
|
16
|
+
def self.load_cache
|
17
|
+
open(DB) do |db|
|
18
|
+
db.each_line do |line|
|
19
|
+
pcode, city, state = line.rstrip.split(FS)
|
20
|
+
@cache[pcode] = [city, state]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Clear the in-memory postal code cache.
|
27
|
+
#
|
28
|
+
def self.clear_cache
|
29
|
+
@cache.clear
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Return the city for the given postal code.
|
34
|
+
#
|
35
|
+
def self.city postal_code
|
36
|
+
fetch(postal_code)[CityOffset]
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Return the state (two-letter abbreviation) for the given postal code.
|
41
|
+
#
|
42
|
+
def self.state postal_code
|
43
|
+
fetch(postal_code)[StateOffset]
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# * All postal code parameters must be strings.
|
48
|
+
# * Postal codes are not calculable, therefore should not be numeric.
|
49
|
+
# * A postal code with a leading zero would be an octal. However, *08880* is
|
50
|
+
# an invalid octal numeric, yet a valid postal code.
|
51
|
+
#
|
52
|
+
def self.valid_format? postal_code
|
53
|
+
postal_code =~ /^\d{5}(-\d{4})?$/
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Fetch postal code record.
|
58
|
+
#
|
59
|
+
def self.fetch postal_code
|
60
|
+
return [nil, nil] unless valid_format? postal_code
|
61
|
+
pcode = postal_code[0..4] # use first 5 characters; ignore ZIP+4 extension
|
62
|
+
|
63
|
+
if @cache.has_key? pcode
|
64
|
+
@cache[pcode]
|
65
|
+
else
|
66
|
+
records = %x[grep "^#{pcode}#{FS}" #{DB}].split(RS)
|
67
|
+
|
68
|
+
if records.empty?
|
69
|
+
[nil, nil]
|
70
|
+
else
|
71
|
+
@cache[pcode] = records.last.split(FS)[1..2]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private_class_method :fetch, :valid_format?
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
#
|
2
|
+
# Postal Code resolution service for network clients.
|
3
|
+
#
|
4
|
+
# Could implement a DRb and TCP/UDP server.
|
5
|
+
#
|
6
|
+
# Requires in user code would look like this:
|
7
|
+
#
|
8
|
+
# require 'postal_code/tcpserver'
|
9
|
+
#
|
10
|
+
# or
|
11
|
+
#
|
12
|
+
# require 'postal_code/drbserver'
|
13
|
+
#
|
14
|
+
|
15
|
+
__END__
|
16
|
+
|
17
|
+
require_relative '../postal_code'
|
18
|
+
|
19
|
+
# DRb
|
20
|
+
require 'drb'
|
21
|
+
|
22
|
+
$SAFE = 1 # disable eval() and friends
|
23
|
+
URI = "druby://127.0.0.1:8787"
|
24
|
+
|
25
|
+
class PostalCode::DRbServer
|
26
|
+
|
27
|
+
def initialize host, port
|
28
|
+
end
|
29
|
+
|
30
|
+
def postal_code code
|
31
|
+
# could return more than one line
|
32
|
+
#zip, city, state = %x[grep ^#{code} #{DB}].split("\t")
|
33
|
+
#Time.now
|
34
|
+
#"#{zip},#{city},#{state}"
|
35
|
+
code
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
DRb.start_service(URI, PCode.new)
|
41
|
+
DRb.thread.join
|
42
|
+
|
43
|
+
|
44
|
+
__END__
|
45
|
+
|
46
|
+
# TCP
|
47
|
+
require 'socket'
|
48
|
+
|
49
|
+
server = TCPServer.new('127.0.0.1', 5100)
|
50
|
+
loop do
|
51
|
+
client = server.accept
|
52
|
+
client.gets
|
53
|
+
client.puts 'Hello!'
|
54
|
+
client.puts "Time is #{Time.now}"
|
55
|
+
client.close
|
56
|
+
end
|
data/postal_code.gemspec
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Gem::Specification.new('postal_code') do |s|
|
2
|
+
s.version = File.read('lib/postal_code.rb')[/VERSION = '(.*)'/, 1]
|
3
|
+
s.author = 'Clint Pachl'
|
4
|
+
s.email = 'pachl@ecentryx.com'
|
5
|
+
s.homepage = 'http://ecentryx.com/gems/postal_code'
|
6
|
+
s.license = 'ISC'
|
7
|
+
s.summary = 'Postal Code Resolver'
|
8
|
+
s.description = 'Resolves a postal code to a corresponding city or state.'
|
9
|
+
s.files = Dir['README.rdoc', 'Rakefile', 'postal_code.gemspec',
|
10
|
+
'lib/**/*.rb', 'test/**/*', 'db/*.csv']
|
11
|
+
s.test_files = Dir['test/*.rb']
|
12
|
+
end
|
data/test/data/us.csv
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
gem 'minitest' # minitest in 1.9 stdlib is crufty
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'postal_code'
|
4
|
+
|
5
|
+
class PostalCodeTest < Minitest::Test
|
6
|
+
|
7
|
+
ProductionDB = PostalCode::DB.dup
|
8
|
+
TestDB = File.dirname(__FILE__) + '/data/us.csv'
|
9
|
+
PostalCode::DB.replace(TestDB)
|
10
|
+
|
11
|
+
def test_database_format
|
12
|
+
[ProductionDB, TestDB].each do |db|
|
13
|
+
[ %x[head -n1 #{db}],
|
14
|
+
%x[tail -n1 #{db}]
|
15
|
+
].each do |tuple|
|
16
|
+
assert_match(/^\d{5}\t.+\t[A-Z]{2}$/, tuple)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_postal_code_with_leading_zeros
|
22
|
+
pcode = '00001'
|
23
|
+
assert_equal 'City1', PostalCode.city(pcode)
|
24
|
+
assert_equal 'AA', PostalCode.state(pcode)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_postal_code_with_plus_4_extension
|
28
|
+
pcode = '00001-1234'
|
29
|
+
assert_equal 'City1', PostalCode.city(pcode)
|
30
|
+
assert_equal 'AA', PostalCode.state(pcode)
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_postal_code_found_in_single_city
|
34
|
+
pcode = '10000'
|
35
|
+
assert_equal 'City2', PostalCode.city(pcode)
|
36
|
+
assert_equal 'BA', PostalCode.state(pcode)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_postal_code_found_in_multiple_cities
|
40
|
+
pcode = '20000'
|
41
|
+
assert_equal 'City4', PostalCode.city(pcode)
|
42
|
+
assert_equal 'CA', PostalCode.state(pcode)
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_postal_code_not_found_in_any_city
|
46
|
+
pcode = '99999'
|
47
|
+
assert_equal nil, PostalCode.city(pcode)
|
48
|
+
assert_equal nil, PostalCode.state(pcode)
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_reactive_cache
|
52
|
+
PostalCode.clear_cache
|
53
|
+
assert_equal 0, PostalCode.instance_variable_get(:@cache).size
|
54
|
+
PostalCode.city('00001') # hit
|
55
|
+
assert_equal 1, PostalCode.instance_variable_get(:@cache).size
|
56
|
+
|
57
|
+
PostalCode.clear_cache
|
58
|
+
assert_equal 0, PostalCode.instance_variable_get(:@cache).size
|
59
|
+
PostalCode.city('99999') # miss
|
60
|
+
assert_equal 0, PostalCode.instance_variable_get(:@cache).size
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_proactive_cache
|
64
|
+
tuple_cnt = open(PostalCode::DB) {|db| db.readlines.size}
|
65
|
+
duplicate_cnt = 1
|
66
|
+
uniq_tuple_cnt = tuple_cnt - duplicate_cnt
|
67
|
+
|
68
|
+
assert_equal 4, tuple_cnt
|
69
|
+
PostalCode.clear_cache
|
70
|
+
assert_equal 0, PostalCode.instance_variable_get(:@cache).size
|
71
|
+
PostalCode.load_cache
|
72
|
+
assert_equal uniq_tuple_cnt, PostalCode.instance_variable_get(:@cache).size
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_postal_code_validations
|
76
|
+
refute PostalCode.send(:valid_format?, '99999').nil?
|
77
|
+
refute PostalCode.send(:valid_format?, '00001').nil?
|
78
|
+
refute PostalCode.send(:valid_format?, '00001-1234').nil?
|
79
|
+
assert PostalCode.send(:valid_format?, '00001-123').nil?
|
80
|
+
assert PostalCode.send(:valid_format?, '00001-').nil?
|
81
|
+
assert PostalCode.send(:valid_format?, 99999).nil?
|
82
|
+
assert PostalCode.send(:valid_format?, 00001).nil?
|
83
|
+
assert PostalCode.send(:valid_format?, '1').nil?
|
84
|
+
assert PostalCode.send(:valid_format?, 1).nil?
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: postal_code
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Clint Pachl
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-05-02 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Resolves a postal code to a corresponding city or state.
|
15
|
+
email: pachl@ecentryx.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.rdoc
|
21
|
+
- Rakefile
|
22
|
+
- postal_code.gemspec
|
23
|
+
- lib/postal_code/server.rb
|
24
|
+
- lib/postal_code.rb
|
25
|
+
- test/test_postal_code.rb
|
26
|
+
- test/data/us.csv
|
27
|
+
- db/us.csv
|
28
|
+
homepage: http://ecentryx.com/gems/postal_code
|
29
|
+
licenses:
|
30
|
+
- ISC
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
requirements: []
|
48
|
+
rubyforge_project:
|
49
|
+
rubygems_version: 1.8.23
|
50
|
+
signing_key:
|
51
|
+
specification_version: 3
|
52
|
+
summary: Postal Code Resolver
|
53
|
+
test_files:
|
54
|
+
- test/test_postal_code.rb
|