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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 81f8212570017ee9537ad0ff17af68e6d4abec2d
4
- data.tar.gz: c24abbd919537c6ebcc4d9f21f72abff19979613
3
+ metadata.gz: 167924f1ab89a79ad0abf1e38fc06346dadc10d8
4
+ data.tar.gz: b9c9d73b3facea10d02c92970fc2f7bfb8fa078c
5
5
  SHA512:
6
- metadata.gz: b6c6b76529f9741f02b1e46c743452b496483d72b89e2c4d0cb164646aa5764d6fd2ed3e9e63760cfc12f98606bc96066fb66ea50be1a12b5c3ca8906301b069
7
- data.tar.gz: 16f706497a1007b065a70aa50bcc08c3b94c7934da54f682601751065b8ce47727922a993089d11d7273f1e534e82eb0c861787f24b6131a2be73ea5006661e3
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.load_data
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
- #### Lookup Zip Code
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
- #### Lookup City
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
@@ -10,6 +10,10 @@ class String
10
10
  Zipcoder.city_info self, **kwargs
11
11
  end
12
12
 
13
+ def cities(**kwargs)
14
+ Zipcoder.cities self, **kwargs
15
+ end
16
+
13
17
  def to_zip
14
18
  self
15
19
  end
@@ -1,3 +1,3 @@
1
1
  module Zipcoder
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
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
- @@zip_data = nil
10
- def self.zip_data
11
- self.load_data if @@zip_data == nil
12
- @@zip_data
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.load_data
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
- @@zip_data = YAML.load(File.open(zip_data))
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.zip_data[zip.to_zip]
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
- self.zip_data.values.each { |info|
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
- # Normalize response
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
- # Get the infos associated with this city/state. While
90
- # getting those, store the min/max so we can create a
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
- # If there were no matches, return 0
104
- info = nil
186
+ normalized = nil
105
187
  elsif infos.count == 1
106
- # If there was 1 match, return it
107
- info = infos[0]
188
+ normalized = {
189
+ zip: infos[0][:zip],
190
+ lat: infos[0][:lat],
191
+ long: infos[0][:long],
192
+ }
108
193
  else
109
- # Create normalized object
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
- # Cache the value
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
@@ -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 "caches the unfiltered value" do
96
- expect {
97
- described_class.city_info "Austin, TX", keys: [:zip, :lat, :long]
98
- }.to change{ described_class.city_cache.values.count }.by(1)
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
- expect {
101
- described_class.city_info "Austin, TX"
102
- }.to change{ described_class.city_cache.values.count }.by(0)
93
+ end
103
94
 
104
- city_info = described_class.city_info "Austin, TX"
105
- expect(city_info[:city]).to eq("AUSTIN")
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("78701-78799")
108
- expect(city_info[:lat]).to eq(30.315)
109
- expect(city_info[:long]).to eq(-48.87)
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
- end
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 "zip lookup" do
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 "city lookup" do
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zipcoder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Chapman