/
zipcodes.rb
300 lines (259 loc) · 10.2 KB
/
zipcodes.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# encoding: utf-8
require 'autocompletion'
require 'unicode'
module SwissMatch
# Represents a collection of SwissMatch::ZipCode objects, and provides a query interface.
class ZipCodes
include Enumerable
# @param [Array<SwissMatch::ZipCode>] zip_codes
# The SwissMatch::ZipCode objects this SwissMatch::ZipCodes should contain
def initialize(zip_codes)
@zip_codes = zip_codes
reset!
end
# @private
# Reinitialize all caching instance variables
def reset!
@by_ordering_number = nil
@by_code = nil
@by_full_code = nil
@by_code_and_name = nil
@by_name = nil
@autocomplete = nil
self
end
# A convenience method to get one or many zip codes by code, code and add-on, code and city or just
# city.
# There are various allowed styles to pass those values.
# All numeric values can be passed either as Integer or String.
# You can pass the code and add-on as six-digit number, or you can pass the code
# as four digit number plus either the add-on or name as second parameter. Or you can
# pass the code alone, or the name alone.
#
# @example All usage styles
# zip_codes[805200] # zip code 8052, add-on 0
# zip_codes["805200"] # zip code 8052, add-on 0
# zip_codes[8052, 0] # zip code 8052, add-on 0
# zip_codes["8052", 0] # zip code 8052, add-on 0
# zip_codes[8052, "0"] # zip code 8052, add-on 0
# zip_codes["8052", 0] # zip code 8052, add-on 0
# zip_codes[8052, "Zürich"] # zip code 8052, add-on 0
# zip_codes["8052", "Zürich"] # zip code 8052, add-on 0
# zip_codes[8052] # all zip codes with code 8052
# zip_codes["8052"] # all zip codes with code 8052
# zip_codes["Zürich"] # all zip codes with name "Zürich"
#
# @see #by_code_and_add_on Get a zip code by code and add-on directly
# @see #by_code_and_name Get a zip code by code and name directly
# @see #by_name Get a collection of zip codes by name directly
# @see #by_ordering_number Get a zip code by its ONRP directly (#[] can't do that)
#
# @param [String, Integer] key
# Either the zip code, zip code and add-on
# @return [SwissMatch::ZipCode, SwissMatch::ZipCodes]
# Either a SwissMatch::ZipCodes collection of zip codes or a single SwissMatch::ZipCode, depending on
# the argument you pass.
def [](key, add_on=nil)
case key
when /\A(\d{4})(\d\d)\z/
by_code_and_add_on($1.to_i, $2.to_i)
when 100_000..999_999
by_code_and_add_on(*key.divmod(100))
when 0..9999, /\A\d{4}\z/
case add_on
when nil
by_code(key.to_i)
when 0..99, /\A\d+\z/
by_code_and_add_on(key.to_i, add_on.to_i)
when String
by_code_and_name(key.to_i, add_on)
else
raise ArgumentError,
"Expected a String, an Integer between 0 and 99, or a String containing an integer between 0 and 99, " \
"but got #{key.class}: #{key.inspect}"
end
when String
by_name(key)
else
raise ArgumentError,
"Expected a String, an Integer between 1000 and 9999, or an " \
"Integer between 100_000 and 999_999, but got #{key.class}:" \
"#{key.inspect}"
end
end
# Calls the block once for every SwissMatch::ZipCode in this SwissMatch::ZipCodes
# instance, passing that zip_code as a parameter.
# The order is the same as the instance was constructed.
#
# @yield [zip_code]
# @yieldparam [SwissMatch::ZipCode] zip_code
#
# @return [self] Returns self
def each(&block)
@zip_codes.each(&block)
self
end
# Calls the block once for every SwissMatch::ZipCode in this SwissMatch::ZipCodes
# instance, passing that zip_code as a parameter.
# The order is the reverse of what the instance was constructed.
#
# @yield [zip_code]
# @yieldparam [SwissMatch::ZipCode] zip_code
#
# @return [self] Returns self
def reverse_each(&block)
@zip_codes.reverse_each(&block)
self
end
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection with all SwissMatch::ZipCode objects for which the block
# returned true (or a trueish value)
def select(*args, &block)
ZipCodes.new(@zip_codes.select(*args, &block))
end
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection with all SwissMatch::ZipCode objects for which the block
# returned false (or a falseish value)
def reject(*args, &block)
ZipCodes.new(@zip_codes.reject(*args, &block))
end
# @see Enumerable#sort
#
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection sorted by the block.
def sort(*args, &block)
ZipCodes.new(@zip_codes.sort(*args, &block))
end
# @see Enumerable#sort_by
#
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection sorted by the block.
def sort_by(*args, &block)
ZipCodes.new(@zip_codes.sort_by(*args, &block))
end
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection with zip codes that are currently active/in use.
def active(date=Date.today, &block)
select { |zip_code| zip_code.in_use?(date) }
end
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection with zip codes that are currently inactive/not in use.
# A zip code is not in use if it has been either retired or is only recorded for future use.
def inactive(date=Date.today, &block)
reject { |zip_code| zip_code.in_use?(date) }
end
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection with zip codes having names that match
# the given string (prefix search on all languages)
def autocomplete(string)
return ZipCodes.new([]) if string.empty? # shortcut
@autocomplete ||= AutoCompletion.map_keys(@zip_codes) { |zip_code|
zip_code.transliterated_names
}
words = SwissMatch.transliterated_words(string)
ZipCodes.new(@autocomplete.complete(*words))
end
# @return [Array<String>]
# An array of ZipCode names which match the given string in an autocompletion.
# Sorted alphabetically (Umlaut-aware)
def autocompleted_names(name)
name_dc = Unicode.downcase(name)
len = name_dc.length
base = autocomplete(name)
names = base.flat_map { |zip_code|
zip_code.reverse_name_transliteration_map.select { |transliterated_name, real_names|
Unicode.downcase(transliterated_name[0, len]) == name_dc
}.values.flatten(1)
}
names.uniq.sort(&Unicode.method(:strcmp))
end
# @return [Array<String>]
# An array of ZipCode names suitable for presentation of a select.
def names_for_select(language=nil)
if language
names = base.flat_map { |zip_code| [zip_code.name, zip_code.suggested_name(I18n.language)] }
else
names = base.map(&:name)
end
names.uniq.sort(&Unicode.method(:strcmp))
end
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection with zip codes of type 10 and 20.
def residential
with_type(10, 20)
end
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection consisting only of zip codes having the given type(s).
def with_type(*types)
select { |zip_code| types.include?(zip_code.type) }
end
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection consisting only of zip codes not having the given type(s).
def without_type(*types)
reject { |zip_code| types.include?(zip_code.type) }
end
# @return [SwissMatch::ZipCode]
# The SwissMatch::ZipCode with the given ordering number (ONRP).
def by_ordering_number(onrp)
@by_ordering_number ||= Hash[@zip_codes.map { |c| [c.ordering_number, c] }]
@by_ordering_number[onrp]
end
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection with all SwissMatch::ZipCode objects having the given 4 digit code.
def by_code(code)
ZipCodes.new(by_code_lookup_table[code] || [])
end
# @return [SwissMatch::ZipCode]
# The SwissMatch::ZipCode with the given 4 digit code and given 2 digit code add-on.
def by_code_and_add_on(code, add_on)
by_full_code_lookup_table[code*100+add_on]
end
# @return [SwissMatch::ZipCode]
# The SwissMatch::ZipCode with the given 4 digit code and name in any language.
def by_code_and_name(code, name)
by_code_and_name_lookup_table[[code, name]]
end
# @return [SwissMatch::ZipCodes]
# A SwissMatch::ZipCodes collection with all SwissMatch::ZipCode objects having the given name.
def by_name(name)
@by_name ||= @zip_codes.each_with_object({}) { |zip_code, hash|
(zip_code.names + zip_code.names_short).map(&:to_s).uniq.each do |name|
hash[name] ||= []
hash[name] << zip_code
end
}
ZipCodes.new(@by_name[name] || [])
end
# @return [Integer] The number of SwissMatch::ZipCode objects in this collection.
def size
@zip_codes.size
end
# @return [Array<SwissMatch::ZipCode>]
# A SwissMatch::ZipCodes collection with all SwissMatch::ZipCode objects in this SwissMatch::ZipCodes.
def to_a
@zip_codes.dup
end
# @return [true, false] Whether this zip code collection contains any zip codes.
def empty?
@zip_codes.size.zero?
end
alias blank? empty?
# @private
# @see Object#inspect
def inspect
sprintf "\#<%s:%x size: %d>", self.class, object_id>>1, size
end
private
def by_code_lookup_table
@by_code ||= @zip_codes.group_by { |c| c.code }
end
def by_full_code_lookup_table
@by_full_code ||= Hash[@zip_codes.map { |c| [c.full_code, c] }]
end
def by_code_and_name_lookup_table
@by_code_and_name ||= Hash[@zip_codes.flat_map { |c|
(c.names + c.names_short).map(&:to_s).uniq.map { |name| [[c.code, name], c] }
}]
end
end
end