locode 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,119 @@
1
+ # encoding: utf-8
2
+ require 'yaml'
3
+ require 'multi_json'
4
+
5
+ require_relative 'locode/version'
6
+ require_relative 'locode/location'
7
+
8
+ module Locode
9
+
10
+ def self.load_data
11
+ YAML.load(File.read(File.expand_path('../../data/yaml/dump.yml', __FILE__)))
12
+ end
13
+ private_class_method :load_data
14
+
15
+ ALL_LOCATIONS = load_data
16
+ private_constant :ALL_LOCATIONS
17
+
18
+ def self.seaports(limit = ALL_LOCATIONS.size)
19
+ ALL_LOCATIONS.select { |location| location.seaport? }.take(limit)
20
+ end
21
+
22
+ def self.rail_terminals(limit = ALL_LOCATIONS.size)
23
+ ALL_LOCATIONS.select { |location| location.rail_terminal? }.take(limit)
24
+ end
25
+
26
+ def self.road_terminals(limit = ALL_LOCATIONS.size)
27
+ ALL_LOCATIONS.select { |location| location.road_terminal? }.take(limit)
28
+ end
29
+
30
+ def self.airport(limit = ALL_LOCATIONS.size)
31
+ ALL_LOCATIONS.select { |location| location.airport? }.take(limit)
32
+ end
33
+
34
+ def self.postal_exchange_offices(limit = ALL_LOCATIONS.size)
35
+ ALL_LOCATIONS.select { |location| location.postal_exchange_office? }.take(limit)
36
+ end
37
+
38
+ def self.inland_clearance_depots(limit = ALL_LOCATIONS.size)
39
+ ALL_LOCATIONS.select { |location| location.inland_clearance_depot? }.take(limit)
40
+ end
41
+
42
+ # the spec says these are currently just oil platforms
43
+ def self.fixed_transport_functions(limit = ALL_LOCATIONS.size)
44
+ ALL_LOCATIONS.select { |location| location.fixed_transport_functions? }.take(limit)
45
+ end
46
+
47
+ def self.border_crossing_functions(limit = ALL_LOCATIONS.size)
48
+ ALL_LOCATIONS.select { |location| location.border_crossing? }.take(limit)
49
+ end
50
+
51
+ # Public: Find Locations that partially match the Search String.
52
+ # This means you can search by just the country code or a
53
+ # whole LOCODE.
54
+ #
55
+ # search_string - The string that will be used in the LOCODE search.
56
+ #
57
+ # Examples
58
+ #
59
+ # Locode.find_by_locode('US')
60
+ # #=> [<Locode::Location: 'US NYC'>,
61
+ # <Locode::Location: 'US LAX'>, ... ]
62
+ #
63
+ # Locode.find_by_locode('DE HAM')
64
+ # #=> [<Locode::Location: 'DE HAM'>]
65
+ #
66
+ # Locode.find_by_locode('foobar')
67
+ # #=> []
68
+ #
69
+ # Returns an Array of Location
70
+ def self.find_by_locode(search_string)
71
+ return [] unless search_string && search_string.is_a?(String)
72
+ ALL_LOCATIONS.select { |location| location.locode.start_with?(search_string.strip.upcase) }
73
+ end
74
+
75
+ # Public: Find Locations whose full name or full name without diacritics
76
+ # matches the search string
77
+ #
78
+ # search_string - The string that will be used in the LOCODE search.
79
+ #
80
+ # Examples
81
+ #
82
+ # Locode.find_by_name('Göteborg')
83
+ # #=> [<Locode::Location: 'SE GOT'>]
84
+ #
85
+ # Locode.find_by_name('Gothenburg')
86
+ # #=> [<Locode::Location: 'SE GOT'>]
87
+ #
88
+ # Returns an Array of Location because the name might not be unique
89
+ def self.find_by_name(search_string)
90
+ return [] unless search_string && search_string.is_a?(String)
91
+ ALL_LOCATIONS.select do |location|
92
+ names = []
93
+ names << location.full_name if location.full_name
94
+ names << location.full_name_without_diacritics if location.full_name_without_diacritics
95
+ names += location.alternative_full_names
96
+ names += location.alternative_full_names_without_diacritics
97
+ names.map { |name| name.downcase }.any? { |name| name.start_with?(search_string.strip.downcase) }
98
+ end
99
+ end
100
+
101
+ # Public: Find locations for a specific country with a specific function
102
+ #
103
+ # country_code - ISO 3166 alpha-2 Country Code String to filter locations by country
104
+ # function - Integer or :B that specifies the function of the location
105
+ # limit - Integer to specify how many locations you want
106
+ #
107
+ # Examples
108
+ #
109
+ # Locode.find_by_country_and_function('BE', 1)
110
+ # #=> [<Locode::Location: 'BE ANR'>, ..]
111
+ #
112
+ # Returns an Array of Locations that satisfy the above conditions
113
+ def self.find_by_country_and_function(country_code, function, limit = ALL_LOCATIONS.size)
114
+ return [] unless country_code.to_s =~ /^[A-Z]{2}$/
115
+ return [] unless function.to_s =~ /^[1-7]{1}|:B{1}$/
116
+
117
+ ALL_LOCATIONS.select { |location| location.country_code == country_code && location.function_classifier.include?(function) }.take(limit)
118
+ end
119
+ end
@@ -0,0 +1,507 @@
1
+ # encoding: utf-8
2
+
3
+ module Locode
4
+ class Location
5
+ # Public: Initializes a new Location
6
+ #
7
+ # location_attributes - A Hash of the following structure
8
+ # {
9
+ # country_code: String | Symbol
10
+ # city_code: String | Symbol
11
+ # full_name: String
12
+ # full_name_without_diacritics: String
13
+ # subdivision: String | Symbol
14
+ # function_classifier: String | Array
15
+ # status: String | Symbol
16
+ # date: String
17
+ # iata_code: String | nil
18
+ # coordinates: String | nil
19
+ # }
20
+ #
21
+ # Examples
22
+ #
23
+ # Locode::Location.new
24
+ # #=> <Locode::Location: invalid location>
25
+ #
26
+ # location_attributes = {
27
+ # country_code: 'US',
28
+ # city_code: 'NYC',
29
+ # full_name: 'New York',
30
+ # full_name_without_diacritics: 'New York',
31
+ # subdivision: 'NY',
32
+ # function_classifier: '12345---',
33
+ # status: 'AI',
34
+ # date: '0401',
35
+ # iata_code: '',
36
+ # coordinates: '4042N 07400W'
37
+ # }
38
+ #
39
+ # Locode::Location.new(location_attributes)
40
+ # #=> <Locode::Location: 'US NYC'>
41
+ #
42
+ #
43
+ def initialize(location_attributes)
44
+ location_attributes.each do |k,v|
45
+ begin
46
+ send("#{k}=", v) if !v.nil?
47
+ rescue NoMethodError
48
+ end
49
+ end
50
+ end
51
+
52
+ # Internal: Once we are done parsing the csv files we no longer want to allow
53
+ # changes to the alternative_full_names or
54
+ # alternative_full_names_without_diacritics.
55
+ #
56
+ # Returns nothing
57
+ def parsing_completed
58
+ private :alternative_full_names=,
59
+ :alternative_full_names_without_diacritics=,
60
+ :parsing_completed
61
+ end
62
+
63
+ # Public: UN/LOCODE
64
+ #
65
+ # Examples
66
+ #
67
+ # Locode.find_by_locode('US NYC').first.locode
68
+ # #=> 'US NYC'
69
+ #
70
+ # Returns a String that represents the UN/LOCODE
71
+ def locode
72
+ "#{country_code.to_s} #{city_code.to_s}".strip
73
+ end
74
+
75
+ # Public: ISO 3166 alpha-2 Country Code
76
+ #
77
+ # Examples
78
+ #
79
+ # Locode.find_by_locode('US NYC').first.country_code
80
+ # #=> 'US'
81
+ #
82
+ # Returns a String containing the country code or an empty string.
83
+ def country_code
84
+ @country_code.to_s
85
+ end
86
+
87
+ # Public: Three letter code for a place
88
+ #
89
+ # Examples
90
+ #
91
+ # Locode.find_by_locode('US NYC').first.city_code
92
+ # #=> 'NYC'
93
+ #
94
+ # Returns a String containing the city code or an empty string.
95
+ def city_code
96
+ @city_code.to_s
97
+ end
98
+
99
+ # Public: The name of a location
100
+ #
101
+ # Examples
102
+ #
103
+ # Locode.find_by_locode('SE GOT').first.full_name
104
+ # #=> 'Göteborg'
105
+ #
106
+ # Returns a String containing the full name of the location or an
107
+ # empty string.
108
+ def full_name
109
+ @full_name
110
+ end
111
+
112
+ # Public: The alternative names of a location
113
+ #
114
+ # Examples
115
+ #
116
+ # Locode.find_by_locode('SE GOT').first.alternative_full_names
117
+ # #=> ['Gothenburg']
118
+ #
119
+ # Returns an Array of Strings containing the alternative full names of the
120
+ # location or an empty array.
121
+ def alternative_full_names
122
+ @alternative_full_names ||= []
123
+ end
124
+
125
+ # Internal: adds the alternative full name of a location to the list of alternatives
126
+ # This might not be coherent with the normal semantics of a setter
127
+ # but I think it is ok since it is just a private method.
128
+ #
129
+ # Returns nothing
130
+ def alternative_full_names=(alternative_full_name)
131
+ if alternative_full_name && alternative_full_name.is_a?(String)
132
+ @alternative_full_names ||= []
133
+ @alternative_full_names << alternative_full_name.strip
134
+ end
135
+ end
136
+
137
+ # Public: The name of the location, but all non-latin-base characters
138
+ # are converted.
139
+ #
140
+ # Examples
141
+ #
142
+ # Locode.find_by_locode('SE GOT').first.full_name_without_diacritics
143
+ # #=> 'Goteborg'
144
+ #
145
+ # Returns a String which contains the the full name without
146
+ # diacritics or an empty string
147
+ def full_name_without_diacritics
148
+ @full_name_without_diacritics
149
+ end
150
+
151
+ # Public: The alternative names of the location, but all non-latin-base
152
+ # characters are converted.
153
+ #
154
+ # Examples
155
+ #
156
+ # Locode.find_by_locode('SE GOT').first.alternative_full_names_without_diacritics
157
+ # #=> ['Gothenburg']
158
+ #
159
+ # Returns an Array of Strings containing the alternative full names without
160
+ # diacritics of the location or an empty array.
161
+ def alternative_full_names_without_diacritics
162
+ @alternative_full_names_without_diacritics ||= []
163
+ end
164
+
165
+ # Internal: adds the alternative full name without diacritics of the location
166
+ # to the list of alternatives.
167
+ # This might not be coherent with the normal semantics of a setter
168
+ # but I think it is ok since it is just a private method.
169
+ #
170
+ # Returns nothing
171
+ def alternative_full_names_without_diacritics=(alternative_full_name_without_diacritics)
172
+ if alternative_full_name_without_diacritics && alternative_full_name_without_diacritics.is_a?(String)
173
+ @alternative_full_names_without_diacritics ||= []
174
+ @alternative_full_names_without_diacritics << alternative_full_name_without_diacritics.strip
175
+ end
176
+ end
177
+
178
+ # Public: The ISO 1 to 3 character alphabetic and/or numeric code for the
179
+ # administrative division (state, province, department, etc.)
180
+ # of the country, as included in ISO 3166-2/1998. Only the
181
+ # latter part of the complete ISO 3166-2 code element (after
182
+ # the hyphen) is shown.
183
+ #
184
+ # Examples
185
+ #
186
+ # Locode.find_by_locode('US NYC').first.subdivision
187
+ # #=> 'NY'
188
+ #
189
+ # Locode.find_by_locode('SE GOT').first.subdivision
190
+ # #=> 'O'
191
+ #
192
+ # Returns a String representing the subdivision or an empty string
193
+ def subdivision
194
+ @subdivision
195
+ end
196
+
197
+ # Public: contains a 1-digit function classifier code for the location
198
+ #
199
+ # Examples
200
+ #
201
+ # Locode.find_by_locode('US NYC').first.function_classifier
202
+ # #=> [1, 2, 3, 4, 5]
203
+ #
204
+ # Returns an Array containing Integer or :B with the following
205
+ # meanings:
206
+ # 1 = seaport, any port with the possibility of transport via water
207
+ # 2 = rail terminal
208
+ # 3 = road terminal
209
+ # 4 = airport
210
+ # 5 = postal exchange office
211
+ # 6 = Inland Clearance Depot – ICD or "Dry Port"
212
+ # 7 = reserved for fixed transport functions (e.g. oil platform)
213
+ # :B = border crossing
214
+ def function_classifier
215
+ @function_classifier
216
+ end
217
+
218
+ # Public: Indicates the status of the entry by a 2-character code
219
+ #
220
+ # Examples
221
+ #
222
+ # Locode.find_by_locode('US NYC').first.status
223
+ # #=> :AI
224
+ #
225
+ # Returns a Symbol with the following meaning or nil
226
+ # :AA = Approved by competent national government agency
227
+ # :AC = Approved by Customs Authority
228
+ # :AF = Approved by national facilitation body
229
+ # :AI = Code adopted by international organisation (IATA or ECLAC)
230
+ # :AM = Approved by the UN/LOCODE Maintenance Agency
231
+ # :AS = Approved by national standardisation body
232
+ # :AQ = Entry approved, functions not verified
233
+ # :RL = Recognised location - Existence and representation of location name
234
+ # confirmed by check against nominated gazetteer or other
235
+ # reference work
236
+ # :RN = Request from credible national sources for locations in their own country
237
+ # :RQ = Request under consideration
238
+ # :RR = Request rejected
239
+ # :QQ = Original entry not verified since date indicated
240
+ # :UR = Entry included on user's request; not officially approved
241
+ # :XX = Entry that will be removed from the next issue of UN/LOCODE
242
+ #
243
+ def status
244
+ @status
245
+ end
246
+
247
+ # Public: The date the location was added or updated
248
+ #
249
+ # Examples
250
+ #
251
+ # Locode.find_by_locode('US NYC').first.date
252
+ # #=> '0401'
253
+ #
254
+ # Returns a String containing the date the location was added to the
255
+ # list of LOCODEs. The meaning of the date values is the following:
256
+ # '0207' equals July 2002, '9501' equals January 1995
257
+ def date
258
+ @date
259
+ end
260
+
261
+ # Public: The IATA code for the location if different from the second
262
+ # part of the UN/LOCODE. Else the second part of the UN/LOCODE.
263
+ #
264
+ # Examples
265
+ #
266
+ # Locode.find_by_locode('SE GOT').first.iata_code
267
+ # #=> 'XWL'
268
+ #
269
+ # Locode.find_by_locode('US NYC').first.iata_code
270
+ # #=> 'NYC'
271
+ #
272
+ # Returns a String which is the IATA code if it is different from
273
+ # the city code of the LOCODE. Else it returns the city
274
+ # code.
275
+ #
276
+ def iata_code
277
+ @iata_code
278
+ end
279
+
280
+ # Public: The coordinates of a location.
281
+ #
282
+ # Examples
283
+ #
284
+ # Locode.find_by_locode('SE GOT').first.coordinates
285
+ # #=> nil
286
+ #
287
+ # Locode.find_by_locode('SE GO2').first.coordinates
288
+ # #=> '5742N 01156E'
289
+ #
290
+ # Returns nil if no coordinates are associated with the Location
291
+ # otherwise it returns a String with the coordinates which
292
+ # represents these with two numbers and letters for the cardinal
293
+ # directions. The first followed by either N or S, the second by
294
+ # either E or W.
295
+ def coordinates
296
+ @coordinates
297
+ end
298
+
299
+ # Public: The String representation of the Location
300
+ #
301
+ # Examples
302
+ #
303
+ # Locode.find_by_locode('US NYC').first.to_s
304
+ # #=> <Locode::Location: 'US NYC'>
305
+ #
306
+ # Returns a String that represents the Location
307
+ def to_s
308
+ "<Locode::Location: '#{locode}'>"
309
+ end
310
+
311
+ # Public: The Hash representation of the Location
312
+ #
313
+ # Examples
314
+ #
315
+ # Locode.find_by_locode('BE ANR').first.to_h
316
+ # #=> {:country_code=>"BE", ... }
317
+ #
318
+ # Returns a Hash that represents the Location
319
+ def to_h
320
+ {
321
+ country_code: country_code,
322
+ city_code: city_code,
323
+ full_name: full_name,
324
+ full_name_without_diacritics: full_name_without_diacritics,
325
+ subdivision: subdivision,
326
+ function_classifier: function_classifier,
327
+ status: status,
328
+ date: date,
329
+ iata_code: iata_code,
330
+ coordinates: coordinates
331
+ }
332
+ end
333
+
334
+ # Public: The JSON representation of the Location
335
+ #
336
+ # Examples
337
+ #
338
+ # Locode.find_by_locode('US NYC').first.to_json
339
+ # #=> {"country_code":"US","city_code":"NYC", ...}
340
+ #
341
+ # Returns a JSON that represents the Location
342
+ def to_json
343
+ MultiJson.dump(self.to_h)
344
+ end
345
+
346
+ # Public: To check whether the Locations attributes are all
347
+ # initialized correctly.
348
+ #
349
+ # Examples
350
+ #
351
+ # Locode.find_by_locode('US NYC').first.valid?
352
+ # #=> true
353
+ #
354
+ # Locode::Location.new.valid?
355
+ # #=> false
356
+ #
357
+ # Returns true or false
358
+ def valid?
359
+ country_code && country_code.size == 2 && city_code && city_code.size == 3
360
+ end
361
+
362
+ private
363
+
364
+ # Internal: sets the ISO 3166 alpha-2 Country Code
365
+ #
366
+ # Returns nothing
367
+ def country_code=(country_code)
368
+ if country_code
369
+ if country_code.is_a?(String)
370
+ @country_code = country_code.strip.upcase.to_sym
371
+ elsif country_code.is_a?(Symbol)
372
+ @country_code = country_code.upcase
373
+ end
374
+ end
375
+ end
376
+
377
+ # Internal: sets the three letter code for a place
378
+ #
379
+ # Returns nothing
380
+ def city_code=(city_code)
381
+ if city_code
382
+ if city_code.is_a?(String)
383
+ @city_code = city_code.strip.upcase.to_sym
384
+ elsif city_code.is_a?(Symbol)
385
+ @city_code = city_code.upcase
386
+ end
387
+ end
388
+ end
389
+
390
+ # Internal: sets the name of a location
391
+ #
392
+ # Returns nothing
393
+ def full_name=(full_name)
394
+ if full_name && full_name.is_a?(String)
395
+ @full_name = full_name.strip
396
+ end
397
+ end
398
+
399
+ # Internal: sets the full name without diacritics of the location
400
+ #
401
+ # Returns nothing
402
+ def full_name_without_diacritics=(full_name_without_diacritics)
403
+ if full_name_without_diacritics && full_name_without_diacritics.is_a?(String)
404
+ @full_name_without_diacritics = full_name_without_diacritics.strip
405
+ end
406
+ end
407
+
408
+ # Internal: sets the ISO 1 to 3 character alphabetic and/or numeric code
409
+ #
410
+ # Returns nothing
411
+ def subdivision=(subdivision)
412
+ if subdivision && subdivision.is_a?(String)
413
+ @subdivision = subdivision.strip
414
+ end
415
+ end
416
+
417
+ # Internal: sets the 1-digit function classifier code for the location
418
+ #
419
+ # Returns nothing
420
+ def function_classifier=(function_classifier)
421
+ if function_classifier
422
+ if function_classifier.is_a?(String)
423
+ @function_classifier = function_classifier.strip.chars.select do |char|
424
+ char.to_i.between?(1, 7) || char.upcase.to_s == "B"
425
+ end.map do |char|
426
+ if char.to_i.between?(1, 7)
427
+ char.to_i
428
+ else
429
+ char.upcase.to_sym
430
+ end
431
+ end
432
+ elsif function_classifier.is_a?(Array)
433
+ @function_classifier = function_classifier.flatten
434
+ end
435
+ end
436
+ end
437
+
438
+ # Internal: sets the status indicator of the entry
439
+ #
440
+ # Returns nothing
441
+ def status=(status)
442
+ if status && (status.is_a?(String) || status.is_a?(Symbol))
443
+ @status = status.upcase.to_sym
444
+ end
445
+ end
446
+
447
+ # Internal: sets the date the location was added or updated
448
+ #
449
+ # Returns nothing
450
+ def date=(date)
451
+ if date && date.is_a?(String)
452
+ @date = date.strip
453
+ end
454
+ end
455
+
456
+ # Internal: sets the IATA code for the location
457
+ #
458
+ # Returns nothing
459
+ def iata_code=(iata_code)
460
+ if iata_code && iata_code.is_a?(String)
461
+ @iata_code = iata_code.strip
462
+ end
463
+ end
464
+
465
+ # Internal: sets the coordinates of a location.
466
+ #
467
+ # Returns nothing
468
+ def coordinates=(coordinates)
469
+ if coordinates && coordinates.is_a?(String)
470
+ @coordinates = coordinates.strip
471
+ end
472
+ end
473
+
474
+ # Internal: Used to link name of a status and the status number
475
+ def self.functions_name_identifier
476
+ {
477
+ seaport: 1,
478
+ rail_terminal: 2,
479
+ road_terminal: 3,
480
+ airport: 4,
481
+ postal_exchange_office: 5,
482
+ inland_clearance_depot: 6,
483
+ fixed_transport_functions: 7,
484
+ border_crossing: :B
485
+ }
486
+ end
487
+
488
+ # Dynamically defines the following predicates:
489
+ #
490
+ # seaport?
491
+ # rail_terminal?
492
+ # road_terminal?
493
+ # airport?
494
+ # postal_exchange_office?
495
+ # inland_clearance_depot?
496
+ # fixed_transport_functions?
497
+ # border_crossing?
498
+ #
499
+ # Each returns: true | false
500
+ #
501
+ functions_name_identifier.each do |key, value|
502
+ define_method "#{key}?" do
503
+ function_classifier.include?(value)
504
+ end
505
+ end
506
+ end
507
+ end