iata 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 176d98285f415c8a930e02e73de40009fcf0d9fd9fae998bbb54fc48de8d908f
4
+ data.tar.gz: 247d5d264d9304aa94392b7a067ec04862f55f7e2a9db50e6310bd4cf811535a
5
+ SHA512:
6
+ metadata.gz: e67416a8025bd8f6aa0de9d72aad336d7ad0190d6cb2fb5c2d7beb198182838ad573536923c4643ef132591118fc87af8386c79c15340a2237c698e89cfa9138
7
+ data.tar.gz: 18d0ac1d2a114631aaf0eb7c3378f60ed5b7b4eb992274883626e4a70ce5d4604dfa36e06dde3a54e644aefe766887f0ff7d2d69a3479880990480277ff8066e
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, Ribose Inc.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.adoc ADDED
@@ -0,0 +1,142 @@
1
+ = iata
2
+ :toc: macro
3
+ :homepage: https://github.com/metanorma/iata
4
+
5
+ `iata` is a Ruby gem that exposes the IATA (International Air Transport
6
+ Association) airport code list as a queryable in-memory registry.
7
+
8
+ The dataset is sourced from Wikidata (property P238, "IATA airport code") and
9
+ ships inside the gem as a JSON file so the registry works offline. Loading is
10
+ lazy — the first call to `Iata.registry` parses the bundled JSON once.
11
+
12
+ == Installation
13
+
14
+ Ruby 3.1 or newer is required.
15
+
16
+ Add to your Gemfile:
17
+
18
+ [source,ruby]
19
+ ----
20
+ gem 'iata'
21
+ ----
22
+
23
+ Or install directly:
24
+
25
+ [source,sh]
26
+ ----
27
+ gem install iata
28
+ ----
29
+
30
+ == Usage
31
+
32
+ [source,ruby]
33
+ ----
34
+ require 'iata'
35
+
36
+ # Lookup by 3-letter IATA code (case-insensitive)
37
+ Iata.find('PVG').name # => "Shanghai Pudong International Airport"
38
+ Iata['HKG'] # => #<Iata::Entry code="HKG" name="Hong Kong International Airport">
39
+
40
+ # Filters — single value or array (any-of)
41
+ Iata.where(country: 'CN').count # => ~200 Chinese airports
42
+ Iata.where(country: %w[CN HK]).count # => airports in China or Hong Kong
43
+
44
+ # Search by name (Regexp substring or case-insensitive String)
45
+ Iata.where(name: /international/i).count
46
+ Iata.where(name: 'narita').map(&:code) # => ["NRT"]
47
+
48
+ # Country listing
49
+ Iata.countries # => ["AE", "AR", "AT", ..., "ZW"] (200+ codes)
50
+ Iata.counts_by_country.first(5)
51
+ ----
52
+
53
+ === What's in an Entry
54
+
55
+ Each `Iata::Entry` exposes the fields the JSON-LD wire format populates:
56
+
57
+ [cols="1,2", options="header"]
58
+ |===
59
+ |Attribute|Description
60
+ |`code`|3-letter IATA airport code
61
+ |`name`|English-language name
62
+ |`wikidata_id`|Wikidata entity ID (Q-number) for cross-referencing
63
+ |`country_iso2`|ISO 3166-1 alpha-2 country code
64
+ |`country_name`|Display name for the country (from Wikidata)
65
+ |`latitude`, `longitude`|WGS-84 decimal degrees
66
+ |===
67
+
68
+ [source,ruby]
69
+ ----
70
+ entry = Iata.find('PVG')
71
+
72
+ entry.country # => "CN"
73
+ entry.country_name # => "China"
74
+ entry.coordinates # => #<Iata::Coordinates lat=31.1434 lon=121.8052>
75
+ entry.coordinates.distance_to(Iata.find('HKG').coordinates) # => ~1255 km
76
+ ----
77
+
78
+ == Data refresh
79
+
80
+ Wikidata updates daily; new airports are added as they receive IATA codes.
81
+
82
+ The gem ships two workflows to keep the bundled data fresh:
83
+
84
+ === `check-upstream` (scheduled, weekly)
85
+
86
+ `.github/workflows/check-upstream.yml` runs every Monday and queries
87
+ Wikidata for the current IATA airport count. If it differs from the bundled
88
+ count, the workflow opens a `data-update` issue with a refresh link.
89
+
90
+ === `update-data` (manual dispatch)
91
+
92
+ `.github/workflows/update-data.yml` is what a maintainer runs after the
93
+ weekly check flags drift. It re-queries Wikidata, commits the refreshed
94
+ `lib/iata/data/airports.json` to a branch, and opens a PR. Merging the PR
95
+ does not, by itself, publish a new gem — to ship a new version, trigger
96
+ the `release` workflow with `next_version=patch`.
97
+
98
+ End-to-end refresh flow:
99
+
100
+ . `check-upstream` opens issue: "Wikidata IATA count N (bundled: M)"
101
+ . Maintainer runs `update-data` workflow
102
+ . Workflow opens PR with refreshed `airports.json`
103
+ . Maintainer merges the PR
104
+ . Maintainer triggers `release` workflow with `next_version=patch`
105
+
106
+ Manual local equivalent:
107
+
108
+ [source,sh]
109
+ ----
110
+ bundle exec rake iata:fetch # refresh bundled airports.json
111
+ ----
112
+
113
+ == Cross-referencing with Wikidata
114
+
115
+ Each `Entry` carries its Wikidata Q-number so you can link out to the
116
+ authoritative record:
117
+
118
+ [source,ruby]
119
+ ----
120
+ entry = Iata.find('PVG')
121
+ entry.wikidata_id # => "Q86792"
122
+ # https://www.wikidata.org/wiki/Q86792
123
+ ----
124
+
125
+ == Development
126
+
127
+ [source,sh]
128
+ ----
129
+ bundle install # install dev deps
130
+ bundle exec rake # spec + rubocop
131
+ bundle exec rake iata:fetch # refresh bundled dataset
132
+ ----
133
+
134
+ == Data attribution
135
+
136
+ Bundled data is sourced from Wikidata (CC0). The IATA organisation does not
137
+ publish its code list under an open license; the Wikidata community
138
+ maintains IATA airport codes as structured data derived from public sources.
139
+
140
+ == License
141
+
142
+ BSD-2-Clause. See link:LICENSE[].
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Iata
4
+ # Geographic coordinates (WGS-84) for an IATA airport entry.
5
+ class Coordinates
6
+ attr_reader :latitude, :longitude
7
+
8
+ def initialize(latitude: nil, longitude: nil)
9
+ @latitude = latitude&.to_f
10
+ @longitude = longitude&.to_f
11
+ end
12
+
13
+ def to_a
14
+ [latitude, longitude].compact
15
+ end
16
+
17
+ def ==(other)
18
+ other.is_a?(Coordinates) &&
19
+ latitude == other.latitude &&
20
+ longitude == other.longitude
21
+ end
22
+
23
+ def to_s
24
+ return '' if latitude.nil? || longitude.nil?
25
+
26
+ format('%<lat>.4f %<lon>.4f', lat: latitude, lon: longitude)
27
+ end
28
+
29
+ # Great-circle distance in kilometres to another Coordinates, using
30
+ # the haversine formula. Returns nil if either side lacks coordinates.
31
+ def distance_to(other)
32
+ return nil if latitude.nil? || longitude.nil? ||
33
+ other.latitude.nil? || other.longitude.nil?
34
+
35
+ earth_radius_km = 6371.0
36
+ d_lat = (other.latitude - latitude) * (Math::PI / 180)
37
+ d_lon = (other.longitude - longitude) * (Math::PI / 180)
38
+ a = (Math.sin(d_lat / 2)**2) +
39
+ (Math.cos(latitude * (Math::PI / 180)) *
40
+ Math.cos(other.latitude * (Math::PI / 180)) *
41
+ (Math.sin(d_lon / 2)**2))
42
+ 2 * earth_radius_km * Math.asin(Math.sqrt(a))
43
+ end
44
+ end
45
+ end