rweather_decoder/
metar.rs

1//! Module for decoding METAR reports.
2//!
3//! The decoding is written based on the following publications:
4//! - World Meteorological Organization (2022). Aerodrome reports and forecasts: A Users’ Handbook to the Codes. Available: <https://library.wmo.int/idurl/4/30224>.
5//! - World Meteorological Organization (2019). Manual on Codes, Volume I.1 – International Codes. Available: <https://library.wmo.int/idurl/4/35713>.
6//! - World Meteorological Organization (2018). Manual on Codes, Volume II – Regional Codes and National Coding Practices. Available: <https://library.wmo.int/idurl/4/35717>.
7
8use std::{ops::{Div, Mul}, str::FromStr};
9
10use anyhow::{anyhow, Error, Result};
11use chrono::{NaiveDateTime, NaiveTime, Datelike, Duration};
12use chronoutil::RelativeDuration;
13use lazy_static::lazy_static;
14use regex::Regex;
15use serde::{Serialize, Deserialize};
16
17use crate::datetime::{UtcDateTime, UtcDayTime, UtcTime};
18
19lazy_static! {
20    static ref WHITESPACE_REPLACE_RE: Regex = Regex::new(r"\s+").unwrap();
21    static ref WHITESPACE_REPLACE_OUT: &'static str = " ";
22
23    static ref END_REPLACE_RE: Regex = Regex::new(r"[\s=]*$").unwrap();
24    static ref END_REPLACE_OUT: &'static str = " ";
25
26    static ref SECTION_RE: Regex = Regex::new(r"(?x)
27        ^(?P<section>NOSIG|TEMPO|BECMG|RMK)
28        (?P<end>\s)
29    ").unwrap();
30
31    static ref HEADER_RE: Regex = Regex::new(r"(?x)
32        ^(?P<station_id>[A-Z][A-Z0-9]{3})
33        \s
34        (?P<day>\d\d)
35        (?P<hour>\d\d)
36        (?P<minute>\d\d)\d?Z?
37        (\s(?P<corrected>COR|CC[A-Z]))?
38        (\s(?P<auto>AUTO))?
39        (?P<end>\s)
40    ").unwrap();
41
42    static ref WIND_RE: Regex = Regex::new(r"(?x)
43        ^E?(?P<direction>\d\d\d|VRB|///)
44        (?P<speed>P?\d\d|//)
45        (G(?P<gust>P?\d\d|//))?
46        (?P<units>KT|MPS)
47        (\s(?P<direction_range>\d\d\dV\d\d\d))?
48        (?P<end>\s)
49    ").unwrap();
50
51    static ref VISIBILITY_RE: Regex = Regex::new(r"(?x)
52        ^(?P<prevailing>[MP]?(\d+\s)?\d/\d{1,2}|[MP]?\d{1,5}|////|[CK]AVOK)
53        (NDV)?
54        \s?
55        (?P<units>SM|KM)?
56        (\s(?P<minimum>[MP]?\d{1,4}))?
57        (?P<directional>(\s[MP]?\d{1,4}[NESW][EW]?)+)?
58        (?P<end>\s)
59    ").unwrap();
60
61    static ref DIRECTIONAL_VISIBILITY_RE: Regex = Regex::new(r"(?x)
62        ^(?P<visibility>[MP]?\d{1,4})
63        (?P<direction>[NESW][EW]?)
64    ").unwrap();
65
66    static ref RUNWAY_VISUAL_RANGE_RE: Regex = Regex::new(r"(?x)
67        ^R(?P<runway>\d\d[A-Z]?)
68        /
69        (?P<visual_range>[MP]?\d\d\d\d(V[MP]?\d\d\d\d)?)
70        (?P<units>FT)?
71        /?
72        (?P<trend>[UDN])?
73        (?P<end>\s)
74    ").unwrap();
75
76    static ref PRESENT_WEATHER_RE: Regex = Regex::new(r"(?x)
77        ^(?P<intensity>[-\+])?
78        (?P<code>(VC|MI|BC|PR|DR|BL|SH|TS|FZ|DZ|RA|SN|SG|PL|GR|GS|UP|BR|FG|FU|VA|DU|SA|HZ|PO|SQ|FC|SS|DS|IC|PY|NSW)+)
79        (?P<end>\s)
80    ").unwrap();
81
82    static ref CLOUD_RE: Regex = Regex::new(r"(?x)
83        ^(?P<cover>CLR|SKC|NSC|NCD|FEW|SCT|BKN|OVC|VV|///)
84        (?P<height>\d{1,3}|///)?
85        (?P<cloud>AC|ACC|ACSL|AS|CB|CBMAM|CC|CCSL|CI|CS|CU|NS|SC|SCSL|ST|TC?U|///)?
86        (?P<end>\s)
87    ").unwrap();
88
89    static ref TEMPERATURE_RE: Regex = Regex::new(r"(?x)
90        ^(?P<temperature>M?\d{1,2}|//|XX)
91        /
92        (?P<dew_point>M?\d{1,2}|//|XX)?
93        (?P<end>\s)
94    ").unwrap();
95
96    static ref PRESSURE_RE: Regex = Regex::new(r"(?x)
97        ^(?P<units>A|Q)
98        (?P<pressure>\d{3,4}|////)
99        (?P<end>\s)
100    ").unwrap();
101
102    static ref RECENT_WEATHER_RE: Regex = Regex::new(r"(?x)
103        ^RE(?P<intensity>[-\+])?
104        (?P<code>(VC|MI|BC|PR|DR|BL|SH|TS|FZ|DZ|RA|SN|SG|PL|GR|GS|UP|BR|FG|FU|VA|DU|SA|HZ|PO|SQ|FC|SS|DS|IC|PY|NSW)+)
105        (?P<end>\s)
106    ").unwrap();
107
108    static ref WIND_SHEAR_RE: Regex = Regex::new(r"(?x)
109        ^WS
110        \s
111        (?P<runway>R\d\d[A-Z]?|ALL\sRWY)
112        (?P<end>\s)
113    ").unwrap();
114
115    static ref SEA_RE: Regex = Regex::new(r"(?x)
116        ^W(?P<temperature>M?\d{1,2}|//|XX)
117        /
118        (S(?P<state>\d|/))?
119        (H(?P<height>\d{1,3}|///))?
120        (?P<end>\s)
121    ").unwrap();
122
123    static ref COLOR_RE: Regex = Regex::new(r"(?x)
124        ^(BLACK|BLU\+?|GRN|WHT|RED|AMB|YLO)+
125        (?P<end>\s)
126    ").unwrap();
127
128    static ref RAINFALL_RE: Regex = Regex::new(r"(?x)
129        ^RF[\d/]{2}[\./][\d/]/[\d/]{3}[\./][\d/]
130        (?P<end>\s)
131    ").unwrap();
132
133    static ref RUNWAY_STATE_RE: Regex = Regex::new(r"(?x)
134        ^R\d\d[A-Z]?/([\d/]{6}|CLRD[\d/]{2})
135        (?P<end>\s)
136    ").unwrap();
137
138    static ref TREND_TIME_RE: Regex = Regex::new(r"(?x)
139        ^(?P<indicator>FM|TL|AT)
140        \s?
141        (?P<hour>\d\d)
142        (?P<minute>\d\d)Z?
143        (?P<end>\s)
144    ").unwrap();
145}
146
147/// TREND forecast change indicator.
148///
149/// JSON representation is in lowercase snake case.
150#[non_exhaustive]
151#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum Trend {
154    /// No significant changes are expected.
155    #[default]
156    NoSignificantChange,
157    /// Expected temporary fluctuations in the meteorological conditions.
158    Temporary,
159    /// Expected changes which reach or pass specified values.
160    Becoming,
161}
162
163impl FromStr for Trend {
164    type Err = Error;
165
166    fn from_str(s: &str) -> Result<Self, Self::Err> {
167        match s {
168            "NOSIG" => Ok(Trend::NoSignificantChange),
169            "TEMPO" => Ok(Trend::Temporary),
170            "BECMG" => Ok(Trend::Becoming),
171            _ => Err(anyhow!("Invalid trend, given {}", s))
172        }
173    }
174}
175
176#[non_exhaustive]
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case")]
179enum Section {
180    Main,
181    Trend(Trend),
182    Remark,
183}
184
185impl FromStr for Section {
186    type Err = Error;
187
188    fn from_str(s: &str) -> Result<Self, Self::Err> {
189        match s {
190            "RMK" => Ok(Section::Remark),
191            s => match Trend::from_str(s) {
192                Ok(trend) => Ok(Section::Trend(trend)),
193                Err(_) => Err(anyhow!("Invalid section, given {}", s))
194            }
195        }
196    }
197}
198
199fn handle_section(text: &str) -> Option<(Section, usize)> {
200    SECTION_RE.captures(text)
201        .map(|capture| {
202            let section = Section::from_str(&capture["section"]).unwrap();
203            let end = capture.name("end").unwrap().end();
204
205            (section, end)
206        })
207}
208
209/// METAR date and time combinations.
210///
211/// JSON representation is adjacently tagged and in lowercase snake case. Example:
212/// ```json
213/// {
214///     "value_type": "date_time",
215///     "value": "2023-12-27T08:30:00Z"
216/// }
217/// ```
218#[non_exhaustive]
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(tag = "value_type", content = "value", rename_all = "snake_case")]
221pub enum MetarTime {
222    /// Date and time.
223    DateTime(UtcDateTime),
224    /// Day and time.
225    DayTime(UtcDayTime),
226    /// Time only.
227    Time(UtcTime),
228}
229
230impl MetarTime {
231    /// Converts any [MetarTime] into [MetarTime::DateTime].
232    ///
233    /// Using `anchor_time`, any [MetarTime] will be converted to a [MetarTime::DateTime] that is nearest
234    /// to the specified `anchor_time` while preserving all the datetime information in the input [MetarTime].
235    /// This conversion correctly handles months with different number of days and also leap years.
236    pub fn to_date_time(&self, anchor_time: NaiveDateTime) -> MetarTime {
237        match self {
238            MetarTime::DateTime(utc_dt) => MetarTime::DateTime(*utc_dt),
239            MetarTime::DayTime(utc_d_t) => {
240                let first_guess_opt = anchor_time.date().with_day(utc_d_t.0).map(|nd| nd.and_time(utc_d_t.1));
241                let second_guess_opt = (anchor_time + RelativeDuration::months(-1)).date().with_day(utc_d_t.0).map(|nd| nd.and_time(utc_d_t.1));
242                let third_guess_opt = (anchor_time + RelativeDuration::months(1)).date().with_day(utc_d_t.0).map(|nd| nd.and_time(utc_d_t.1));
243
244                let mut final_guess_opt = None;
245                let mut final_delta = i64::MAX;
246
247                for guess_opt in [first_guess_opt, second_guess_opt, third_guess_opt] {
248                    if let Some(guess) = guess_opt {
249                        let delta = guess.signed_duration_since(anchor_time).num_seconds().abs();
250                        if delta < final_delta {
251                            final_guess_opt = guess_opt;
252                            final_delta = delta;
253                        }
254                    }
255                }
256
257                match final_guess_opt {
258                    Some(final_guess) => MetarTime::DateTime(UtcDateTime(final_guess)),
259                    None => panic!("{}", format!("Date guessing failed, given time {:?} and anchor time {}", self, anchor_time))
260                }
261            },
262            MetarTime::Time(utc_t) => {
263                let first_guess = anchor_time.date().and_time(utc_t.0);
264                let second_guess = first_guess + Duration::days(-1);
265                let third_guess = first_guess + Duration::days(1);
266
267                let mut final_guess = first_guess;
268                let mut final_delta = final_guess.signed_duration_since(anchor_time).num_seconds().abs();
269
270                for guess in [second_guess, third_guess] {
271                    let delta = guess.signed_duration_since(anchor_time).num_seconds().abs();
272                    if delta < final_delta {
273                        final_guess = guess;
274                        final_delta = delta;
275                    }
276                }
277
278                MetarTime::DateTime(UtcDateTime(final_guess))
279            },
280        }
281    }
282}
283
284/// Identification groups.
285#[non_exhaustive]
286#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
287pub struct Header {
288    /// ICAO airport code.
289    pub station_id: Option<String>,
290    /// Observation time of the report.
291    pub observation_time: Option<MetarTime>,
292    /// Flag if the report is corrected.
293    pub is_corrected: Option<bool>,
294    /// Flag if the report comes from a fully automated observation.
295    pub is_automated: Option<bool>,
296}
297
298impl Header {
299    fn is_empty(&self) -> bool {
300        self.station_id.is_none() && self.observation_time.is_none() && self.is_corrected.is_none() && self.is_automated.is_none()
301    }
302}
303
304fn handle_header(text: &str, anchor_time: Option<NaiveDateTime>) -> Option<(Header, usize)> {
305    HEADER_RE.captures(text)
306        .map(|capture| {
307            let station_id = Some(capture["station_id"].to_string());
308
309            let day = capture["day"].parse().unwrap();
310            let hour = capture["hour"].parse().unwrap();
311            let minute = capture["minute"].parse().unwrap();
312
313            let naive_time = NaiveTime::from_hms_opt(hour, minute, 0);
314            let mut time = naive_time.map(|nt| MetarTime::DayTime(UtcDayTime(day, nt)));
315
316            if let Some(at) = anchor_time {
317                time = time.map(|t| t.to_date_time(at));
318            }
319
320            let is_corrected = Some(capture.name("corrected").is_some());
321
322            let is_automated = Some(capture.name("auto").is_some());
323
324            let end = capture.name("end").unwrap().end();
325
326            let header = Header { station_id, observation_time: time, is_corrected, is_automated };
327
328            (header, end)
329        })
330}
331
332/// Unit of a physical quantity.
333///
334/// JSON representation is using common symbols.
335#[non_exhaustive]
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
337pub enum Unit {
338    /// True degree.
339    ///
340    /// JSON representation:
341    /// ```json
342    /// "degT"
343    /// ```
344    #[serde(rename = "degT")]
345    DegreeTrue,
346    /// Knot.
347    ///
348    /// JSON representation:
349    /// ```json
350    /// "kt"
351    /// ```
352    #[serde(rename = "kt")]
353    Knot,
354    /// Metre per second.
355    ///
356    /// JSON representation:
357    /// ```json
358    /// "m/s"
359    /// ```
360    #[serde(rename = "m/s")]
361    MetrePerSecond,
362    /// Kilometre.
363    ///
364    /// JSON representation:
365    /// ```json
366    /// "km"
367    /// ```
368    #[serde(rename = "km")]
369    KiloMetre,
370    /// Metre.
371    ///
372    /// JSON representation:
373    /// ```json
374    /// "m"
375    /// ```
376    #[serde(rename = "m")]
377    Metre,
378    /// Statute mile.
379    ///
380    /// JSON representation:
381    /// ```json
382    /// "mi"
383    /// ```
384    #[serde(rename = "mi")]
385    StatuteMile,
386    /// Foot.
387    ///
388    /// JSON representation:
389    /// ```json
390    /// "ft"
391    /// ```
392    #[serde(rename = "ft")]
393    Foot,
394    /// Degree Celsius.
395    ///
396    /// JSON representation:
397    /// ```json
398    /// "degC"
399    /// ```
400    #[serde(rename = "degC")]
401    DegreeCelsius,
402    /// Hectopascal.
403    ///
404    /// JSON representation:
405    /// ```json
406    /// "hPa"
407    /// ```
408    #[serde(rename = "hPa")]
409    HectoPascal,
410    /// Inch of mercury.
411    ///
412    /// JSON representation:
413    /// ```json
414    /// "inHg"
415    /// ```
416    #[serde(rename = "inHg")]
417    InchOfMercury,
418}
419
420impl FromStr for Unit {
421    type Err = Error;
422
423    fn from_str(s: &str) -> Result<Self, Self::Err> {
424        match s {
425            "KT" => Ok(Unit::Knot),
426            "MPS" => Ok(Unit::MetrePerSecond),
427            "KM" => Ok(Unit::KiloMetre),
428            "SM" => Ok(Unit::StatuteMile),
429            "FT" => Ok(Unit::Foot),
430            "Q" => Ok(Unit::HectoPascal),
431            "A" => Ok(Unit::InchOfMercury),
432            _ => Err(anyhow!("Invalid units, given {}", s))
433        }
434    }
435}
436
437fn parse_value(s: &str) -> Result<f32> {
438    if s.contains(' ') && s.contains('/') {
439        let mut split_space = s.split(' ');
440        let number: f32 = split_space.next().unwrap().parse()?;
441
442        let mut split_slash = split_space.next().unwrap().split('/');
443        let numerator: f32 = split_slash.next().unwrap().parse()?;
444        let denominator: f32 = split_slash.next().unwrap().parse()?;
445
446        Ok(number + numerator / denominator)
447    } else if s.contains('/') {
448        let mut split = s.split('/');
449        let numerator: f32 = split.next().unwrap().parse()?;
450        let denominator: f32 = split.next().unwrap().parse()?;
451
452        Ok(numerator / denominator)
453    } else {
454        Ok(s.parse()?)
455    }
456}
457
458/// Value in range variants.
459///
460/// JSON representation is adjacently tagged and in lowercase snake case. Example:
461/// ```json
462/// {
463///     "value_type": "above",
464///     "value": 3.5
465/// }
466/// ```
467#[non_exhaustive]
468#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
469#[serde(tag = "value_type", content = "value", rename_all = "snake_case")]
470pub enum ValueInRange {
471    /// Above specified number.
472    Above(f32),
473    /// Below specified number.
474    Below(f32),
475    /// Same as specified number.
476    Exact(f32),
477}
478
479impl FromStr for ValueInRange {
480    type Err = Error;
481
482    fn from_str(s: &str) -> Result<Self, Self::Err> {
483        if let Some(stripped) = s.strip_prefix('P') {
484            let value = parse_value(stripped).unwrap();
485            Ok(ValueInRange::Above(value))
486        } else if let Some(stripped) = s.strip_prefix('M') {
487            let value = parse_value(stripped).unwrap();
488            Ok(ValueInRange::Below(value))
489        } else {
490            let value = parse_value(s).unwrap();
491            Ok(ValueInRange::Exact(value))
492        }
493    }
494}
495
496impl Div<f32> for ValueInRange {
497    type Output = ValueInRange;
498
499    fn div(self, rhs: f32) -> Self::Output {
500        match self {
501            ValueInRange::Above(x) => ValueInRange::Above(x / rhs),
502            ValueInRange::Below(x) => ValueInRange::Below(x / rhs),
503            ValueInRange::Exact(x) => ValueInRange::Exact(x / rhs),
504        }
505    }
506}
507
508impl Mul<f32> for ValueInRange {
509    type Output = ValueInRange;
510
511    fn mul(self, rhs: f32) -> Self::Output {
512        match self {
513            ValueInRange::Above(x) => ValueInRange::Above(x * rhs),
514            ValueInRange::Below(x) => ValueInRange::Below(x * rhs),
515            ValueInRange::Exact(x) => ValueInRange::Exact(x * rhs),
516        }
517    }
518}
519
520/// Value variants.
521///
522/// JSON representation is adjacently tagged and in lowercase snake case. Example:
523/// ```json
524/// {
525///     "value_type": "below",
526///     "value": 3.5
527/// }
528/// ```
529#[non_exhaustive]
530#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
531#[serde(tag = "value_type", content = "value", rename_all = "snake_case")]
532pub enum Value {
533    /// Variable number.
534    Variable,
535    /// Above specified number.
536    Above(f32),
537    /// Below specified number.
538    Below(f32),
539    /// Between specified [`ValueInRange`] values.
540    Range(ValueInRange, ValueInRange),
541    /// Same as specified number.
542    Exact(f32),
543}
544
545impl FromStr for Value {
546    type Err = Error;
547
548    fn from_str(s: &str) -> Result<Self, Self::Err> {
549        if s == "VRB" {
550            Ok(Value::Variable)
551        } else if s.contains('V') {
552            let mut split = s.split('V');
553            let value1 = ValueInRange::from_str(split.next().unwrap()).unwrap();
554            let value2 = ValueInRange::from_str(split.next().unwrap()).unwrap();
555            Ok(Value::Range(value1, value2))
556        } else if let Some(stripped) = s.strip_prefix('P') {
557            let value = parse_value(stripped).unwrap();
558            Ok(Value::Above(value))
559        } else if let Some(stripped) = s.strip_prefix('M') {
560            let value = parse_value(stripped).unwrap();
561            Ok(Value::Below(value))
562        } else {
563            let value = parse_value(s).unwrap();
564            Ok(Value::Exact(value))
565        }
566    }
567}
568
569impl Div<f32> for Value {
570    type Output = Value;
571
572    fn div(self, rhs: f32) -> Self::Output {
573        match self {
574            Value::Variable => Value::Variable,
575            Value::Above(x) => Value::Above(x / rhs),
576            Value::Below(x) => Value::Below(x / rhs),
577            Value::Range(x, y) => Value::Range(x / rhs, y / rhs),
578            Value::Exact(x) => Value::Exact(x / rhs),
579        }
580    }
581}
582
583impl Mul<f32> for Value {
584    type Output = Value;
585
586    fn mul(self, rhs: f32) -> Self::Output {
587        match self {
588            Value::Variable => Value::Variable,
589            Value::Above(x) => Value::Above(x * rhs),
590            Value::Below(x) => Value::Below(x * rhs),
591            Value::Range(x, y) => Value::Range(x * rhs, y * rhs),
592            Value::Exact(x) => Value::Exact(x * rhs),
593        }
594    }
595}
596
597/// Physical quantity.
598#[non_exhaustive]
599#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
600pub struct Quantity {
601    /// Value.
602    ///
603    /// JSON representation is flattened once.
604    #[serde(flatten)]
605    pub value: Value,
606    pub units: Unit,
607}
608
609impl Quantity {
610    fn new(value: Value, units: Unit) -> Quantity {
611        Quantity { value, units }
612    }
613
614    fn new_opt(value: Option<Value>, units: Unit) -> Option<Quantity> {
615        value.map(|v| Quantity { value: v, units })
616    }
617}
618
619/// Surface wind groups.
620#[non_exhaustive]
621#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
622pub struct Wind {
623    /// Wind direction. Reported by the direction from which the wind originates.
624    pub wind_from_direction: Option<Quantity>,
625    /// Range of wind directions if they vary significantly.
626    /// Reported by the direction from which the wind originates.
627    pub wind_from_direction_range: Option<Quantity>,
628    pub wind_speed: Option<Quantity>,
629    pub wind_gust: Option<Quantity>,
630}
631
632impl Wind {
633    fn is_empty(&self) -> bool {
634        self.wind_from_direction.is_none() && self.wind_from_direction_range.is_none() && self.wind_speed.is_none() && self.wind_gust.is_none()
635    }
636}
637
638fn handle_wind(text: &str) -> Option<(Wind, usize)> {
639    WIND_RE.captures(text)
640        .map(|capture| {
641            let mut from_direction_value = match &capture["direction"] {
642                "///" => None,
643                s => Some(Value::from_str(s).unwrap()),
644            };
645
646            if &capture["direction"] == "000" && &capture["speed"] == "00" {
647                // calm wind has no direction
648                from_direction_value = None;
649            }
650
651            let speed_value = match &capture["speed"] {
652                "//" => None,
653                s => Some(Value::from_str(s).unwrap()),
654            };
655
656            let gust_value = capture.name("gust").and_then(|c| match c.as_str() {
657                "//" => None,
658                s => Some(Value::from_str(s).unwrap()),
659            });
660
661            let units = Unit::from_str(&capture["units"]).unwrap();
662
663            let from_direction_range_value = capture.name("direction_range")
664                .map(|s| Value::from_str(s.as_str()).unwrap());
665
666            let wind_from_direction = Quantity::new_opt(from_direction_value, Unit::DegreeTrue);
667            let wind_from_direction_range = Quantity::new_opt(from_direction_range_value, Unit::DegreeTrue);
668            let wind_speed = Quantity::new_opt(speed_value, units);
669            let wind_gust = Quantity::new_opt(gust_value, units);
670
671            let end = capture.name("end").unwrap().end();
672
673            let wind = Wind { wind_from_direction, wind_from_direction_range, wind_speed, wind_gust };
674
675            (wind, end)
676        })
677}
678
679/// Direction octant.
680///
681/// JSON representation is in lowercase snake case.
682#[non_exhaustive]
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
684#[serde(rename_all = "snake_case")]
685pub enum DirectionOctant {
686    North,
687    NorthEast,
688    East,
689    SouthEast,
690    South,
691    SouthWest,
692    West,
693    NorthWest,
694}
695
696impl FromStr for DirectionOctant {
697    type Err = Error;
698
699    fn from_str(s: &str) -> Result<Self, Self::Err> {
700        match s {
701            "N" => Ok(DirectionOctant::North),
702            "NE" => Ok(DirectionOctant::NorthEast),
703            "E" => Ok(DirectionOctant::East),
704            "SE" => Ok(DirectionOctant::SouthEast),
705            "S" => Ok(DirectionOctant::South),
706            "SW" => Ok(DirectionOctant::SouthWest),
707            "W" => Ok(DirectionOctant::West),
708            "NW" => Ok(DirectionOctant::NorthWest),
709            _ => Err(anyhow!("Invalid direction octant, given {}", s))
710        }
711    }
712}
713
714/// Directional visibility.
715#[non_exhaustive]
716#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
717pub struct DirectionalVisibility {
718    pub visibility: Quantity,
719    pub direction: DirectionOctant,
720}
721
722/// Visibility groups.
723#[non_exhaustive]
724#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
725pub struct Visibility {
726    pub prevailing_visibility: Option<Quantity>,
727    pub minimum_visibility: Option<Quantity>,
728    pub directional_visibilites: Vec<DirectionalVisibility>,
729}
730
731impl Visibility {
732    fn is_empty(&self) -> bool {
733        self.prevailing_visibility.is_none() && self.minimum_visibility.is_none() && self.directional_visibilites.is_empty()
734    }
735}
736
737fn handle_visibility(text: &str) -> Option<(Visibility, bool, usize)> {
738    VISIBILITY_RE.captures(text)
739        .map(|capture| {
740            let mut is_cavok = false;
741
742            let mut prevailing_visibility_value = match &capture["prevailing"] {
743                "////" => None,
744                "CAVOK" | "KAVOK" => {
745                    is_cavok = true;
746                    Some(Value::Above(10000.0))
747                },
748                s => Some(Value::from_str(s).unwrap()),
749            };
750
751            let units = capture.name("units")
752                .map(|c| Unit::from_str(c.as_str()).unwrap())
753                .unwrap_or(Unit::Metre);
754
755            if prevailing_visibility_value == Some(Value::Exact(9999.0)) && units == Unit::Metre {
756                prevailing_visibility_value = Some(Value::Above(10000.0));
757            }
758
759            let minimum_visibility_value = capture.name("minimum").map(|c| Value::from_str(c.as_str()).unwrap());
760
761            let directional_visibilites = capture.name("directional")
762                .map(|c| c.as_str().split(' ')
763                    .map(|group| DIRECTIONAL_VISIBILITY_RE.captures(group))
764                    .filter(|capture| capture.is_some())
765                    .map(|capture| DirectionalVisibility {
766                        visibility: Quantity::new(Value::from_str(&capture.as_ref().unwrap()["visibility"]).unwrap(), units),
767                        direction: DirectionOctant::from_str(&capture.unwrap()["direction"]).unwrap(),
768                    })
769                    .collect::<Vec<_>>())
770                .unwrap_or_default();
771
772            let prevailing_visibility = Quantity::new_opt(prevailing_visibility_value, units);
773            let minimum_visibility = Quantity::new_opt(minimum_visibility_value, units);
774
775            let end = capture.name("end").unwrap().end();
776
777            let visibility = Visibility { prevailing_visibility, minimum_visibility, directional_visibilites };
778
779            (visibility, is_cavok, end)
780        })
781}
782
783/// Runway visual range (RVR) trend.
784///
785/// JSON representation is in lowercase snake case.
786#[non_exhaustive]
787#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
788#[serde(rename_all = "snake_case")]
789pub enum RunwayVisualRangeTrend {
790    Increasing,
791    Decreasing,
792    NoChange,
793}
794
795impl FromStr for RunwayVisualRangeTrend {
796    type Err = Error;
797
798    fn from_str(s: &str) -> Result<Self, Self::Err> {
799        match s {
800            "U" => Ok(RunwayVisualRangeTrend::Increasing),
801            "D" => Ok(RunwayVisualRangeTrend::Decreasing),
802            "N" => Ok(RunwayVisualRangeTrend::NoChange),
803            _ => Err(anyhow!("Invalid runway visual range trend, given {}", s))
804        }
805    }
806}
807
808/// Runway visual range (RVR).
809#[non_exhaustive]
810#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
811pub struct RunwayVisualRange {
812    pub runway: String,
813    pub visual_range: Quantity,
814    pub trend: Option<RunwayVisualRangeTrend>,
815}
816
817fn handle_runway_visual_range(text: &str) -> Option<(RunwayVisualRange, usize)> {
818    RUNWAY_VISUAL_RANGE_RE.captures(text)
819        .map(|capture| {
820            let runway = capture["runway"].to_string();
821
822            let visual_range_value = Value::from_str(&capture["visual_range"]).unwrap();
823
824            let units = capture.name("units")
825                .map(|c| Unit::from_str(c.as_str()).unwrap())
826                .unwrap_or(Unit::Metre);
827
828            let trend = capture.name("trend")
829                .map(|c| RunwayVisualRangeTrend::from_str(c.as_str()).unwrap());
830
831            let visual_range = Quantity::new(visual_range_value, units);
832
833            let end = capture.name("end").unwrap().end();
834
835            let rvr = RunwayVisualRange { runway, visual_range, trend };
836
837            (rvr, end)
838        })
839}
840
841/// Weather intensity.
842///
843/// JSON representation is in lowercase snake case.
844#[non_exhaustive]
845#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
846#[serde(rename_all = "snake_case")]
847pub enum WeatherIntensity {
848    Light,
849    Moderate,
850    Heavy,
851}
852
853impl FromStr for WeatherIntensity {
854    type Err = Error;
855
856    fn from_str(s: &str) -> Result<Self, Self::Err> {
857        match s {
858            "-" => Ok(WeatherIntensity::Light),
859            "+" => Ok(WeatherIntensity::Heavy),
860            _ => Err(anyhow!("Invalid weather intensity, given {}", s))
861        }
862    }
863}
864
865/// Weather descriptor.
866///
867/// JSON representation is in lowercase snake case.
868#[non_exhaustive]
869#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
870#[serde(rename_all = "snake_case")]
871pub enum WeatherDescriptor {
872    Shallow,
873    Patches,
874    Partial,
875    LowDrifting,
876    Blowing,
877    Shower,
878    Thunderstorm,
879    Freezing,
880}
881
882impl FromStr for WeatherDescriptor {
883    type Err = Error;
884
885    fn from_str(s: &str) -> Result<Self, Self::Err> {
886        match s {
887            "MI" => Ok(WeatherDescriptor::Shallow),
888            "BC" => Ok(WeatherDescriptor::Patches),
889            "PR" => Ok(WeatherDescriptor::Partial),
890            "DR" => Ok(WeatherDescriptor::LowDrifting),
891            "BL" => Ok(WeatherDescriptor::Blowing),
892            "SH" => Ok(WeatherDescriptor::Shower),
893            "TS" => Ok(WeatherDescriptor::Thunderstorm),
894            "FZ" => Ok(WeatherDescriptor::Freezing),
895            _ => Err(anyhow!("Invalid weather descriptor, given {}", s))
896        }
897    }
898}
899
900/// Weather phenomena.
901///
902/// JSON representation is in lowercase snake case.
903#[non_exhaustive]
904#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
905#[serde(rename_all = "snake_case")]
906pub enum WeatherPhenomena {
907    Drizzle,
908    Rain,
909    Snow,
910    SnowGrains,
911    IcePellets,
912    Hail,
913    SnowPellets,
914    UnknownPrecipitation,
915    Mist,
916    Fog,
917    Smoke,
918    VolcanicAsh,
919    Dust,
920    Sand,
921    Haze,
922    DustWhirls,
923    Squalls,
924    FunnelCloud,
925    Sandstorm,
926    Duststorm,
927    IceCrystals,
928    Spray,
929    NilSignificantWeather,
930}
931
932impl FromStr for WeatherPhenomena {
933    type Err = Error;
934
935    fn from_str(s: &str) -> Result<Self, Self::Err> {
936        match s {
937            "DZ" => Ok(WeatherPhenomena::Drizzle),
938            "RA" => Ok(WeatherPhenomena::Rain),
939            "SN" => Ok(WeatherPhenomena::Snow),
940            "SG" => Ok(WeatherPhenomena::SnowGrains),
941            "PL" => Ok(WeatherPhenomena::IcePellets),
942            "GR" => Ok(WeatherPhenomena::Hail),
943            "GS" => Ok(WeatherPhenomena::SnowPellets),
944            "UP" => Ok(WeatherPhenomena::UnknownPrecipitation),
945            "BR" => Ok(WeatherPhenomena::Mist),
946            "FG" => Ok(WeatherPhenomena::Fog),
947            "FU" => Ok(WeatherPhenomena::Smoke),
948            "VA" => Ok(WeatherPhenomena::VolcanicAsh),
949            "DU" => Ok(WeatherPhenomena::Dust),
950            "SA" => Ok(WeatherPhenomena::Sand),
951            "HZ" => Ok(WeatherPhenomena::Haze),
952            "PO" => Ok(WeatherPhenomena::DustWhirls),
953            "SQ" => Ok(WeatherPhenomena::Squalls),
954            "FC" => Ok(WeatherPhenomena::FunnelCloud),
955            "SS" => Ok(WeatherPhenomena::Sandstorm),
956            "DS" => Ok(WeatherPhenomena::Duststorm),
957            "IC" => Ok(WeatherPhenomena::IceCrystals),
958            "PY" => Ok(WeatherPhenomena::Spray),
959            "NSW" => Ok(WeatherPhenomena::NilSignificantWeather),
960            _ => Err(anyhow!("Invalid weather phenomena, given {}", s))
961        }
962    }
963}
964
965/// Weather condition.
966#[non_exhaustive]
967#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
968pub struct WeatherCondition {
969    pub intensity: WeatherIntensity,
970    /// Flag if the specified weather condition occurs only in the vicinity
971    /// of the aerodrome but not at/above the aerodrome.
972    pub is_in_vicinity: bool,
973    pub descriptors: Vec<WeatherDescriptor>,
974    pub phenomena: Vec<WeatherPhenomena>,
975}
976
977fn handle_weather(weather_re: &Regex, text: &str) -> Option<(WeatherCondition, usize)> {
978    weather_re.captures(text)
979        .map(|capture| {
980            let intensity = capture.name("intensity")
981                .map(|c| WeatherIntensity::from_str(c.as_str()).unwrap())
982                .unwrap_or(WeatherIntensity::Moderate);
983
984            let groups = if &capture["code"] == "NSW" {
985                vec!["NSW".to_string()]
986            } else {
987                capture["code"].chars()
988                    .collect::<Vec<_>>()
989                    .chunks(2)
990                    .map(String::from_iter)
991                    .collect::<Vec<_>>()
992            };
993
994            let mut is_in_vicinity = false;
995            let mut descriptors = Vec::new();
996            let mut phenomena = Vec::new();
997
998            for group in groups.iter() {
999                if group == "VC" {
1000                    is_in_vicinity = true;
1001                } else if let Ok(wd) = WeatherDescriptor::from_str(group) {
1002                    descriptors.push(wd);
1003                } else if let Ok(wp) = WeatherPhenomena::from_str(group) {
1004                    phenomena.push(wp);
1005                }
1006            }
1007
1008            let end = capture.name("end").unwrap().end();
1009
1010            let weather = WeatherCondition { intensity, is_in_vicinity, descriptors, phenomena };
1011
1012            (weather, end)
1013        })
1014}
1015
1016fn handle_present_weather(text: &str) -> Option<(WeatherCondition, usize)> {
1017    handle_weather(&PRESENT_WEATHER_RE, text)
1018}
1019
1020fn handle_recent_weather(text: &str) -> Option<(WeatherCondition, usize)> {
1021    handle_weather(&RECENT_WEATHER_RE, text)
1022}
1023
1024/// Cloud cover.
1025///
1026/// JSON representation is in lowercase snake case.
1027#[non_exhaustive]
1028#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1029#[serde(rename_all = "snake_case")]
1030pub enum CloudCover {
1031    Clear,
1032    SkyClear,
1033    NilSignificantCloud,
1034    NoCloudDetected,
1035    Few,
1036    Scattered,
1037    Broken,
1038    Overcast,
1039    /// Obscured sky but vertical visibility is available.
1040    VerticalVisibility,
1041    /// No cloud of operational significance in CAVOK conditions.
1042    CeilingOk,
1043}
1044
1045impl FromStr for CloudCover {
1046    type Err = Error;
1047
1048    fn from_str(s: &str) -> Result<Self, Self::Err> {
1049        match s {
1050            "CLR" => Ok(CloudCover::Clear),
1051            "SKC" => Ok(CloudCover::SkyClear),
1052            "NSC" => Ok(CloudCover::NilSignificantCloud),
1053            "NCD" => Ok(CloudCover::NoCloudDetected),
1054            "FEW" => Ok(CloudCover::Few),
1055            "SCT" => Ok(CloudCover::Scattered),
1056            "BKN" => Ok(CloudCover::Broken),
1057            "OVC" => Ok(CloudCover::Overcast),
1058            "VV" => Ok(CloudCover::VerticalVisibility),
1059            _ => Err(anyhow!("Invalid cloud cover, given {}", s))
1060        }
1061    }
1062}
1063
1064/// Cloud type.
1065///
1066/// JSON representation is in lowercase snake case.
1067#[non_exhaustive]
1068#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1069#[serde(rename_all = "snake_case")]
1070pub enum CloudType {
1071    Altocumulus,
1072    AltocumulusCastellanus,
1073    AltocumulusLenticularis,
1074    Altostratus,
1075    Cumulonimbus,
1076    CumulonimbusMammatus,
1077    Cirrocumulus,
1078    CirrocumulusLenticularis,
1079    Cirrus,
1080    Cirrostratus,
1081    Cumulus,
1082    Nimbostratus,
1083    Stratocumulus,
1084    StratocumulusLenticularis,
1085    Stratus,
1086    ToweringCumulus,
1087}
1088
1089impl FromStr for CloudType {
1090    type Err = Error;
1091
1092    fn from_str(s: &str) -> Result<Self, Self::Err> {
1093        match s {
1094            "AC" => Ok(CloudType::Altocumulus),
1095            "ACC" => Ok(CloudType::AltocumulusCastellanus),
1096            "ACSL" => Ok(CloudType::AltocumulusLenticularis),
1097            "AS" => Ok(CloudType::Altostratus),
1098            "CB" => Ok(CloudType::Cumulonimbus),
1099            "CBMAM" => Ok(CloudType::CumulonimbusMammatus),
1100            "CC" => Ok(CloudType::Cirrocumulus),
1101            "CCSL" => Ok(CloudType::CirrocumulusLenticularis),
1102            "CI" => Ok(CloudType::Cirrus),
1103            "CS" => Ok(CloudType::Cirrostratus),
1104            "CU" => Ok(CloudType::Cumulus),
1105            "NS" => Ok(CloudType::Nimbostratus),
1106            "SC" => Ok(CloudType::Stratocumulus),
1107            "SCSL" => Ok(CloudType::StratocumulusLenticularis),
1108            "ST" => Ok(CloudType::Stratus),
1109            "TCU" | "TU" => Ok(CloudType::ToweringCumulus),
1110            _ => Err(anyhow!("Invalid cloud type, given {s}"))
1111        }
1112    }
1113}
1114
1115/// Cloud layer.
1116#[non_exhaustive]
1117#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1118pub struct CloudLayer {
1119    pub cover: Option<CloudCover>,
1120    /// Height above the ground level (AGL).
1121    pub height: Option<Quantity>,
1122    pub cloud_type: Option<CloudType>,
1123}
1124
1125impl CloudLayer {
1126    fn is_empty(&self) -> bool {
1127        self.cover.is_none() && self.height.is_none() && self.cloud_type.is_none()
1128    }
1129}
1130
1131fn handle_cloud_layer(text: &str) -> Option<(CloudLayer, usize)> {
1132    CLOUD_RE.captures(text)
1133        .map(|capture| {
1134            let cover = match &capture["cover"] {
1135                "///" => None,
1136                s => Some(CloudCover::from_str(s).unwrap()),
1137            };
1138
1139            let height_value = capture.name("height").and_then(|c| match c.as_str() {
1140                "///" => None,
1141                s => Some(Value::from_str(s).unwrap() * 100.0),
1142            });
1143
1144            let cloud_type = capture.name("cloud").and_then(|c| match c.as_str() {
1145                "///" => None,
1146                s => Some(CloudType::from_str(s).unwrap()),
1147            });
1148
1149            let height = Quantity::new_opt(height_value, Unit::Foot);
1150
1151            let end = capture.name("end").unwrap().end();
1152
1153            let cloud_layer = CloudLayer { cover, height, cloud_type };
1154
1155            (cloud_layer, end)
1156        })
1157}
1158
1159/// Temperature groups.
1160#[non_exhaustive]
1161#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
1162pub struct Temperature {
1163    pub temperature: Option<Quantity>,
1164    pub dew_point: Option<Quantity>,
1165}
1166
1167impl Temperature {
1168    fn is_empty(&self) -> bool {
1169        self.temperature.is_none() && self.dew_point.is_none()
1170    }
1171}
1172
1173fn handle_temperature(text: &str) -> Option<(Temperature, usize)> {
1174    TEMPERATURE_RE.captures(text)
1175        .map(|capture| {
1176            let temperature_value = match &capture["temperature"] {
1177                "//" | "XX" => None,
1178                s => Some(Value::from_str(&s.replace('M', "-")).unwrap()),
1179            };
1180
1181            let dew_point_value = capture.name("dew_point").and_then(|c| match c.as_str() {
1182                "//" | "XX" => None,
1183                s => Some(Value::from_str(&s.replace('M', "-")).unwrap()),
1184            });
1185
1186            let temperature = Quantity::new_opt(temperature_value, Unit::DegreeCelsius);
1187            let dew_point = Quantity::new_opt(dew_point_value, Unit::DegreeCelsius);
1188
1189            let end = capture.name("end").unwrap().end();
1190
1191            let temperature = Temperature { temperature, dew_point };
1192
1193            (temperature, end)
1194        })
1195}
1196
1197/// Pressure group.
1198#[non_exhaustive]
1199#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
1200pub struct Pressure {
1201    pub pressure: Option<Quantity>,
1202}
1203
1204impl Pressure {
1205    fn is_empty(&self) -> bool {
1206        self.pressure.is_none()
1207    }
1208}
1209
1210fn handle_pressure(text: &str) -> Option<(Pressure, usize)> {
1211    PRESSURE_RE.captures(text)
1212        .map(|capture| {
1213            let mut pressure_value = match &capture["pressure"] {
1214                "////" => None,
1215                s => Some(Value::from_str(s).unwrap()),
1216            };
1217
1218            let units = Unit::from_str(&capture["units"]).unwrap();
1219
1220            if units == Unit::InchOfMercury {
1221                pressure_value = pressure_value.map(|p| p / 100.0)
1222            }
1223
1224            let pressure = Quantity::new_opt(pressure_value, units);
1225
1226            let end = capture.name("end").unwrap().end();
1227
1228            let pressure = Pressure { pressure };
1229
1230            (pressure, end)
1231        })
1232}
1233
1234/// Wind shear group.
1235#[non_exhaustive]
1236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1237pub struct WindShear {
1238    pub runway: String,
1239}
1240
1241fn handle_wind_shear(text: &str) -> Option<(WindShear, usize)> {
1242    WIND_SHEAR_RE.captures(text)
1243        .map(|capture| {
1244            let runway = match &capture["runway"] {
1245                "ALL RWY" => "all".to_string(),
1246                s => s[1..].to_string(),
1247            };
1248
1249            let end = capture.name("end").unwrap().end();
1250
1251            let ws = WindShear { runway };
1252
1253            (ws, end)
1254        })
1255}
1256
1257/// Sea state from WMO Code Table 3700.
1258///
1259/// JSON representation is in lowercase snake case.
1260#[non_exhaustive]
1261#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1262#[serde(rename_all = "snake_case")]
1263pub enum SeaState {
1264    Glassy,
1265    Rippled,
1266    Smooth,
1267    Slight,
1268    Moderate,
1269    Rough,
1270    VeryRough,
1271    High,
1272    VeryHigh,
1273    Phenomenal,
1274}
1275
1276impl FromStr for SeaState {
1277    type Err = Error;
1278
1279    fn from_str(s: &str) -> Result<Self, Self::Err> {
1280        match s {
1281            "0" => Ok(SeaState::Glassy),
1282            "1" => Ok(SeaState::Rippled),
1283            "2" => Ok(SeaState::Smooth),
1284            "3" => Ok(SeaState::Slight),
1285            "4" => Ok(SeaState::Moderate),
1286            "5" => Ok(SeaState::Rough),
1287            "6" => Ok(SeaState::VeryRough),
1288            "7" => Ok(SeaState::High),
1289            "8" => Ok(SeaState::VeryHigh),
1290            "9" => Ok(SeaState::Phenomenal),
1291            _ => Err(anyhow!("Invalid sea state, given {}", s))
1292        }
1293    }
1294}
1295
1296/// Sea groups.
1297#[non_exhaustive]
1298#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
1299pub struct Sea {
1300    pub sea_temperature: Option<Quantity>,
1301    pub sea_state: Option<SeaState>,
1302    pub wave_height: Option<Quantity>,
1303}
1304
1305impl Sea {
1306    fn is_empty(&self) -> bool {
1307        self.sea_temperature.is_none() && self.sea_state.is_none() && self.wave_height.is_none()
1308    }
1309}
1310
1311fn handle_sea(text: &str) -> Option<(Sea, usize)> {
1312    SEA_RE.captures(text)
1313        .map(|capture| {
1314            let temperature_value = match &capture["temperature"] {
1315                "//" | "XX" => None,
1316                s => Some(Value::from_str(&s.replace('M', "-")).unwrap()),
1317            };
1318
1319            let sea_state = capture.name("state").and_then(|c| match c.as_str() {
1320                "/" => None,
1321                s => Some(SeaState::from_str(s).unwrap()),
1322            });
1323
1324            let height_value = capture.name("height").and_then(|c| match c.as_str() {
1325                "///" => None,
1326                s => Some(Value::from_str(s).unwrap() / 10.0),
1327            });
1328
1329            let sea_temperature = Quantity::new_opt(temperature_value, Unit::DegreeCelsius);
1330            let wave_height = Quantity::new_opt(height_value, Unit::Metre);
1331
1332            let end = capture.name("end").unwrap().end();
1333
1334            let sea = Sea { sea_temperature, sea_state, wave_height };
1335
1336            (sea, end)
1337        })
1338}
1339
1340fn handle_color(text: &str) -> Option<usize> {
1341    COLOR_RE.captures(text)
1342        .map(|capture| {
1343            capture.name("end").unwrap().end()
1344        })
1345}
1346
1347fn handle_rainfall(text: &str) -> Option<usize> {
1348    RAINFALL_RE.captures(text)
1349        .map(|capture| {
1350            capture.name("end").unwrap().end()
1351        })
1352}
1353
1354fn handle_runway_state(text: &str) -> Option<usize> {
1355    RUNWAY_STATE_RE.captures(text)
1356        .map(|capture| {
1357            capture.name("end").unwrap().end()
1358        })
1359}
1360
1361#[non_exhaustive]
1362#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1363#[serde(rename_all = "snake_case")]
1364enum TrendTimeIndicator {
1365    From,
1366    Until,
1367    At,
1368}
1369
1370impl FromStr for TrendTimeIndicator {
1371    type Err = Error;
1372
1373    fn from_str(s: &str) -> Result<Self, Self::Err> {
1374        match s {
1375            "FM" => Ok(TrendTimeIndicator::From),
1376            "TL" => Ok(TrendTimeIndicator::Until),
1377            "AT" => Ok(TrendTimeIndicator::At),
1378            _ => Err(anyhow!("Invalid trend time indicator, given {}", s))
1379        }
1380    }
1381}
1382
1383#[non_exhaustive]
1384#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1385struct TrendTime {
1386    indicator: TrendTimeIndicator,
1387    time: Option<MetarTime>,
1388}
1389
1390fn handle_trend_time(text: &str, anchor_time: Option<NaiveDateTime>) -> Option<(TrendTime, usize)> {
1391    TREND_TIME_RE.captures(text)
1392        .map(|capture| {
1393            let indicator = TrendTimeIndicator::from_str(&capture["indicator"]).unwrap();
1394            let mut hour = capture["hour"].parse().unwrap();
1395            let minute = capture["minute"].parse().unwrap();
1396
1397            if hour == 24 {
1398                hour = 0;
1399            }
1400
1401            let naive_time = NaiveTime::from_hms_opt(hour, minute, 0);
1402            let mut time = naive_time.map(|nt| MetarTime::Time(UtcTime(nt)));
1403
1404            if let Some(at) = anchor_time {
1405                time = time.map(|t| t.to_date_time(at));
1406            }
1407
1408            let end = capture.name("end").unwrap().end();
1409
1410            let trend_time = TrendTime { indicator, time };
1411
1412            (trend_time, end)
1413        })
1414}
1415
1416/// Significant changes in the meteorological conditions in the TREND forecast.
1417///
1418/// Only elements for which a significant change is expected are [Option::Some].
1419#[non_exhaustive]
1420#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1421pub struct TrendChange {
1422    pub indicator: Trend,
1423    pub from_time: Option<MetarTime>,
1424    pub to_time: Option<MetarTime>,
1425    pub at_time: Option<MetarTime>,
1426    /// Surface wind groups.
1427    ///
1428    /// JSON representation is flattened once.
1429    #[serde(flatten)]
1430    pub wind: Wind,
1431    /// Visibility groups.
1432    ///
1433    /// JSON representation is flattened once.
1434    #[serde(flatten)]
1435    pub visibility: Visibility,
1436    pub weather: Vec<WeatherCondition>,
1437    pub clouds: Vec<CloudLayer>,
1438}
1439
1440/// Decoded METAR report.
1441#[non_exhaustive]
1442#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1443pub struct Metar {
1444    /// Identification groups.
1445    ///
1446    /// JSON representation is flattened once.
1447    #[serde(flatten)]
1448    pub header: Header,
1449    /// Surface wind groups.
1450    ///
1451    /// JSON representation is flattened once.
1452    #[serde(flatten)]
1453    pub wind: Wind,
1454    /// Visibility groups.
1455    ///
1456    /// JSON representation is flattened once.
1457    #[serde(flatten)]
1458    pub visibility: Visibility,
1459    pub runway_visual_ranges: Vec<RunwayVisualRange>,
1460    pub present_weather: Vec<WeatherCondition>,
1461    pub clouds: Vec<CloudLayer>,
1462    /// Temperature groups.
1463    ///
1464    /// JSON representation is flattened once.
1465    #[serde(flatten)]
1466    pub temperature: Temperature,
1467    /// Pressure group.
1468    ///
1469    /// JSON representation is flattened once.
1470    #[serde(flatten)]
1471    pub pressure: Pressure,
1472    pub recent_weather: Vec<WeatherCondition>,
1473    pub wind_shears: Vec<WindShear>,
1474    /// Sea groups.
1475    ///
1476    /// JSON representation is flattened once.
1477    #[serde(flatten)]
1478    pub sea: Sea,
1479    pub trend_changes: Vec<TrendChange>,
1480    pub report: String,
1481}
1482
1483/// Decodes a METAR report into a [Metar] struct.
1484///
1485/// # Arguments
1486///
1487/// * `report` - METAR report to decode.
1488/// * `anchor_time` - Specifies a datetime that is ideally close to that one when the report was actually published.
1489///                   If given, the decoded METAR day and time will be converted to a full datetime. See also [MetarTime::to_date_time()].
1490pub fn decode_metar(report: &str, anchor_time: Option<NaiveDateTime>) -> Result<Metar> {
1491    let mut sanitized = report.to_uppercase().trim().replace('\x00', "");
1492    sanitized = WHITESPACE_REPLACE_RE.replace_all(&sanitized, *WHITESPACE_REPLACE_OUT).to_string();
1493    let report = END_REPLACE_RE.replace_all(&sanitized, *END_REPLACE_OUT).to_string();
1494
1495    let mut section = Section::Main;
1496
1497    let mut metar = Metar::default();
1498    metar.report = report.trim().to_string();
1499
1500    let mut processing_trend_change = false;
1501    let mut trend_change = TrendChange::default();
1502
1503    let mut unparsed_groups = Vec::new();
1504
1505    // Handlers return mostly `Option<(some struct, end index)>` which gives us:
1506    // - None => the handler didn't parse the group which often leads to trying an another handler
1507    // - Some(some struct, end index) => the handler parsed the group (to some struct) and also returned an index of the group end
1508    //                                   which enables to further slice the report for other handlers to work with
1509    //
1510    // In certain cases, some struct may be empty (determined by `.is_empty()`) because all of its fields are missing.
1511    // For example, this typically happens when clouds are unknown (//////) and such empty struct will be skipped.
1512
1513    let mut idx = 0;
1514
1515    while idx < report.len() {
1516        let sub_report = &report[idx..];
1517
1518        if let Some((sec, relative_end)) = handle_section(sub_report) {
1519            section = sec;
1520            idx += relative_end;
1521
1522            if processing_trend_change {
1523                metar.trend_changes.push(trend_change.clone());
1524                processing_trend_change = false;
1525                trend_change = TrendChange::default();
1526            }
1527
1528            if let Section::Trend(trend) = section {
1529                processing_trend_change = true;
1530                trend_change.indicator = trend;
1531            }
1532
1533            continue;
1534        }
1535
1536        match section {
1537            Section::Main => {
1538                if metar.header.is_empty() {
1539                    if let Some((header, relative_end)) = handle_header(sub_report, anchor_time) {
1540                        metar.header = header;
1541                        idx += relative_end;
1542                        continue;
1543                    }
1544                }
1545
1546                if metar.wind.is_empty() {
1547                    if let Some((wind, relative_end)) = handle_wind(sub_report) {
1548                        metar.wind = wind;
1549                        idx += relative_end;
1550                        continue;
1551                    }
1552                }
1553
1554                if metar.visibility.is_empty() {
1555                    if let Some((visibility, is_cavok, relative_end)) = handle_visibility(sub_report) {
1556                        metar.visibility = visibility;
1557
1558                        if is_cavok {
1559                            let cloud_layer = CloudLayer { cover: Some(CloudCover::CeilingOk) , height: None, cloud_type: None };
1560                            metar.clouds.push(cloud_layer);
1561                        }
1562
1563                        idx += relative_end;
1564                        continue;
1565                    }
1566                }
1567
1568                if let Some((weather_condition, relative_end)) = handle_present_weather(sub_report) {
1569                    metar.present_weather.push(weather_condition);
1570                    idx += relative_end;
1571                    continue;
1572                }
1573
1574                if let Some((runway_visual_range, relative_end)) = handle_runway_visual_range(sub_report) {
1575                    metar.runway_visual_ranges.push(runway_visual_range);
1576                    idx += relative_end;
1577                    continue;
1578                }
1579
1580                if let Some((cloud_layer, relative_end)) = handle_cloud_layer(sub_report) {
1581                    if !cloud_layer.is_empty() {
1582                        metar.clouds.push(cloud_layer);
1583                    }
1584
1585                    idx += relative_end;
1586                    continue;
1587                }
1588
1589                if metar.temperature.is_empty() {
1590                    if let Some((temperature, relative_end)) = handle_temperature(sub_report) {
1591                        if !temperature.is_empty() {
1592                            metar.temperature = temperature;
1593                        }
1594
1595                        idx += relative_end;
1596                        continue;
1597                    }
1598                }
1599
1600                if metar.pressure.is_empty() {
1601                    if let Some((pressure, relative_end)) = handle_pressure(sub_report) {
1602                        metar.pressure = pressure;
1603                        idx += relative_end;
1604                        continue;
1605                    }
1606                }
1607
1608                if let Some((weather_condition, relative_end)) = handle_recent_weather(sub_report) {
1609                    metar.recent_weather.push(weather_condition);
1610                    idx += relative_end;
1611                    continue;
1612                }
1613
1614                if let Some((wind_shear, relative_end)) = handle_wind_shear(sub_report) {
1615                    metar.wind_shears.push(wind_shear);
1616                    idx += relative_end;
1617                    continue;
1618                }
1619
1620                if metar.sea.is_empty() {
1621                    if let Some((sea, relative_end)) = handle_sea(sub_report) {
1622                        if !sea.is_empty() {
1623                            metar.sea = sea;
1624                        }
1625
1626                        idx += relative_end;
1627                        continue;
1628                    }
1629                }
1630
1631                // Colour state, won't store. For more info check:
1632                // <https://en.wikipedia.org/wiki/Colour_state>
1633                if let Some(relative_end) = handle_color(sub_report) {
1634                    idx += relative_end;
1635                    continue;
1636                }
1637
1638                // Rainfall in last 10min / since 0900 local time, won't store. For more info check:
1639                // <http://www.bom.gov.au/aviation/Aerodrome/metar-speci.pdf>
1640                if let Some(relative_end) = handle_rainfall(sub_report) {
1641                    idx += relative_end;
1642                    continue;
1643                }
1644
1645                // Runway state (should be part of SNOWTAM), won't store. For more info check:
1646                // <https://www.icao.int/WACAF/Documents/Meetings/2021/GRF/2.%20Provisions%20on%20GRF.pdf>
1647                if let Some(relative_end) = handle_runway_state(sub_report) {
1648                    idx += relative_end;
1649                    continue;
1650                }
1651            },
1652            Section::Trend(_) => {
1653                if let Some((trend_time, relative_end)) = handle_trend_time(sub_report, anchor_time) {
1654                    match trend_time.indicator {
1655                        TrendTimeIndicator::From => {
1656                            trend_change.from_time = trend_time.time;
1657                        },
1658                        TrendTimeIndicator::Until => {
1659                            trend_change.to_time = trend_time.time;
1660                        },
1661                        TrendTimeIndicator::At => {
1662                            trend_change.at_time = trend_time.time;
1663                        },
1664                    }
1665
1666                    idx += relative_end;
1667                    continue;
1668                }
1669
1670                if trend_change.wind.is_empty() {
1671                    if let Some((wind, relative_end)) = handle_wind(sub_report) {
1672                        trend_change.wind = wind;
1673                        idx += relative_end;
1674                        continue;
1675                    }
1676                }
1677
1678                if trend_change.visibility.is_empty() {
1679                    if let Some((visibility, is_cavok, relative_end)) = handle_visibility(sub_report) {
1680                        trend_change.visibility = visibility;
1681
1682                        if is_cavok {
1683                            let cloud_layer = CloudLayer { cover: Some(CloudCover::CeilingOk) , height: None, cloud_type: None };
1684                            trend_change.clouds.push(cloud_layer);
1685                        }
1686
1687                        idx += relative_end;
1688                        continue;
1689                    }
1690                }
1691
1692                if let Some((weather_condition, relative_end)) = handle_present_weather(sub_report) {
1693                    trend_change.weather.push(weather_condition);
1694                    idx += relative_end;
1695                    continue;
1696                }
1697
1698                if let Some((cloud_layer, relative_end)) = handle_cloud_layer(sub_report) {
1699                    if !cloud_layer.is_empty() {
1700                        trend_change.clouds.push(cloud_layer);
1701                    }
1702
1703                    idx += relative_end;
1704                    continue;
1705                }
1706            },
1707            Section::Remark => (), // TODO: https://github.com/meandair/rweather-decoder/issues/15
1708        }
1709
1710        let relative_end = sub_report.find(' ').unwrap();
1711
1712        let unparsed = &report[idx..idx + relative_end];
1713        if unparsed.chars().any(|c| c != '/') {
1714            unparsed_groups.push(unparsed);
1715        }
1716
1717        idx += relative_end + 1;
1718    }
1719
1720    if processing_trend_change {
1721        metar.trend_changes.push(trend_change);
1722    }
1723
1724    if !unparsed_groups.is_empty() {
1725        log::debug!("Unparsed data: {}, report: {}", unparsed_groups.join(" "), report);
1726    }
1727
1728    Ok(metar)
1729}