Skip to content

Commit

Permalink
Edge case tolerant extraction of part markers
Browse files Browse the repository at this point in the history
  • Loading branch information
Sven Schwyn committed Nov 18, 2022
1 parent 1af22f1 commit 750597c
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 73 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
* `NOTAM::Schedule.parse` now returns an array of `NOTAM_Schedule` instances
instead of just a single one.

#### Changes
* Edge case tolerant extraction of `PART n OF n` and `END PART n OF n` markers

#### Additions
* Support for datetime ranges (i.e. `1 APR 2000-20 MAY 2000`) as well as times
across midnight (i.e. `1 APR 1900-0500`) on D items.
Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ bundle install --trust-policy MediumSecurity
raw_notam_text_message = <<~END
W0902/22 NOTAMN
Q) LSAS/QRRCA/V/BO/W/000/148/4624N00702E004
A) LSAS B) 2204110900 C) 2205131400 EST
A) LSAS PART 2 OF 3 B) 2204110900 C) 2205131400 EST
D) APR 11 SR MINUS15-1900, 20-21 26-28 MAY 03-05 10-12 0530-2100, APR
14 22 29 MAY 06 13 0530-1400, APR 19 25 MAY 02 09 0800-2100
E) R-AREA LS-R7 HONGRIN ACT DUE TO FRNG.
F) GND
G) 14800FT AMSL
END PART 2 OF 3
CREATED: 11 Apr 2022 06:10:00
SOURCE: LSSNYNYX
END
Expand Down Expand Up @@ -75,8 +76,8 @@ The resulting hash for this example looks as follows:
center_point: #<AIXM::XY 46.40000000N 007.03333333E>,
radius: #<AIXM::D 4.0 nm>,
locations: ["LSAS"],
part_index: 1,
part_index_max: 1,
part_index: 2,
part_index_max: 3,
effective_at: 2022-04-11 09:00:00 UTC,
expiration_at: 2022-05-13 14:00:00 UTC,
estimated_expiration?: false,
Expand Down Expand Up @@ -119,8 +120,9 @@ See the [API documentation](https://www.rubydoc.info/gems/notam) for more.

### Anatomy of a NOTAM message

A NOTAM message consists of a header followed by the following items:
A NOTAM message consists of the following items in order:

* Header: ID and type of NOTAM
* [Q item](https://www.rubydoc.info/gems/notam/NOTAM/Q): Essential information such as purpose or center point and radius
* [A item](https://www.rubydoc.info/gems/notam/NOTAM/A): Affected locations
* [B item](https://www.rubydoc.info/gems/notam/NOTAM/B): When the NOTAM becomes effective
Expand All @@ -129,6 +131,9 @@ A NOTAM message consists of a header followed by the following items:
* [E item](https://www.rubydoc.info/gems/notam/NOTAM/E): Free text description
* [F item](https://www.rubydoc.info/gems/notam/NOTAM/F): Upper limit (optional)
* [G item](https://www.rubydoc.info/gems/notam/NOTAM/G): Lower limit (optional)
* Footer: Any number of lines with metadata such as `CREATED` and `SOURCE`

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.

### FIR

Expand Down Expand Up @@ -219,12 +224,13 @@ Please [create a translation request issue](https://github.com/svoop/notam/issue

## Tests and Fixtures

The test suite may run against live NOTAM if you set the `SPEC_SCOPE` environment variable:
The test suite may run against live NOTAM depending on whether and how you set the `SPEC_SCOPE` environment variable:

```
export SPEC_SCOPE=none # don't run against any NOTAM fixtures (default)
export SPEC_SCOPE=W0214/22 # run against given NOTAM fixture only
export SPEC_SCOPE=all # run against all NOTAM fixtures
export SPEC_SCOPE=all-fast # run against all NOTAM fixtures but exit on the first failure
export SPEC_SCOPE=W0214/22 # run against given NOTAM fixture only
```

The NOTAM fixtures are written to `spec/fixtures`, you can manage them using a Rake tasks:
Expand Down
13 changes: 1 addition & 12 deletions lib/notam/item/a.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ class A < Item
\A
A\)\s?
(?<locations>(?:#{ICAO_RE}\s?)+)
(?<parts>(?<part_index>\d+)\s+OF\s+(?<part_index_max>\d+))?
\z
)x.freeze

Expand All @@ -18,19 +17,9 @@ def locations
captures['locations'].split(/\s/)
end

# @return [Integer, nil]
def part_index
captures['parts'] ? captures['part_index'].to_i : 1
end

# @return [Integer, nil]
def part_index_max
captures['parts'] ? captures['part_index_max'].to_i : 1
end

# @see NOTAM::Item#merge
def merge
super(:locations, :part_index, :part_index_max)
super(:locations)
end

end
Expand Down
42 changes: 31 additions & 11 deletions lib/notam/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ module NOTAM

# NOTAM messages are plain text and consist of several ordered items:
#
# WDDDD/DD ... <- Header line (mandatory)
# Q) ... <- Q line: context (mandatory)
# A) ... <- A line: locations (mandatory)
# B) ... <- B line: effective from (mandatory)
# C) ... <- C line: effective until (optional)
# D) ... <- D line: timesheets (optional, may contain newlines)
# E) ... <- E line: description (mandatory, may contain newlines)
# F) ... <- F line: upper limit (optional)
# G) ... <- G line: lower limit (optional)
# WDDDD/DD ... <- Header (mandatory)
# Q) ... <- Q item: context (mandatory)
# A) ... <- A item: locations (mandatory)
# B) ... <- B item: effective from (mandatory)
# C) ... <- C item: effective until (optional)
# D) ... <- D item: timesheets (optional, may contain newlines)
# E) ... <- E item: description (mandatory, may contain newlines)
# F) ... <- F item: upper limit (optional)
# G) ... <- G item: lower limit (optional)
# CREATED: ... <- Footer (optional)
# SOURCE: ... <- Footer (optional)
#
# 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.
class Message

UNSUPPORTED_FORMATS = %r(
Expand All @@ -24,6 +29,10 @@ class Message
\w{3}\s[A-Z]\d{4}/\d{2}\sMILITARY # USA: military
)xi.freeze

PART_RE = %r(
(?:END\s+)?PART\s+(?<part_index>\d+)\s+OF\s+(?<part_index_max>\d+)
)xim.freeze

FINGERPRINTS = %w[Q) A) B) C) D) E) F) G) CREATED: SOURCE:].freeze

# Raw NOTAM text message
Expand All @@ -44,10 +53,9 @@ class Message
def initialize(text)
fail(NOTAM::ParserError, "unsupported format") unless self.class.supported_format? text
@text, @items, @data = text, [], {}
itemize(text).each do |raw_item|
itemize(departition(@text)).each do |raw_item|
item = NOTAM::Item.new(raw_item, data: @data).parse.merge
@items << item
@data = item.data
end
end

Expand Down Expand Up @@ -96,6 +104,18 @@ def supported_format?(text)

private

# @return [String]
def departition(text)
text.gsub(PART_RE, '').tap do
if $~ # part marker found
@data.merge!(
part_index: $~[:part_index].to_i,
part_index_max: $~[:part_index_max].to_i
)
end
end
end

# @return [Array]
def itemize(text)
lines = text.gsub(/\s(#{NOTAM::Item::RE})/, "\n\\1").split("\n")
Expand Down
65 changes: 50 additions & 15 deletions spec/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,55 @@ class Factory
class << self

def message
<<~END
A0135/20 NOTAMN
Q) EGTT/QMRXX/IV/NBO/A/000/999/5129N00028W005
A) EGLL
B) 0201010600
C) 0203312300
D) MON-WED FRI SR MINUS15-1130 EXC 01-02 FEB 01-02 MAR 01-02
E) RWY 09R/27L DUE WIP NO CENTRELINE, TDZ OR SALS LIGHTING AVBL
F) 2000 FT AMSL
G) 2050 FT AMSL
CREATED: 01 Jan 2002 01:00:00
SOURCE: LSSNYNYX
END
@message ||= {
single:
<<~NEND,
A0135/20 NOTAMN
Q) EGTT/QMRXX/IV/NBO/A/000/999/5129N00028W005
A) EGLL
B) 0201010600
C) 0203312300
D) MON-WED FRI SR MINUS15-1130 EXC 01-02 FEB 01-02 MAR 01-02
E) RWY 09R/27L DUE WIP NO CENTRELINE, TDZ OR SALS LIGHTING AVBL
F) 2000 FT AMSL
G) 2050 FT AMSL
CREATED: 01 Jan 2002 01:00:00
SOURCE: LSSNYNYX
NEND
partitioned_without_end:
<<~NEND,
D3616/22 NOTAMN
Q) EDGG/QRTCA/IV/BO /W /000/100/5003N00804E012
A) EDGG PART 10 OF 11 B) 2211160800 C) 2211181800
D) NOV 16 0800-2200, NOV 17 0500-2200, NOV 18 0500-1800
E) TEMPO RESTRICTED AREA EDR RHEINGAU ESTABLISHED
CREATED: 15 Nov 2022 15:42:00
SOURCE: EUECYIYN
NEND
partitioned_with_end:
<<~NEND,
D3616/22 NOTAMN
Q) EDGG/QRTCA/IV/BO /W /000/100/5003N00804E012
A) EDGG PART 1 OF 2 B) 2211160800 C) 2211181800
D) NOV 16 0800-2200, NOV 17 0500-2200, NOV 18 0500-1800
E) TEMPO RESTRICTED AREA EDR RHEINGAU ESTABLISHED
END PART 1 OF 2
CREATED: 15 Nov 2022 15:42:00
SOURCE: EUECYIYN
NEND
partitioned_with_end_anywhere:
<<~NEND,
D3616/22 NOTAMN
Q) EDGG/QRTCA/IV/BO /W /000/100/5003N00804E012
A) EDGG PART 2 OF 2 B) 2211160800 C) 2211181800
D) NOV 16 0800-2200, NOV 17 0500-2200, NOV 18 0500-1800
E) - FLTS CONDUCTED ENTIRELY UNDER IFR
F) GND G) FL100
END PART 2 OF 2
CREATED: 15 Nov 2022 15:42:00
SOURCE: EUECYIYN
NEND
}
end

def header
Expand All @@ -38,8 +74,7 @@ def q
def a
@a ||= {
egll: 'A) EGLL',
lsas: 'A) LSAS LOVV LIMM',
checklist: 'A) LSAS PART 1 OF 5'
lsas: 'A) LSAS LOVV LIMM'
}
end

Expand Down
20 changes: 0 additions & 20 deletions spec/lib/notam/item/a_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,4 @@
_(subject[:lsas].locations).must_equal %w(LSAS LOVV LIMM)
end
end

describe :part_index do
it "detects multipart NOTAM index" do
_(subject[:checklist].part_index).must_equal 1
end

it "returns 1 for non-multipart NOTAM" do
_(subject[:egll].part_index).must_equal 1
end
end

describe :part_index_max do
it "detects multipart NOTAM max index" do
_(subject[:checklist].part_index_max).must_equal 5
end

it "returns 1 for non-multipart NOTAM" do
_(subject[:egll].part_index_max).must_equal 1
end
end
end
Loading

0 comments on commit 750597c

Please sign in to comment.