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 |