locode 0.1.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.
@@ -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