notam 0.1.2 → 1.0.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 +0 -0
- data/CHANGELOG.md +19 -0
- data/README.md +27 -9
- 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/f.rb +3 -3
- data/lib/notam/item/g.rb +3 -3
- data/lib/notam/item/header.rb +2 -0
- data/lib/notam/item.rb +7 -3
- data/lib/notam/message.rb +31 -11
- data/lib/notam/schedule.rb +167 -91
- 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: d3f0a2b936b2f99302c03166135f539d6d9aa8c90ded6da55052e72c27937dcf
|
4
|
+
data.tar.gz: de7f1ff14e4c8c5de825f88a3d83cb22b7726a41b6ed5fffa7bf99c19ac856b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53d1288241008d166fde8a71b1e13ae0cc088f62f552503f70282e2de4cca086e22b6cc80437d3dbfd911ec8056e55481a6aaa8f7221c963ab3dc99ff1799eba
|
7
|
+
data.tar.gz: 6c94aa7b09ec15c27d9af9ba4232cf8e5b39086a4444d14bda82c4f5ddd8414ee89c6d9cb53694663b36aa1a0861cb87de2bcbc16b2fe965af3bafbdf36bcc7a
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,25 @@
|
|
2
2
|
|
3
3
|
Nothing so far
|
4
4
|
|
5
|
+
## 1.0.0
|
6
|
+
|
7
|
+
#### Breaking Changes
|
8
|
+
* `NOTAM::Schedule.parse` now returns an array of `NOTAM_Schedule` instances
|
9
|
+
instead of just a single one.
|
10
|
+
|
11
|
+
#### Changes
|
12
|
+
* Edge case tolerant extraction of `PART n OF n` and `END PART n OF n` markers
|
13
|
+
|
14
|
+
#### Additions
|
15
|
+
* Support for datetime ranges (i.e. `1 APR 2000-20 MAY 2000`) as well as times
|
16
|
+
across midnight (i.e. `1 APR 1900-0500`) on D items.
|
17
|
+
* Wrap all exceptions raised while parsing items.
|
18
|
+
|
19
|
+
## 0.1.3
|
20
|
+
|
21
|
+
#### Fixes
|
22
|
+
* Reverse accidentally flipped F and G item.
|
23
|
+
|
5
24
|
## 0.1.2
|
6
25
|
|
7
26
|
#### Changes
|
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
|
@@ -75,8 +76,8 @@ The resulting hash for this example looks as follows:
|
|
75
76
|
center_point: #<AIXM::XY 46.40000000N 007.03333333E>,
|
76
77
|
radius: #<AIXM::D 4.0 nm>,
|
77
78
|
locations: ["LSAS"],
|
78
|
-
part_index:
|
79
|
-
part_index_max:
|
79
|
+
part_index: 2,
|
80
|
+
part_index_max: 3,
|
80
81
|
effective_at: 2022-04-11 09:00:00 UTC,
|
81
82
|
expiration_at: 2022-05-13 14:00:00 UTC,
|
82
83
|
estimated_expiration?: false,
|
@@ -106,7 +107,12 @@ A few highlights to note here:
|
|
106
107
|
|
107
108
|
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
109
|
|
109
|
-
You get a `NOTAM::ParseError` in case the raw NOTAM text message fails to be parsed.
|
110
|
+
You get a `NOTAM::ParseError` in case the raw NOTAM text message fails to be parsed. This error object features two notable methods:
|
111
|
+
|
112
|
+
* `item` – the faulty item (if already available)
|
113
|
+
* `cause` – the underlying error object (if any)
|
114
|
+
|
115
|
+
If you're sure the NOTAM is correct, please [submit an issue](#development) or fix the bug and [submit a pull request](#development).
|
110
116
|
|
111
117
|
See the [API documentation](https://www.rubydoc.info/gems/notam) for more.
|
112
118
|
|
@@ -114,8 +120,9 @@ See the [API documentation](https://www.rubydoc.info/gems/notam) for more.
|
|
114
120
|
|
115
121
|
### Anatomy of a NOTAM message
|
116
122
|
|
117
|
-
A NOTAM message consists of
|
123
|
+
A NOTAM message consists of the following items in order:
|
118
124
|
|
125
|
+
* Header: ID and type of NOTAM
|
119
126
|
* [Q item](https://www.rubydoc.info/gems/notam/NOTAM/Q): Essential information such as purpose or center point and radius
|
120
127
|
* [A item](https://www.rubydoc.info/gems/notam/NOTAM/A): Affected locations
|
121
128
|
* [B item](https://www.rubydoc.info/gems/notam/NOTAM/B): When the NOTAM becomes effective
|
@@ -124,6 +131,9 @@ A NOTAM message consists of a header followed by the following items:
|
|
124
131
|
* [E item](https://www.rubydoc.info/gems/notam/NOTAM/E): Free text description
|
125
132
|
* [F item](https://www.rubydoc.info/gems/notam/NOTAM/F): Upper limit (optional)
|
126
133
|
* [G item](https://www.rubydoc.info/gems/notam/NOTAM/G): Lower limit (optional)
|
134
|
+
* Footer: Any number of lines with metadata such as `CREATED` and `SOURCE`
|
135
|
+
|
136
|
+
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
137
|
|
128
138
|
### FIR
|
129
139
|
|
@@ -190,11 +200,18 @@ For compatibility, schedule dates and times are expressed using the correspondin
|
|
190
200
|
* [Day](https://www.rubydoc.info/gems/aixm/AIXM/Schedule/Day)
|
191
201
|
* [Time](https://www.rubydoc.info/gems/aixm/AIXM/Schedule/Time)
|
192
202
|
|
203
|
+
Raw and parsed NOTAM schedule times differ in how the "end of day" is encoded:
|
204
|
+
|
205
|
+
NOTAM | Beginning of Day | End of Day | Remarks
|
206
|
+
-------|------------------|------------|--------
|
207
|
+
Raw | `"0000"` | `"2359"` | `"2400"` is considered illegal
|
208
|
+
Parsed | `00:00` | `24:00` | the Ruby way
|
209
|
+
|
193
210
|
### References
|
194
211
|
|
195
|
-
* [ICAO
|
212
|
+
* [ICAO Doc 8126: Aeronautical Information Services Manual](https://www.icao.int/NACC/Documents/eDOCS/AIM/8126_unedited_en%20Jul2021.pdf)
|
213
|
+
* [EUROCONTROL Guidelines Operating Procedures AIS Dynamic Data (OPADD)](https://www.eurocontrol.int/sites/default/files/2021-07/eurocontrol-guidelines-opadd-ed4-1.pdf)
|
196
214
|
* [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
215
|
* [NOTAM Contractions](https://www.notams.faa.gov/downloads/contractions.pdf)
|
199
216
|
* [NOTAM format cheat sheet](http://vat-air.dk/files/ICAO%20NOTAM%20format.pdf)
|
200
217
|
* [Introduction on Wikipedia](https://en.wikipedia.org/wiki/NOTAM)
|
@@ -207,12 +224,13 @@ Please [create a translation request issue](https://github.com/svoop/notam/issue
|
|
207
224
|
|
208
225
|
## Tests and Fixtures
|
209
226
|
|
210
|
-
The test suite may run against live NOTAM
|
227
|
+
The test suite may run against live NOTAM depending on whether and how you set the `SPEC_SCOPE` environment variable:
|
211
228
|
|
212
229
|
```
|
230
|
+
export SPEC_SCOPE=none # don't run against any NOTAM fixtures (default)
|
231
|
+
export SPEC_SCOPE=W0214/22 # run against given NOTAM fixture only
|
213
232
|
export SPEC_SCOPE=all # run against all NOTAM fixtures
|
214
233
|
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
234
|
```
|
217
235
|
|
218
236
|
The NOTAM fixtures are written to `spec/fixtures`, you can manage them using a Rake tasks:
|
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/f.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module NOTAM
|
4
4
|
|
5
|
-
# The F item defines the
|
5
|
+
# The F item defines the lower limit for this NOTAM.
|
6
6
|
class F < Item
|
7
7
|
|
8
8
|
RE = %r(
|
@@ -17,7 +17,7 @@ module NOTAM
|
|
17
17
|
)x.freeze
|
18
18
|
|
19
19
|
# @return [AIXM::Z]
|
20
|
-
def
|
20
|
+
def lower_limit
|
21
21
|
case captures['all']
|
22
22
|
when 'UNL' then AIXM::UNLIMITED
|
23
23
|
when 'SFC', 'GND' then AIXM::GROUND
|
@@ -27,7 +27,7 @@ module NOTAM
|
|
27
27
|
|
28
28
|
# @see NOTAM::Item#merge
|
29
29
|
def merge
|
30
|
-
super(:
|
30
|
+
super(:lower_limit)
|
31
31
|
end
|
32
32
|
|
33
33
|
end
|
data/lib/notam/item/g.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module NOTAM
|
4
4
|
|
5
|
-
# The G item defines the
|
5
|
+
# The G item defines the upper limit for this NOTAM.
|
6
6
|
class G < Item
|
7
7
|
|
8
8
|
RE = %r(
|
@@ -17,7 +17,7 @@ module NOTAM
|
|
17
17
|
)x.freeze
|
18
18
|
|
19
19
|
# @return [AIXM::Z]
|
20
|
-
def
|
20
|
+
def upper_limit
|
21
21
|
case captures['all']
|
22
22
|
when 'UNL' then AIXM::UNLIMITED
|
23
23
|
when 'SFC', 'GND' then AIXM::GROUND
|
@@ -27,7 +27,7 @@ module NOTAM
|
|
27
27
|
|
28
28
|
# @see NOTAM::Item#merge
|
29
29
|
def merge
|
30
|
-
super(:
|
30
|
+
super(:upper_limit)
|
31
31
|
end
|
32
32
|
|
33
33
|
end
|
data/lib/notam/item/header.rb
CHANGED
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/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: 0.
|
4
|
+
version: 1.0.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-18 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.2
|
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.2
|
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
|