notam 0.1.3 → 1.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 +4 -4
- checksums.yaml.gz.sig +3 -1
- data/CHANGELOG.md +19 -0
- data/README.md +29 -9
- data/lib/locales/en.yml +24 -0
- data/lib/notam/errors.rb +8 -1
- data/lib/notam/item/a.rb +1 -12
- data/lib/notam/item/d.rb +3 -11
- data/lib/notam/item/header.rb +2 -0
- data/lib/notam/item/q.rb +11 -1
- data/lib/notam/item.rb +7 -3
- data/lib/notam/message.rb +31 -11
- data/lib/notam/schedule.rb +167 -91
- data/lib/notam/translation.rb +50 -0
- data/lib/notam/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +23 -24
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 45b4ce39f423833013299133d3b54d3ee759d701aa6a53d0075e8546bd48dfa2
|
4
|
+
data.tar.gz: f4a86ad18546f654becd05db944a2c9b22213f4095794c20dc911dccdfa93be8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c62bd682d3849093fb96d887f16933159945ecc60edae31da0415cc35f8c2bd4e30655d336a5134eddc0cb9443e7b492aae99f3868dad09c47d3731d7f69bb17
|
7
|
+
data.tar.gz: a52c3ccb305e863d5cd8dc4bd2ff345406ca9a73ae4493cbc4445b8f7f3d63ab98371f219689b236e2c0257aab503b86700b1a770abd2bebc3a1fbd3a92a0e3a
|
checksums.yaml.gz.sig
CHANGED
@@ -1 +1,3 @@
|
|
1
|
-
|
1
|
+
���3Ƈ�3�*�P���3g��X�).5W��}!ҕ� �ږ!�pTmش���v�z���4m���ڤU���]X�HFK���a��i��r�Dj��`������9f��$$�����?z/����]�o��Z5��L/��Tbt�8jπ����������3>ε�1C��9z#�E��ޅ�y�r5a�+��,
|
2
|
+
���љt�u���A$2)�D�Vh��B��^���4,��(�T�I���jHV��
|
3
|
+
�ϛ�5�@�u�
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,25 @@
|
|
2
2
|
|
3
3
|
Nothing so far
|
4
4
|
|
5
|
+
## 1.1.0
|
6
|
+
|
7
|
+
#### Additions
|
8
|
+
* Extract subject group and condition group on Q item
|
9
|
+
|
10
|
+
## 1.0.0
|
11
|
+
|
12
|
+
#### Breaking Changes
|
13
|
+
* `NOTAM::Schedule.parse` now returns an array of `NOTAM_Schedule` instances
|
14
|
+
instead of just a single one.
|
15
|
+
|
16
|
+
#### Changes
|
17
|
+
* Edge case tolerant extraction of `PART n OF n` and `END PART n OF n` markers
|
18
|
+
|
19
|
+
#### Additions
|
20
|
+
* Support for datetime ranges (i.e. `1 APR 2000-20 MAY 2000`) as well as times
|
21
|
+
across midnight (i.e. `1 APR 1900-0500`) on D items.
|
22
|
+
* Wrap all exceptions raised while parsing items.
|
23
|
+
|
5
24
|
## 0.1.3
|
6
25
|
|
7
26
|
#### Fixes
|
data/README.md
CHANGED
@@ -41,12 +41,13 @@ bundle install --trust-policy MediumSecurity
|
|
41
41
|
raw_notam_text_message = <<~END
|
42
42
|
W0902/22 NOTAMN
|
43
43
|
Q) LSAS/QRRCA/V/BO/W/000/148/4624N00702E004
|
44
|
-
A) LSAS B) 2204110900 C) 2205131400 EST
|
44
|
+
A) LSAS PART 2 OF 3 B) 2204110900 C) 2205131400 EST
|
45
45
|
D) APR 11 SR MINUS15-1900, 20-21 26-28 MAY 03-05 10-12 0530-2100, APR
|
46
46
|
14 22 29 MAY 06 13 0530-1400, APR 19 25 MAY 02 09 0800-2100
|
47
47
|
E) R-AREA LS-R7 HONGRIN ACT DUE TO FRNG.
|
48
48
|
F) GND
|
49
49
|
G) 14800FT AMSL
|
50
|
+
END PART 2 OF 3
|
50
51
|
CREATED: 11 Apr 2022 06:10:00
|
51
52
|
SOURCE: LSSNYNYX
|
52
53
|
END
|
@@ -65,7 +66,9 @@ The resulting hash for this example looks as follows:
|
|
65
66
|
id_year: 2022,
|
66
67
|
new?: true,
|
67
68
|
fir: "LSAS",
|
69
|
+
subject_group: :airspace_restrictions,
|
68
70
|
subject: :restricted_area,
|
71
|
+
condition_group: :changes,
|
69
72
|
condition: :activated,
|
70
73
|
traffic: :vfr,
|
71
74
|
purpose: [:operational_significance, :flight_operations],
|
@@ -75,8 +78,8 @@ The resulting hash for this example looks as follows:
|
|
75
78
|
center_point: #<AIXM::XY 46.40000000N 007.03333333E>,
|
76
79
|
radius: #<AIXM::D 4.0 nm>,
|
77
80
|
locations: ["LSAS"],
|
78
|
-
part_index:
|
79
|
-
part_index_max:
|
81
|
+
part_index: 2,
|
82
|
+
part_index_max: 3,
|
80
83
|
effective_at: 2022-04-11 09:00:00 UTC,
|
81
84
|
expiration_at: 2022-05-13 14:00:00 UTC,
|
82
85
|
estimated_expiration?: false,
|
@@ -106,7 +109,12 @@ A few highlights to note here:
|
|
106
109
|
|
107
110
|
Since NOTAM may contain a certain level of redundancy, the parser does some integrity checks, fixes the payload if possible and issues a warning.
|
108
111
|
|
109
|
-
You get a `NOTAM::ParseError` in case the raw NOTAM text message fails to be parsed.
|
112
|
+
You get a `NOTAM::ParseError` in case the raw NOTAM text message fails to be parsed. This error object features two notable methods:
|
113
|
+
|
114
|
+
* `item` – the faulty item (if already available)
|
115
|
+
* `cause` – the underlying error object (if any)
|
116
|
+
|
117
|
+
If you're sure the NOTAM is correct, please [submit an issue](#development) or fix the bug and [submit a pull request](#development).
|
110
118
|
|
111
119
|
See the [API documentation](https://www.rubydoc.info/gems/notam) for more.
|
112
120
|
|
@@ -114,8 +122,9 @@ See the [API documentation](https://www.rubydoc.info/gems/notam) for more.
|
|
114
122
|
|
115
123
|
### Anatomy of a NOTAM message
|
116
124
|
|
117
|
-
A NOTAM message consists of
|
125
|
+
A NOTAM message consists of the following items in order:
|
118
126
|
|
127
|
+
* Header: ID and type of NOTAM
|
119
128
|
* [Q item](https://www.rubydoc.info/gems/notam/NOTAM/Q): Essential information such as purpose or center point and radius
|
120
129
|
* [A item](https://www.rubydoc.info/gems/notam/NOTAM/A): Affected locations
|
121
130
|
* [B item](https://www.rubydoc.info/gems/notam/NOTAM/B): When the NOTAM becomes effective
|
@@ -124,6 +133,9 @@ A NOTAM message consists of a header followed by the following items:
|
|
124
133
|
* [E item](https://www.rubydoc.info/gems/notam/NOTAM/E): Free text description
|
125
134
|
* [F item](https://www.rubydoc.info/gems/notam/NOTAM/F): Upper limit (optional)
|
126
135
|
* [G item](https://www.rubydoc.info/gems/notam/NOTAM/G): Lower limit (optional)
|
136
|
+
* Footer: Any number of lines with metadata such as `CREATED` and `SOURCE`
|
137
|
+
|
138
|
+
Furthermore, oversized NOTAM may be split into several partial messages which contain with `PART n OF n` and `END PART n OF n` markers. This is an unofficial extension and therefore the markers may be found in different places such as on the A item, on the E item or even somewhere in between.
|
127
139
|
|
128
140
|
### FIR
|
129
141
|
|
@@ -190,11 +202,18 @@ For compatibility, schedule dates and times are expressed using the correspondin
|
|
190
202
|
* [Day](https://www.rubydoc.info/gems/aixm/AIXM/Schedule/Day)
|
191
203
|
* [Time](https://www.rubydoc.info/gems/aixm/AIXM/Schedule/Time)
|
192
204
|
|
205
|
+
Raw and parsed NOTAM schedule times differ in how the "end of day" is encoded:
|
206
|
+
|
207
|
+
NOTAM | Beginning of Day | End of Day | Remarks
|
208
|
+
-------|------------------|------------|--------
|
209
|
+
Raw | `"0000"` | `"2359"` | `"2400"` is considered illegal
|
210
|
+
Parsed | `00:00` | `24:00` | the Ruby way
|
211
|
+
|
193
212
|
### References
|
194
213
|
|
195
|
-
* [ICAO
|
214
|
+
* [ICAO Doc 8126: Aeronautical Information Services Manual](https://www.icao.int/NACC/Documents/eDOCS/AIM/8126_unedited_en%20Jul2021.pdf)
|
215
|
+
* [EUROCONTROL Guidelines Operating Procedures AIS Dynamic Data (OPADD)](https://www.eurocontrol.int/sites/default/files/2021-07/eurocontrol-guidelines-opadd-ed4-1.pdf)
|
196
216
|
* [NOTAM Q Codes](https://www.faa.gov/air_traffic/publications/atpubs/notam_html/appendix_b.html)
|
197
|
-
* [Guide de la consultation NOTAM (fr)](https://www.sia.aviation-civile.gouv.fr/pub/media/news/file/g/u/guide_de_la_consultation_notam_05-10-2017-1.pdf)
|
198
217
|
* [NOTAM Contractions](https://www.notams.faa.gov/downloads/contractions.pdf)
|
199
218
|
* [NOTAM format cheat sheet](http://vat-air.dk/files/ICAO%20NOTAM%20format.pdf)
|
200
219
|
* [Introduction on Wikipedia](https://en.wikipedia.org/wiki/NOTAM)
|
@@ -207,12 +226,13 @@ Please [create a translation request issue](https://github.com/svoop/notam/issue
|
|
207
226
|
|
208
227
|
## Tests and Fixtures
|
209
228
|
|
210
|
-
The test suite may run against live NOTAM
|
229
|
+
The test suite may run against live NOTAM depending on whether and how you set the `SPEC_SCOPE` environment variable:
|
211
230
|
|
212
231
|
```
|
232
|
+
export SPEC_SCOPE=none # don't run against any NOTAM fixtures (default)
|
233
|
+
export SPEC_SCOPE=W0214/22 # run against given NOTAM fixture only
|
213
234
|
export SPEC_SCOPE=all # run against all NOTAM fixtures
|
214
235
|
export SPEC_SCOPE=all-fast # run against all NOTAM fixtures but exit on the first failure
|
215
|
-
export SPEC_SCOPE=W0214/22 # run against given NOTAM fixture only
|
216
236
|
```
|
217
237
|
|
218
238
|
The NOTAM fixtures are written to `spec/fixtures`, you can manage them using a Rake tasks:
|
data/lib/locales/en.yml
CHANGED
@@ -307,6 +307,22 @@ en:
|
|
307
307
|
ZSHA: "Shanghai ACC"
|
308
308
|
ZWUQ: "Urumqi ACC"
|
309
309
|
ZYSH: "Shenyang ACC"
|
310
|
+
subject_groups:
|
311
|
+
airspace_organization: "airspace organization"
|
312
|
+
communications_and_surveillance_facilities: "communications and surveillance facilities"
|
313
|
+
facilities_and_services: "facilities and services"
|
314
|
+
gnss_services: "GNSS services"
|
315
|
+
instrument_and_microwave_landing_system: "instrument and microwave landing system"
|
316
|
+
checklist: "checklist"
|
317
|
+
lighting_facilities: "lighting facilities"
|
318
|
+
movement_and_landing_area: "movement and landing area"
|
319
|
+
terminal_and_en_route_navigation_facilities: "terminal and en route navigation facilities"
|
320
|
+
other_information: "other information"
|
321
|
+
air_traffic_procedures: "air traffic procedures"
|
322
|
+
airspace_restrictions: "airspace restrictions"
|
323
|
+
air_traffic_and_volmet_services: "air traffic and VOLMET services"
|
324
|
+
warning: "warning"
|
325
|
+
other: "other"
|
310
326
|
subjects:
|
311
327
|
minimum_altitude: "minimum altitude"
|
312
328
|
class_bcde_surface_area: "class B/C/D/E surface area"
|
@@ -488,6 +504,14 @@ en:
|
|
488
504
|
aerial_survey: "aerial survey"
|
489
505
|
model_flying: "model flying"
|
490
506
|
other: "other"
|
507
|
+
condition_groups:
|
508
|
+
availability: "availabiity"
|
509
|
+
changes: "changes"
|
510
|
+
hazard_conditions: "hazard conditions"
|
511
|
+
checklist: "checklist"
|
512
|
+
limitations: "limitations"
|
513
|
+
trigger: "trigger"
|
514
|
+
other: "other"
|
491
515
|
conditions:
|
492
516
|
withdrawn_for_maintenance: "withdrawn for maintenance"
|
493
517
|
available_for_daylight_operation: "available for daylight operation"
|
data/lib/notam/errors.rb
CHANGED
data/lib/notam/item/a.rb
CHANGED
@@ -9,7 +9,6 @@ module NOTAM
|
|
9
9
|
\A
|
10
10
|
A\)\s?
|
11
11
|
(?<locations>(?:#{ICAO_RE}\s?)+)
|
12
|
-
(?<parts>(?<part_index>\d+)\s+OF\s+(?<part_index_max>\d+))?
|
13
12
|
\z
|
14
13
|
)x.freeze
|
15
14
|
|
@@ -18,19 +17,9 @@ module NOTAM
|
|
18
17
|
captures['locations'].split(/\s/)
|
19
18
|
end
|
20
19
|
|
21
|
-
# @return [Integer, nil]
|
22
|
-
def part_index
|
23
|
-
captures['parts'] ? captures['part_index'].to_i : 1
|
24
|
-
end
|
25
|
-
|
26
|
-
# @return [Integer, nil]
|
27
|
-
def part_index_max
|
28
|
-
captures['parts'] ? captures['part_index_max'].to_i : 1
|
29
|
-
end
|
30
|
-
|
31
20
|
# @see NOTAM::Item#merge
|
32
21
|
def merge
|
33
|
-
super(:locations
|
22
|
+
super(:locations)
|
34
23
|
end
|
35
24
|
|
36
25
|
end
|
data/lib/notam/item/d.rb
CHANGED
@@ -14,10 +14,12 @@ module NOTAM
|
|
14
14
|
# @see NOTAM::Item#parse
|
15
15
|
def parse
|
16
16
|
base_date = AIXM.date(data[:effective_at])
|
17
|
-
@schedules =
|
17
|
+
@schedules = text.sub(/\AD\)/, '').split(',').flat_map do |string|
|
18
18
|
Schedule.parse(string, base_date: base_date)
|
19
19
|
end
|
20
20
|
self
|
21
|
+
rescue
|
22
|
+
fail! 'invalid D item'
|
21
23
|
end
|
22
24
|
|
23
25
|
# Whether the D item is active at the given time.
|
@@ -54,15 +56,5 @@ module NOTAM
|
|
54
56
|
@five_day_base ||= [data[:effective_at], Time.now.utc.round].max
|
55
57
|
end
|
56
58
|
|
57
|
-
# @params string [String] string to clean up
|
58
|
-
# @return [String]
|
59
|
-
def cleanup(string)
|
60
|
-
string
|
61
|
-
.gsub(/\s+/, ' ') # collapse whitespaces to single space
|
62
|
-
.gsub(/ ?([-,]) ?/, '\1') # remove spaces around dashes and commas
|
63
|
-
.sub(/\AD\) /, '') # remove item identifier
|
64
|
-
.strip
|
65
|
-
end
|
66
|
-
|
67
59
|
end
|
68
60
|
end
|
data/lib/notam/item/header.rb
CHANGED
data/lib/notam/item/q.rb
CHANGED
@@ -26,11 +26,21 @@ module NOTAM
|
|
26
26
|
captures['fir']
|
27
27
|
end
|
28
28
|
|
29
|
+
# @return [Symbol]
|
30
|
+
def subject_group
|
31
|
+
NOTAM.subject_group_for(captures['subject'][0,1])
|
32
|
+
end
|
33
|
+
|
29
34
|
# @return [Symbol]
|
30
35
|
def subject
|
31
36
|
NOTAM.subject_for(captures['subject'])
|
32
37
|
end
|
33
38
|
|
39
|
+
# @return [Symbol]
|
40
|
+
def condition_group
|
41
|
+
NOTAM.condition_group_for(captures['condition'][0,1])
|
42
|
+
end
|
43
|
+
|
34
44
|
# @return [Symbol]
|
35
45
|
def condition
|
36
46
|
NOTAM.condition_for(captures['condition'])
|
@@ -82,7 +92,7 @@ module NOTAM
|
|
82
92
|
|
83
93
|
# @see NOTAM::Item#merge
|
84
94
|
def merge
|
85
|
-
super(:fir, :subject, :condition, :traffic, :purpose, :scope, :lower_limit, :upper_limit, :center_point, :radius)
|
95
|
+
super(:fir, :subject_group, :subject, :condition_group, :condition, :traffic, :purpose, :scope, :lower_limit, :upper_limit, :center_point, :radius)
|
86
96
|
end
|
87
97
|
|
88
98
|
end
|
data/lib/notam/item.rb
CHANGED
@@ -91,8 +91,12 @@ module NOTAM
|
|
91
91
|
# @return [self]
|
92
92
|
def parse
|
93
93
|
if match_data = self.class::RE.match(text)
|
94
|
-
|
95
|
-
|
94
|
+
begin
|
95
|
+
@captures = match_data.named_captures
|
96
|
+
self
|
97
|
+
rescue
|
98
|
+
fail! "invalid #{self.class.to_s.split('::').last} item"
|
99
|
+
end
|
96
100
|
else
|
97
101
|
fail! 'text does not match regexp'
|
98
102
|
end
|
@@ -124,7 +128,7 @@ module NOTAM
|
|
124
128
|
# @param message [String] optional error message
|
125
129
|
# @raise [NOTAM::ParseError]
|
126
130
|
def fail!(message=nil)
|
127
|
-
fail(
|
131
|
+
fail ParseError.new([message, text].compact.join(': '), item: self)
|
128
132
|
end
|
129
133
|
|
130
134
|
# @return [String]
|
data/lib/notam/message.rb
CHANGED
@@ -4,17 +4,22 @@ module NOTAM
|
|
4
4
|
|
5
5
|
# NOTAM messages are plain text and consist of several ordered items:
|
6
6
|
#
|
7
|
-
# WDDDD/DD ... <- Header
|
8
|
-
# Q) ... <- Q
|
9
|
-
# A) ... <- A
|
10
|
-
# B) ... <- B
|
11
|
-
# C) ... <- C
|
12
|
-
# D) ... <- D
|
13
|
-
# E) ... <- E
|
14
|
-
# F) ... <- F
|
15
|
-
# G) ... <- G
|
7
|
+
# WDDDD/DD ... <- Header (mandatory)
|
8
|
+
# Q) ... <- Q item: context (mandatory)
|
9
|
+
# A) ... <- A item: locations (mandatory)
|
10
|
+
# B) ... <- B item: effective from (mandatory)
|
11
|
+
# C) ... <- C item: effective until (optional)
|
12
|
+
# D) ... <- D item: timesheets (optional, may contain newlines)
|
13
|
+
# E) ... <- E item: description (mandatory, may contain newlines)
|
14
|
+
# F) ... <- F item: upper limit (optional)
|
15
|
+
# G) ... <- G item: lower limit (optional)
|
16
16
|
# CREATED: ... <- Footer (optional)
|
17
17
|
# SOURCE: ... <- Footer (optional)
|
18
|
+
#
|
19
|
+
# Furthermore, oversized NOTAM may be split into several partial messages
|
20
|
+
# which contain with +PART n OF n+ and +END PART n OF n+ markers. This is an
|
21
|
+
# unofficial extension and therefore the markers may be found in different
|
22
|
+
# places such as on the A item, on the E item or even somewhere in between.
|
18
23
|
class Message
|
19
24
|
|
20
25
|
UNSUPPORTED_FORMATS = %r(
|
@@ -24,6 +29,10 @@ module NOTAM
|
|
24
29
|
\w{3}\s[A-Z]\d{4}/\d{2}\sMILITARY # USA: military
|
25
30
|
)xi.freeze
|
26
31
|
|
32
|
+
PART_RE = %r(
|
33
|
+
(?:END\s+)?PART\s+(?<part_index>\d+)\s+OF\s+(?<part_index_max>\d+)
|
34
|
+
)xim.freeze
|
35
|
+
|
27
36
|
FINGERPRINTS = %w[Q) A) B) C) D) E) F) G) CREATED: SOURCE:].freeze
|
28
37
|
|
29
38
|
# Raw NOTAM text message
|
@@ -44,10 +53,9 @@ module NOTAM
|
|
44
53
|
def initialize(text)
|
45
54
|
fail(NOTAM::ParserError, "unsupported format") unless self.class.supported_format? text
|
46
55
|
@text, @items, @data = text, [], {}
|
47
|
-
itemize(text).each do |raw_item|
|
56
|
+
itemize(departition(@text)).each do |raw_item|
|
48
57
|
item = NOTAM::Item.new(raw_item, data: @data).parse.merge
|
49
58
|
@items << item
|
50
|
-
@data = item.data
|
51
59
|
end
|
52
60
|
end
|
53
61
|
|
@@ -96,6 +104,18 @@ module NOTAM
|
|
96
104
|
|
97
105
|
private
|
98
106
|
|
107
|
+
# @return [String]
|
108
|
+
def departition(text)
|
109
|
+
text.gsub(PART_RE, '').tap do
|
110
|
+
if $~ # part marker found
|
111
|
+
@data.merge!(
|
112
|
+
part_index: $~[:part_index].to_i,
|
113
|
+
part_index_max: $~[:part_index_max].to_i
|
114
|
+
)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
99
119
|
# @return [Array]
|
100
120
|
def itemize(text)
|
101
121
|
lines = text.gsub(/\s(#{NOTAM::Item::RE})/, "\n\\1").split("\n")
|
data/lib/notam/schedule.rb
CHANGED
@@ -7,21 +7,24 @@ module NOTAM
|
|
7
7
|
# Structure to accommodate individual schedules used on D items
|
8
8
|
class Schedule
|
9
9
|
EVENTS = { 'SR' => :sunrise, 'SS' => :sunset }.freeze
|
10
|
+
EVENT_HOURS = { sunrise: AIXM.time('06:00'), sunset: AIXM.time('18:00') }.freeze
|
10
11
|
OPERATIONS = { 'PLUS' => 1, 'MINUS' => -1 }.freeze
|
11
12
|
MONTHS = { 'JAN' => 1, 'FEB' => 2, 'MAR' => 3, 'APR' => 4, 'MAY' => 5, 'JUN' => 6, 'JUL' => 7, 'AUG' => 8, 'SEP' => 9, 'OCT' => 10, 'NOV' => 11, 'DEC' => 12 }.freeze
|
12
13
|
DAYS = { 'MON' => :monday, 'TUE' => :tuesday, 'WED' => :wednesday, 'THU' => :thursday, 'FRI' => :friday, 'SAT' => :saturday, 'SUN' => :sunday, 'DAILY' => :any, 'DLY' => :any }.freeze
|
13
14
|
|
15
|
+
DATE_RE = /[0-2]\d|3[01]/.freeze
|
16
|
+
DAY_RE = /#{DAYS.keys.join('|')}/.freeze
|
17
|
+
MONTH_RE = /#{MONTHS.keys.join('|')}/.freeze
|
14
18
|
H24_RE = /(?<h24>H24)/.freeze
|
15
19
|
HOUR_RE = /(?<hour>[01]\d|2[0-4])(?<minute>[0-5]\d)/.freeze
|
16
20
|
OPERATIONS_RE = /#{OPERATIONS.keys.join('|')}/.freeze
|
17
21
|
EVENT_RE = /(?<event>SR|SS)(?:\s(?<operation>#{OPERATIONS_RE})(?<delta>\d+))?/.freeze
|
18
22
|
TIME_RE = /#{HOUR_RE}|#{EVENT_RE}/.freeze
|
19
23
|
TIME_RANGE_RE = /#{TIME_RE}-#{TIME_RE}|#{H24_RE}/.freeze
|
20
|
-
|
21
|
-
|
22
|
-
MONTH_RE = /#{MONTHS.keys.join('|')}/.freeze
|
24
|
+
DATETIME_RE = /(?:(?<month>#{MONTH_RE}) )?(?<date>#{DATE_RE}) (?<time>#{TIME_RE})/.freeze
|
25
|
+
DATETIME_RANGE_RE = /#{DATETIME_RE}-#{DATETIME_RE}/.freeze
|
23
26
|
|
24
|
-
H24 = (AIXM
|
27
|
+
H24 = (AIXM::BEGINNING_OF_DAY..AIXM::END_OF_DAY).freeze
|
25
28
|
|
26
29
|
# Active dates or days
|
27
30
|
#
|
@@ -46,8 +49,7 @@ module NOTAM
|
|
46
49
|
|
47
50
|
# @!visibility private
|
48
51
|
def initialize(actives, times, inactives, base_date:)
|
49
|
-
@actives, @times, @inactives = actives, times, inactives
|
50
|
-
@base_date ||= base_date.at(day: 1)
|
52
|
+
@actives, @times, @inactives, @base_date = actives, times, inactives, base_date
|
51
53
|
end
|
52
54
|
|
53
55
|
class << self
|
@@ -58,23 +60,156 @@ module NOTAM
|
|
58
60
|
# @param string [String] raw schedule string
|
59
61
|
# @param base_date [Date] month and year to assume when missing (day is
|
60
62
|
# force set to 1)
|
63
|
+
# @return [Array<NOTAM::Schedule>] array of at least one schedule object
|
61
64
|
def parse(string, base_date:)
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
65
|
+
@rules, @exceptions = cleanup(string).split(/ EXC /).map(&:strip)
|
66
|
+
@base_date = base_date.at(day: 1)
|
67
|
+
case @rules
|
68
|
+
when /^#{DATETIME_RANGE_RE}$/
|
69
|
+
parse_datetimes
|
70
|
+
when /^(#{DAY_RE}|#{TIME_RANGE_RE})/
|
71
|
+
parse_days
|
72
|
+
when /^(#{DATE_RE}|#{MONTH_RE})/
|
73
|
+
parse_dates
|
74
|
+
else
|
75
|
+
fail! "unrecognized schedule"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def parse_datetimes
|
82
|
+
from, to = @rules.split(/-/).map { datetime_from(_1) }
|
83
|
+
delta = to.date - from.date
|
84
|
+
fail! "invalid datetime" if delta < 1
|
85
|
+
inactives = days_from(@exceptions)
|
86
|
+
[
|
87
|
+
new(Dates[from.date], Times[(from.time..AIXM::END_OF_DAY)], inactives, base_date: @base_date),
|
88
|
+
(new(Dates[(from.date.next..to.date.prev)], Times[H24], inactives, base_date: @base_date) if delta > 1),
|
89
|
+
new(Dates[to.date], Times[(AIXM::BEGINNING_OF_DAY..to.time)], inactives, base_date: @base_date)
|
90
|
+
].compact
|
91
|
+
end
|
92
|
+
|
93
|
+
%i(dates days).each do |active_unit|
|
94
|
+
inactive_unit = active_unit == :days ? :dates : :days
|
95
|
+
define_method("parse_#{active_unit}") do
|
96
|
+
raw_active_unit, raw_times, unmatched = @rules.split(/((?: ?#{TIME_RANGE_RE.decapture})+)/, 3)
|
97
|
+
fail! "unrecognized part after times" unless unmatched.empty?
|
98
|
+
actives = send("#{active_unit}_from", raw_active_unit.strip)
|
99
|
+
times = times_from(raw_times.strip)
|
100
|
+
inactives = send("#{inactive_unit}_from", @exceptions)
|
101
|
+
if times.any? &method(:across_midnight?)
|
102
|
+
times.each_with_object([]) do |time, array|
|
103
|
+
if across_midnight? time
|
104
|
+
array << new(actives, [(time.first..AIXM::END_OF_DAY)], inactives, base_date: @base_date)
|
105
|
+
array << new(actives.next, [(AIXM::BEGINNING_OF_DAY..time.last)], inactives, base_date: @base_date)
|
106
|
+
else
|
107
|
+
array << new(actives, [time], inactives, base_date: @base_date)
|
108
|
+
end
|
109
|
+
end
|
70
110
|
else
|
71
|
-
actives
|
72
|
-
inactives = raw_inactives ? days_from(raw_inactives) : Days.new
|
111
|
+
[new(actives, times, inactives, base_date: @base_date)]
|
73
112
|
end
|
74
|
-
initialize(actives, times, inactives, base_date: base_date)
|
75
|
-
self
|
76
113
|
end
|
77
114
|
end
|
115
|
+
|
116
|
+
# @return [String]
|
117
|
+
def cleanup(string)
|
118
|
+
string
|
119
|
+
.gsub(/\s+/, ' ') # collapse whitespaces to single space
|
120
|
+
.gsub(/ *- */, '-') # remove spaces around dashes
|
121
|
+
.strip
|
122
|
+
end
|
123
|
+
|
124
|
+
# @return [AIXM::Schedule::DateTime]
|
125
|
+
def datetime_from(string)
|
126
|
+
parts = string.match(DATETIME_RE).named_captures
|
127
|
+
parts['year'] = @base_date.year
|
128
|
+
parts['month'] = MONTHS[parts['month']] || @base_date.month
|
129
|
+
AIXM.datetime(
|
130
|
+
AIXM.date('%4d-%02d-%02d' % parts.slice('year', 'month', 'date').values.map(&:to_i)),
|
131
|
+
AIXM.time(parts['time'])
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
# @return [Array<Date, Range<Date>>]
|
136
|
+
def dates_from(string)
|
137
|
+
return Dates.new if string.nil?
|
138
|
+
array, index, base_date = [], 0, @base_date.dup
|
139
|
+
while index < string.length
|
140
|
+
case string[index..]
|
141
|
+
when /\A((?<from>#{DATE_RE})-(?:(?<month>#{MONTH_RE}) )?(?<to>#{DATE_RE}))/ # range of dates
|
142
|
+
month = $~[:month] ? MONTHS.fetch($~[:month]) : base_date.month
|
143
|
+
base_date = base_date.at(month: month, wrap: true).tap do |to_base_date|
|
144
|
+
array << (base_date.at(day: $~[:from].to_i)..to_base_date.at(day: $~[:to].to_i))
|
145
|
+
end
|
146
|
+
when /\A(?<day>#{DATE_RE})/ # single date
|
147
|
+
array << base_date.at(day: $~[:day].to_i)
|
148
|
+
when /\A(?<month>#{MONTH_RE})/
|
149
|
+
base_date = base_date.at(month: MONTHS.fetch($~[:month]), wrap: true)
|
150
|
+
else
|
151
|
+
fail! "unrecognized date"
|
152
|
+
end
|
153
|
+
index += $&.length + 1
|
154
|
+
end
|
155
|
+
Dates.new(array)
|
156
|
+
end
|
157
|
+
|
158
|
+
# @return [Array<AIXM::Schedule::Day, Range<AIXM::Schedule::Day>>]
|
159
|
+
def days_from(string)
|
160
|
+
return Days.new if string.nil?
|
161
|
+
array = if string.empty? # no declared day implies any day
|
162
|
+
[AIXM::ANY_DAY]
|
163
|
+
else
|
164
|
+
string.split(' ').map do |token|
|
165
|
+
from, to = token.split('-')
|
166
|
+
if to # range of days
|
167
|
+
(AIXM.day(DAYS.fetch(from))..AIXM.day(DAYS.fetch(to)))
|
168
|
+
else # single day
|
169
|
+
AIXM.day(DAYS.fetch(from))
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
Days.new(array)
|
174
|
+
end
|
175
|
+
|
176
|
+
# @return [Array<Range<AIXM::Schedule::Time>>]
|
177
|
+
def times_from(string)
|
178
|
+
array = string.split(/ (?!#{OPERATIONS_RE})/).map { time_range_from(_1) }
|
179
|
+
Times.new(array)
|
180
|
+
end
|
181
|
+
|
182
|
+
# @return [Range<AIXM::Schedule::Time>]
|
183
|
+
def time_range_from(string)
|
184
|
+
case string
|
185
|
+
when H24_RE
|
186
|
+
H24
|
187
|
+
else
|
188
|
+
from, to = string.split('-')
|
189
|
+
(time_from(from)..time_from(to))
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# @return [AIXM::Schedule::Time]
|
194
|
+
def time_from(string)
|
195
|
+
case string
|
196
|
+
when HOUR_RE
|
197
|
+
hour, minute = $~[:hour], $~[:minute]
|
198
|
+
AIXM.time([hour, minute].join(':'))
|
199
|
+
when EVENT_RE
|
200
|
+
event, operation, delta = $~[:event], $~[:operation], $~[:delta]&.to_i
|
201
|
+
AIXM.time(EVENTS.fetch(event), plus: delta ? OPERATIONS.fetch(operation) * delta : 0)
|
202
|
+
else
|
203
|
+
fail! "unrecognized time"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# @return [Boolean]
|
208
|
+
def across_midnight?(time_range)
|
209
|
+
from = time_range.first.time || EVENT_HOURS.fetch(time_range.first.event).time
|
210
|
+
to = time_range.last.time || EVENT_HOURS.fetch(time_range.last.event).time
|
211
|
+
from > to
|
212
|
+
end
|
78
213
|
end
|
79
214
|
|
80
215
|
# @return [String]
|
@@ -139,78 +274,6 @@ module NOTAM
|
|
139
274
|
resolve(on: date, xy: xy).slice(date).times.cover? AIXM.time(at)
|
140
275
|
end
|
141
276
|
|
142
|
-
private
|
143
|
-
|
144
|
-
# @return [Array<Date, Range<Date>>]
|
145
|
-
def dates_from(string)
|
146
|
-
array, index, base_date = [], 0, @base_date.dup
|
147
|
-
while index < string.length
|
148
|
-
case string[index..]
|
149
|
-
when /\A((?<from>#{DATE_RE})-(?:(?<month>#{MONTH_RE}) )?(?<to>#{DATE_RE}))/ # range of dates
|
150
|
-
month = $~[:month] ? MONTHS.fetch($~[:month]) : base_date.month
|
151
|
-
base_date = base_date.at(month: month, wrap: true).tap do |to_base_date|
|
152
|
-
array << (base_date.at(day: $~[:from].to_i)..to_base_date.at(day: $~[:to].to_i))
|
153
|
-
end
|
154
|
-
when /\A(?<day>#{DATE_RE})/ # single date
|
155
|
-
array << base_date.at(day: $~[:day].to_i)
|
156
|
-
when /\A(?<month>#{MONTH_RE})/
|
157
|
-
base_date = base_date.at(month: MONTHS.fetch($~[:month]), wrap: true)
|
158
|
-
else
|
159
|
-
fail! "unrecognized date"
|
160
|
-
end
|
161
|
-
index += $&.length + 1
|
162
|
-
end
|
163
|
-
Dates.new(array)
|
164
|
-
end
|
165
|
-
|
166
|
-
# @return [Array<AIXM::Schedule::Day, Range<AIXM::Schedue::Day>>]
|
167
|
-
def days_from(string)
|
168
|
-
array = if string.empty? # no declared day implies any day
|
169
|
-
[AIXM.day(:any)]
|
170
|
-
else
|
171
|
-
string.split(' ').map do |token|
|
172
|
-
from, to = token.split('-')
|
173
|
-
if to # range of days
|
174
|
-
(AIXM.day(DAYS.fetch(from))..AIXM.day(DAYS.fetch(to)))
|
175
|
-
else # single day
|
176
|
-
AIXM.day(DAYS.fetch(from))
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|
180
|
-
Days.new(array)
|
181
|
-
end
|
182
|
-
|
183
|
-
# @return [Array<Range<AIXM::Schedule::Time>>]
|
184
|
-
def times_from(string)
|
185
|
-
array = string.split(/ (?!#{OPERATIONS_RE})/).map { time_range_from(_1) }
|
186
|
-
Times.new(array)
|
187
|
-
end
|
188
|
-
|
189
|
-
# @return [Range<AIXM::Schedule::Time>]
|
190
|
-
def time_range_from(string)
|
191
|
-
case string
|
192
|
-
when H24_RE
|
193
|
-
H24
|
194
|
-
else
|
195
|
-
from, to = string.split('-')
|
196
|
-
(time_from(from)..time_from(to))
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
# @return [AIXM::Schedule::Time]
|
201
|
-
def time_from(string)
|
202
|
-
case string
|
203
|
-
when HOUR_RE
|
204
|
-
hour, minute = $~[:hour], $~[:minute]
|
205
|
-
AIXM.time([hour, minute].join(':'))
|
206
|
-
when EVENT_RE
|
207
|
-
event, operation, delta = $~[:event], $~[:operation], $~[:delta]&.to_i
|
208
|
-
AIXM.time(EVENTS.fetch(event), plus: delta ? OPERATIONS.fetch(operation) * delta : 0)
|
209
|
-
else
|
210
|
-
fail! "unrecognized time"
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
277
|
# @abstract
|
215
278
|
class ScheduleArray < Array
|
216
279
|
# @return [String]
|
@@ -231,6 +294,19 @@ module NOTAM
|
|
231
294
|
def cover?(object)
|
232
295
|
any? { object.covered_by? _1 }
|
233
296
|
end
|
297
|
+
|
298
|
+
# Step through all elements and shift all dates or days to the next day
|
299
|
+
#
|
300
|
+
# @return [ScheduleArray]
|
301
|
+
def next
|
302
|
+
entries.map do |entry|
|
303
|
+
if entry.instance_of? Range
|
304
|
+
(entry.first.next..entry.last.next)
|
305
|
+
else
|
306
|
+
entry.next
|
307
|
+
end
|
308
|
+
end.then { self.class.new(_1) }
|
309
|
+
end
|
234
310
|
end
|
235
311
|
|
236
312
|
class Dates < ScheduleArray
|
@@ -240,7 +316,7 @@ module NOTAM
|
|
240
316
|
def cluster
|
241
317
|
self.class.new(
|
242
318
|
entries
|
243
|
-
.slice_when { _1.
|
319
|
+
.slice_when { _1.next != _2 }
|
244
320
|
.map { _1.count > 1 ? (_1.first.._1.last) : _1.first }
|
245
321
|
)
|
246
322
|
end
|
data/lib/notam/translation.rb
CHANGED
@@ -26,6 +26,14 @@ module NOTAM
|
|
26
26
|
FIRS.fetch(fir)
|
27
27
|
end
|
28
28
|
|
29
|
+
# Translates the NOTAM subject group code to human/machine readable symbol
|
30
|
+
#
|
31
|
+
# @param code [String] one letter subject group code
|
32
|
+
# @return [Symbol] value from {NOTAM::SUBJECT_GROUPS}
|
33
|
+
def subject_group_for(code)
|
34
|
+
SUBJECT_GROUPS.fetch(code)
|
35
|
+
end
|
36
|
+
|
29
37
|
# Translates the NOTAM subject code to human/machine readable symbol
|
30
38
|
#
|
31
39
|
# @param code [String] two letter subject code
|
@@ -34,6 +42,14 @@ module NOTAM
|
|
34
42
|
SUBJECTS.fetch(code)
|
35
43
|
end
|
36
44
|
|
45
|
+
# Translates the NOTAM condition group code to human/machine readable symbol
|
46
|
+
#
|
47
|
+
# @param code [String] one letter condition group code
|
48
|
+
# @return [Symbol] value from {NOTAM::CONDITION_GROUPS}
|
49
|
+
def condition_group_for(code)
|
50
|
+
CONDITION_GROUPS.fetch(code)
|
51
|
+
end
|
52
|
+
|
37
53
|
# Translates the NOTAM condition code to human/machine readable symbol
|
38
54
|
#
|
39
55
|
# @param code [String] two letter condition code
|
@@ -393,6 +409,27 @@ module NOTAM
|
|
393
409
|
'ZYSH' => [:CH]
|
394
410
|
}.freeze
|
395
411
|
|
412
|
+
# International NOTAM Q codes for subject groups
|
413
|
+
#
|
414
|
+
# @see https://www.faa.gov/air_traffic/publications/atpubs/notam_html/appendix_b.html
|
415
|
+
SUBJECT_GROUPS = {
|
416
|
+
'A' => :airspace_organization,
|
417
|
+
'C' => :communications_and_surveillance_facilities,
|
418
|
+
'F' => :facilities_and_services,
|
419
|
+
'G' => :gnss_services,
|
420
|
+
'I' => :instrument_and_microwave_landing_system,
|
421
|
+
'K' => :checklist,
|
422
|
+
'L' => :lighting_facilities,
|
423
|
+
'M' => :movement_and_landing_area,
|
424
|
+
'N' => :terminal_and_en_route_navigation_facilities,
|
425
|
+
'O' => :other_information,
|
426
|
+
'P' => :air_traffic_procedures,
|
427
|
+
'R' => :airspace_restrictions,
|
428
|
+
'S' => :air_traffic_and_volmet_services,
|
429
|
+
'W' => :warning,
|
430
|
+
'X' => :other
|
431
|
+
}.freeze
|
432
|
+
|
396
433
|
# International NOTAM Q codes for subjects
|
397
434
|
#
|
398
435
|
# @see https://www.faa.gov/air_traffic/publications/atpubs/notam_html/appendix_b.html
|
@@ -579,6 +616,19 @@ module NOTAM
|
|
579
616
|
'XX' => :other
|
580
617
|
}.freeze
|
581
618
|
|
619
|
+
# International NOTAM Q codes for condition groups
|
620
|
+
#
|
621
|
+
# @see https://www.faa.gov/air_traffic/publications/atpubs/notam_html/appendix_b.html
|
622
|
+
CONDITION_GROUPS = {
|
623
|
+
'A' => :availability,
|
624
|
+
'C' => :changes,
|
625
|
+
'H' => :hazard_conditions,
|
626
|
+
'K' => :checklist,
|
627
|
+
'L' => :limitations,
|
628
|
+
'T' => :trigger,
|
629
|
+
'X' => :other
|
630
|
+
}.freeze
|
631
|
+
|
582
632
|
# International NOTAM Q codes for conditions
|
583
633
|
#
|
584
634
|
# @see https://www.faa.gov/air_traffic/publications/atpubs/notam_html/appendix_b.html
|
data/lib/notam/version.rb
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: notam
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sven Schwyn
|
@@ -10,27 +10,26 @@ bindir: bin
|
|
10
10
|
cert_chain:
|
11
11
|
- |
|
12
12
|
-----BEGIN CERTIFICATE-----
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
5JcY2h7owdMxXIvgk1oakgldFJc=
|
13
|
+
MIIDODCCAiCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDDBhydWJ5
|
14
|
+
L0RDPWJpdGNldGVyYS9EQz1jb20wHhcNMjIxMTA2MTIzNjUwWhcNMjMxMTA2MTIz
|
15
|
+
NjUwWjAjMSEwHwYDVQQDDBhydWJ5L0RDPWJpdGNldGVyYS9EQz1jb20wggEiMA0G
|
16
|
+
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDcLg+IHjXYaUlTSU7R235lQKD8ZhEe
|
17
|
+
KMhoGlSUonZ/zo1OT3KXcqTCP1iMX743xYs6upEGALCWWwq+nxvlDdnWRjF3AAv7
|
18
|
+
ikC+Z2BEowjyeCCT/0gvn4ohKcR0JOzzRaIlFUVInlGSAHx2QHZ2N8ntf54lu7nd
|
19
|
+
L8CiDK8rClsY4JBNGOgH9UC81f+m61UUQuTLxyM2CXfAYkj/sGNTvFRJcNX+nfdC
|
20
|
+
hM9r2kH1+7wsa8yG7wJ2IkrzNACD8v84oE6qVusN8OLEMUI/NaEPVPbw2LUM149H
|
21
|
+
PVa0i729A4IhroNnFNmw4wOC93ARNbM1+LW36PLMmKjKudf5Exg8VmDVAgMBAAGj
|
22
|
+
dzB1MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBSfK8MtR62mQ6oN
|
23
|
+
yoX/VKJzFjLSVDAdBgNVHREEFjAUgRJydWJ5QGJpdGNldGVyYS5jb20wHQYDVR0S
|
24
|
+
BBYwFIEScnVieUBiaXRjZXRlcmEuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAYG2na
|
25
|
+
ye8OE2DANQIFM/xDos/E4DaPWCJjX5xvFKNKHMCeQYPeZvLICCwyw2paE7Otwk6p
|
26
|
+
uvbg2Ks5ykXsbk5i6vxDoeeOLvmxCqI6m+tHb8v7VZtmwRJm8so0eSX0WvTaKnIf
|
27
|
+
CAn1bVUggczVdNoBXw9WAILKyw9bvh3Ft740XZrR74sd+m2pGwjCaM8hzLvrVbGP
|
28
|
+
DyYhlBeRWyQKQ0WDIsiTSRhzK8HwSTUWjvPwx7SEdIU/HZgyrk0ETObKPakVu6bH
|
29
|
+
kAyiRqgxF4dJviwtqI7mZIomWL63+kXLgjOjMe1SHxfIPo/0ji6+r1p4KYa7o41v
|
30
|
+
fwIwU1MKlFBdsjkd
|
32
31
|
-----END CERTIFICATE-----
|
33
|
-
date: 2022-
|
32
|
+
date: 2022-11-19 00:00:00.000000000 Z
|
34
33
|
dependencies:
|
35
34
|
- !ruby/object:Gem::Dependency
|
36
35
|
name: aixm
|
@@ -41,7 +40,7 @@ dependencies:
|
|
41
40
|
version: '1'
|
42
41
|
- - ">="
|
43
42
|
- !ruby/object:Gem::Version
|
44
|
-
version: 1.3.
|
43
|
+
version: 1.3.2
|
45
44
|
type: :runtime
|
46
45
|
prerelease: false
|
47
46
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -51,7 +50,7 @@ dependencies:
|
|
51
50
|
version: '1'
|
52
51
|
- - ">="
|
53
52
|
- !ruby/object:Gem::Version
|
54
|
-
version: 1.3.
|
53
|
+
version: 1.3.2
|
55
54
|
- !ruby/object:Gem::Dependency
|
56
55
|
name: i18n
|
57
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -258,7 +257,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
258
257
|
- !ruby/object:Gem::Version
|
259
258
|
version: '0'
|
260
259
|
requirements: []
|
261
|
-
rubygems_version: 3.3.
|
260
|
+
rubygems_version: 3.3.26
|
262
261
|
signing_key:
|
263
262
|
specification_version: 4
|
264
263
|
summary: Parser for NOTAM (Notice to Airmen) messages
|
metadata.gz.sig
CHANGED
Binary file
|