zipcoder 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +43 -4
- data/lib/ext/string.rb +4 -0
- data/lib/zipcoder/version.rb +1 -1
- data/lib/zipcoder.rb +106 -40
- data/spec/zipcoder_spec.rb +54 -23
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 167924f1ab89a79ad0abf1e38fc06346dadc10d8
|
4
|
+
data.tar.gz: b9c9d73b3facea10d02c92970fc2f7bfb8fa078c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 28eb144189cb04c3b097b5c8bb25635d2170999a89a0f3dd55c3d5f52383a6e43ca3251556e76c647abf38a52231ef10c22090f5581cfd78784fd8b93191d521
|
7
|
+
data.tar.gz: 206ae14c6b611a25f0d5bd790257083684e60b9c88313543ea3c6608126e3f39c4a7d48dc24ea71e7433159199b6dcba14aae847904e4a3bed3a24846da0671e
|
data/README.md
CHANGED
@@ -7,6 +7,10 @@ Gem for performing zip code lookup operations
|
|
7
7
|
|
8
8
|
## Revision History
|
9
9
|
|
10
|
+
- v0.3.0:
|
11
|
+
- API Change!! - intitialization method change from "load_data" to "load_cache"
|
12
|
+
- added city and state caches
|
13
|
+
- added "cities" call
|
10
14
|
- v0.2.0:
|
11
15
|
- Internal code rework
|
12
16
|
- API Change!! - changed "city,state" to return normalized info rather
|
@@ -42,17 +46,19 @@ when application is loaded, you can do the following RAILS example
|
|
42
46
|
|
43
47
|
``` ruby
|
44
48
|
require 'zipcoder'
|
45
|
-
Zipcoder.
|
49
|
+
Zipcoder.load_cache
|
46
50
|
```
|
47
51
|
|
48
|
-
This will immediately load the data structures
|
52
|
+
This will immediately load the data structures. Currently it takes roughly 3s
|
53
|
+
to create and import all of the data structures. I will look at ways to
|
54
|
+
reduce this later.
|
49
55
|
|
50
56
|
### Methods
|
51
57
|
|
52
58
|
The library overrides the String (and in some instances Integer) class to
|
53
59
|
provide the following methods
|
54
60
|
|
55
|
-
####
|
61
|
+
#### "zip".zip_info
|
56
62
|
|
57
63
|
This will return information about the zip code
|
58
64
|
|
@@ -84,7 +90,7 @@ puts "78748".zip_info(keys: [:city, :state])
|
|
84
90
|
# > {:city=>"AUSTIN", :state=>"TX"}
|
85
91
|
```
|
86
92
|
|
87
|
-
####
|
93
|
+
#### "city, state".city_info
|
88
94
|
|
89
95
|
This will return info about a city
|
90
96
|
|
@@ -117,6 +123,39 @@ puts "Austin, TX".city_info(keys: [:zip])
|
|
117
123
|
# > {:zip=>"78701-78799"}
|
118
124
|
```
|
119
125
|
|
126
|
+
#### "state".cities
|
127
|
+
|
128
|
+
This will return the cities in a state
|
129
|
+
|
130
|
+
``` ruby
|
131
|
+
require 'zipcoder'
|
132
|
+
|
133
|
+
puts "TX".cities
|
134
|
+
# > {:zip=>"78701-7879", :city=>"AUSTIN", :state=>"TX", :lat=>30.26, :long=>-97.74}
|
135
|
+
```
|
136
|
+
|
137
|
+
Notes:
|
138
|
+
|
139
|
+
- the "zip", "lat", "long" are the combined values from all of the
|
140
|
+
individual zip codes
|
141
|
+
- the library will normalize the key by removing all of the whitespace
|
142
|
+
and capitalizing the letters. So for example, "Austin, TX" becomes
|
143
|
+
"AUSTIN,TX"
|
144
|
+
- the library will cache the normalized cities to improve performance
|
145
|
+
on subsequent calls
|
146
|
+
|
147
|
+
##### "keys" argument
|
148
|
+
|
149
|
+
You can filter the keys that are returned by including the "keys" argument
|
150
|
+
as shown below
|
151
|
+
|
152
|
+
``` ruby
|
153
|
+
require 'zipcoder'
|
154
|
+
|
155
|
+
puts "Austin, TX".city_info(keys: [:zip])
|
156
|
+
# > {:zip=>"78701-78799"}
|
157
|
+
```
|
158
|
+
|
120
159
|
### Updating Data
|
121
160
|
|
122
161
|
The library is using the free public zip code data base located
|
data/lib/ext/string.rb
CHANGED
data/lib/zipcoder/version.rb
CHANGED
data/lib/zipcoder.rb
CHANGED
@@ -6,10 +6,12 @@ require "yaml"
|
|
6
6
|
module Zipcoder
|
7
7
|
|
8
8
|
# Data Structure Load and Lookup
|
9
|
-
@@
|
10
|
-
def self.
|
11
|
-
|
12
|
-
|
9
|
+
@@zip_cache = nil
|
10
|
+
def self.zip_cache
|
11
|
+
if @@zip_cache == nil
|
12
|
+
self.load_cache
|
13
|
+
end
|
14
|
+
@@zip_cache
|
13
15
|
end
|
14
16
|
|
15
17
|
@@city_cache = {}
|
@@ -17,12 +19,52 @@ module Zipcoder
|
|
17
19
|
@@city_cache
|
18
20
|
end
|
19
21
|
|
22
|
+
@@state_cache = {}
|
23
|
+
def self.state_cache
|
24
|
+
@@state_cache
|
25
|
+
end
|
26
|
+
|
20
27
|
# Loads the data into memory
|
21
|
-
def self.
|
28
|
+
def self.load_cache
|
22
29
|
this_dir = File.expand_path(File.dirname(__FILE__))
|
23
30
|
|
31
|
+
# Load zip cache from file
|
24
32
|
zip_data = File.join(this_dir, 'data', 'zip_data.yml')
|
25
|
-
@@
|
33
|
+
@@zip_cache = YAML.load(File.open(zip_data))
|
34
|
+
|
35
|
+
# Iterate through zip codes to populate city and state data
|
36
|
+
city_states = {}
|
37
|
+
self.zip_cache.values.each do |info|
|
38
|
+
city = info[:city]
|
39
|
+
state = info[:state]
|
40
|
+
|
41
|
+
# Create the city lookups
|
42
|
+
city_state = "#{city},#{state}"
|
43
|
+
infos = city_states[city_state] || []
|
44
|
+
infos << info
|
45
|
+
city_states[city_state] = infos
|
46
|
+
end
|
47
|
+
|
48
|
+
# Normalize each city and populate the state cache
|
49
|
+
city_states.each do |city_state, infos|
|
50
|
+
state = infos[0][:state]
|
51
|
+
|
52
|
+
# Populate the City Cache
|
53
|
+
normalized = self.city_info(city_state, infos)
|
54
|
+
self.city_cache[city_state] = normalized
|
55
|
+
|
56
|
+
# Populate the State Cache
|
57
|
+
cities = self.state_cache[state] || []
|
58
|
+
cities << normalized
|
59
|
+
self.state_cache[state] = cities
|
60
|
+
end
|
61
|
+
|
62
|
+
# Sort the city arrays
|
63
|
+
self.state_cache.keys.each do |state|
|
64
|
+
infos = self.state_cache[state]
|
65
|
+
new_infos = infos.sort_by { |hsh| hsh[:city] }
|
66
|
+
self.state_cache[state] = new_infos
|
67
|
+
end
|
26
68
|
end
|
27
69
|
|
28
70
|
# Looks up zip code information
|
@@ -31,7 +73,7 @@ module Zipcoder
|
|
31
73
|
# If zip is not nil, then we are returning a single value
|
32
74
|
if zip != nil
|
33
75
|
# Get the info
|
34
|
-
info = self.
|
76
|
+
info = self.zip_cache[zip.to_zip]
|
35
77
|
|
36
78
|
# Inform callback that we have a match
|
37
79
|
block.call(info) if block != nil
|
@@ -41,12 +83,12 @@ module Zipcoder
|
|
41
83
|
else
|
42
84
|
# If zip is nil, then we are returning an array of values
|
43
85
|
|
44
|
-
infos = []
|
45
86
|
city_filter = kwargs[:city] != nil ? kwargs[:city].upcase : nil
|
46
87
|
state_filter = kwargs[:state] != nil ? kwargs[:state].upcase : nil
|
47
88
|
|
48
89
|
# Iterate through and only add the ones that match the filters
|
49
|
-
|
90
|
+
infos = []
|
91
|
+
self.zip_cache.values.each { |info|
|
50
92
|
if (city_filter == nil or info[:city] == city_filter) and
|
51
93
|
(state_filter == nil or info[:state] == state_filter)
|
52
94
|
infos << self._filter_hash_args(info, kwargs[:keys])
|
@@ -61,7 +103,7 @@ module Zipcoder
|
|
61
103
|
end
|
62
104
|
|
63
105
|
# Looks up city information
|
64
|
-
def self.city_info(city_state, **kwargs)
|
106
|
+
def self.city_info(city_state, infos=nil, **kwargs)
|
65
107
|
unless city_state.include? ','
|
66
108
|
raise Exception, "city/state must include ','"
|
67
109
|
end
|
@@ -78,7 +120,49 @@ module Zipcoder
|
|
78
120
|
city = components[0].strip.upcase
|
79
121
|
state = components[1].strip.upcase
|
80
122
|
|
81
|
-
#
|
123
|
+
# Get the infos
|
124
|
+
infos ||= self.zip_info(city: city, state: state)
|
125
|
+
info = self._normalize_city(infos)
|
126
|
+
if info != nil
|
127
|
+
info[:city] = city
|
128
|
+
info[:state] = state
|
129
|
+
end
|
130
|
+
|
131
|
+
# Cache the value
|
132
|
+
self.city_cache[cache_key] = info if info != nil
|
133
|
+
|
134
|
+
# Filter the args
|
135
|
+
self._filter_hash_args info, kwargs[:keys]
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns the cities in a state
|
139
|
+
def self.cities(state, **kwargs)
|
140
|
+
state = state.strip.upcase
|
141
|
+
|
142
|
+
# Filter the returned cities
|
143
|
+
infos = []
|
144
|
+
self.state_cache[state].each { |info|
|
145
|
+
infos << self._filter_hash_args(info, kwargs[:keys])
|
146
|
+
}
|
147
|
+
|
148
|
+
infos
|
149
|
+
end
|
150
|
+
|
151
|
+
# Filters arguments in return hash
|
152
|
+
def self._filter_hash_args(hash, keys)
|
153
|
+
return nil if hash == nil
|
154
|
+
|
155
|
+
if keys != nil
|
156
|
+
new_hash = {}
|
157
|
+
keys.each { |k| new_hash[k] = hash[k] }
|
158
|
+
hash = new_hash
|
159
|
+
end
|
160
|
+
hash
|
161
|
+
end
|
162
|
+
|
163
|
+
# Normalizes the values
|
164
|
+
def self._normalize_city(infos)
|
165
|
+
# Values
|
82
166
|
zip_min = 100000
|
83
167
|
zip_max = 0
|
84
168
|
lat_min = 200
|
@@ -86,10 +170,8 @@ module Zipcoder
|
|
86
170
|
long_min = 200
|
87
171
|
long_max = 0
|
88
172
|
|
89
|
-
#
|
90
|
-
|
91
|
-
# normalized version of this object
|
92
|
-
infos = self.zip_info(city: city, state: state) do |info|
|
173
|
+
# Iterate through the info and get min/max of zip/lat/long
|
174
|
+
infos.each do |info|
|
93
175
|
zip = info[:zip].to_i
|
94
176
|
zip_min = zip if zip < zip_min
|
95
177
|
zip_max = zip if zip > zip_max
|
@@ -99,40 +181,24 @@ module Zipcoder
|
|
99
181
|
long_max = info[:long] if info[:long] > long_max
|
100
182
|
end
|
101
183
|
|
184
|
+
# Create the normalized value
|
102
185
|
if infos.count == 0
|
103
|
-
|
104
|
-
info = nil
|
186
|
+
normalized = nil
|
105
187
|
elsif infos.count == 1
|
106
|
-
|
107
|
-
|
188
|
+
normalized = {
|
189
|
+
zip: infos[0][:zip],
|
190
|
+
lat: infos[0][:lat],
|
191
|
+
long: infos[0][:long],
|
192
|
+
}
|
108
193
|
else
|
109
|
-
|
110
|
-
info = {
|
194
|
+
normalized = {
|
111
195
|
zip: "#{zip_min.to_zip}-#{zip_max.to_zip}",
|
112
|
-
city: city,
|
113
|
-
state: state,
|
114
196
|
lat: (lat_min+lat_max)/2,
|
115
197
|
long: (long_min+long_max)/2
|
116
198
|
}
|
117
199
|
end
|
118
200
|
|
119
|
-
|
120
|
-
self.city_cache[cache_key] = info if info != nil
|
121
|
-
|
122
|
-
# Filter the args
|
123
|
-
self._filter_hash_args info, kwargs[:keys]
|
124
|
-
end
|
125
|
-
|
126
|
-
# Filters arguments in return hash
|
127
|
-
def self._filter_hash_args(hash, keys)
|
128
|
-
return nil if hash == nil
|
129
|
-
|
130
|
-
if keys != nil
|
131
|
-
new_hash = {}
|
132
|
-
keys.each { |k| new_hash[k] = hash[k] }
|
133
|
-
hash = new_hash
|
134
|
-
end
|
135
|
-
hash
|
201
|
+
normalized
|
136
202
|
end
|
137
203
|
|
138
204
|
end
|
data/spec/zipcoder_spec.rb
CHANGED
@@ -1,14 +1,7 @@
|
|
1
1
|
require "spec_helper"
|
2
|
+
require 'benchmark'
|
2
3
|
|
3
4
|
describe Zipcoder do
|
4
|
-
before(:all) do
|
5
|
-
described_class.load_data
|
6
|
-
end
|
7
|
-
|
8
|
-
before(:each) do
|
9
|
-
described_class.city_cache.clear
|
10
|
-
end
|
11
|
-
|
12
5
|
it "has a version number" do
|
13
6
|
expect(Zipcoder::VERSION).not_to be nil
|
14
7
|
end
|
@@ -92,27 +85,40 @@ describe Zipcoder do
|
|
92
85
|
expect(city_info[:long]).to eq(-48.87)
|
93
86
|
end
|
94
87
|
|
95
|
-
it
|
96
|
-
|
97
|
-
|
98
|
-
|
88
|
+
it 'returns nil on mismatch' do
|
89
|
+
city_info = described_class.city_info "Bad City, TX"
|
90
|
+
expect(city_info).to be_nil
|
91
|
+
end
|
99
92
|
|
100
|
-
|
101
|
-
described_class.city_info "Austin, TX"
|
102
|
-
}.to change{ described_class.city_cache.values.count }.by(0)
|
93
|
+
end
|
103
94
|
|
104
|
-
|
105
|
-
|
95
|
+
describe "#cities" do
|
96
|
+
it "returns the cities for a state" do
|
97
|
+
cities = described_class.cities "TX"
|
98
|
+
expect(cities.count).to eq(1170)
|
99
|
+
|
100
|
+
city_info = cities[0]
|
101
|
+
expect(city_info[:city]).to eq("ABBOTT")
|
106
102
|
expect(city_info[:state]).to eq("TX")
|
107
|
-
expect(city_info[:zip]).to eq("
|
108
|
-
expect(city_info[:lat]).to eq(
|
109
|
-
expect(city_info[:long]).to eq(-
|
103
|
+
expect(city_info[:zip]).to eq("76621")
|
104
|
+
expect(city_info[:lat]).to eq(31.88)
|
105
|
+
expect(city_info[:long]).to eq(-97.07)
|
110
106
|
end
|
111
|
-
|
107
|
+
it "returns the cities for a state filtered" do
|
108
|
+
cities = described_class.cities "TX", keys: [:zip, :city]
|
109
|
+
expect(cities.count).to eq(1170)
|
112
110
|
|
111
|
+
city_info = cities[0]
|
112
|
+
expect(city_info[:city]).to eq("ABBOTT")
|
113
|
+
expect(city_info[:state]).to be_nil
|
114
|
+
expect(city_info[:zip]).to eq("76621")
|
115
|
+
expect(city_info[:lat]).to be_nil
|
116
|
+
expect(city_info[:long]).to be_nil
|
117
|
+
end
|
118
|
+
end
|
113
119
|
|
114
120
|
describe("String") do
|
115
|
-
describe "
|
121
|
+
describe "zip_info" do
|
116
122
|
it "returns the info for a particular zip_code" do
|
117
123
|
zip_info = "78748".zip_info
|
118
124
|
expect(zip_info[:city]).to eq("AUSTIN")
|
@@ -132,7 +138,7 @@ describe Zipcoder do
|
|
132
138
|
end
|
133
139
|
end
|
134
140
|
|
135
|
-
describe "
|
141
|
+
describe "city_info" do
|
136
142
|
it "returns the info for a particular city" do
|
137
143
|
city_info = "Austin, TX".city_info
|
138
144
|
expect(city_info[:city]).to eq("AUSTIN")
|
@@ -150,6 +156,31 @@ describe Zipcoder do
|
|
150
156
|
expect(city_info[:long]).to be_nil
|
151
157
|
end
|
152
158
|
end
|
159
|
+
|
160
|
+
describe "cities" do
|
161
|
+
it "returns the cities for a state" do
|
162
|
+
cities = "TX".cities
|
163
|
+
expect(cities.count).to eq(1170)
|
164
|
+
|
165
|
+
city_info = cities[0]
|
166
|
+
expect(city_info[:city]).to eq("ABBOTT")
|
167
|
+
expect(city_info[:state]).to eq("TX")
|
168
|
+
expect(city_info[:zip]).to eq("76621")
|
169
|
+
expect(city_info[:lat]).to eq(31.88)
|
170
|
+
expect(city_info[:long]).to eq(-97.07)
|
171
|
+
end
|
172
|
+
it "returns the cities for a state filtered" do
|
173
|
+
cities = "TX".cities keys: [:zip, :city]
|
174
|
+
expect(cities.count).to eq(1170)
|
175
|
+
|
176
|
+
city_info = cities[0]
|
177
|
+
expect(city_info[:city]).to eq("ABBOTT")
|
178
|
+
expect(city_info[:state]).to be_nil
|
179
|
+
expect(city_info[:zip]).to eq("76621")
|
180
|
+
expect(city_info[:lat]).to be_nil
|
181
|
+
expect(city_info[:long]).to be_nil
|
182
|
+
end
|
183
|
+
end
|
153
184
|
end
|
154
185
|
|
155
186
|
describe("Integer") do
|