#![allow(clippy::expect_used, clippy::panic)]
use super::*;
fn cat(ceiling: Option<f64>, vis: Option<f64>) -> Option<FlightCategory> {
FlightCategory::from_ceiling_visibility(ceiling, vis)
}
#[test]
fn category_is_the_worse_of_ceiling_and_visibility() {
use FlightCategory::{Ifr, Lifr, Mvfr, Vfr};
assert_eq!(cat(Some(5000.0), Some(10.0)), Some(Vfr));
assert_eq!(cat(None, Some(10.0)), Some(Vfr), "no ceiling = unlimited");
assert_eq!(cat(Some(12000.0), Some(4.0)), Some(Mvfr));
assert_eq!(cat(Some(900.0), Some(10.0)), Some(Ifr));
assert_eq!(cat(Some(300.0), Some(2.0)), Some(Lifr));
assert_eq!(cat(Some(800.0), Some(0.5)), Some(Lifr), "vis < 1 = LIFR");
}
#[test]
fn ceiling_category_boundaries_are_inclusive_per_faa() {
use FlightCategory::{Ifr, Lifr, Mvfr, Vfr};
let vfr_vis = Some(10.0); assert_eq!(cat(Some(499.0), vfr_vis), Some(Lifr));
assert_eq!(cat(Some(500.0), vfr_vis), Some(Ifr), "500 is IFR");
assert_eq!(cat(Some(999.0), vfr_vis), Some(Ifr));
assert_eq!(cat(Some(1000.0), vfr_vis), Some(Mvfr), "1000 is MVFR");
assert_eq!(cat(Some(3000.0), vfr_vis), Some(Mvfr), "3000 is MVFR");
assert_eq!(cat(Some(3001.0), vfr_vis), Some(Vfr), ">3000 is VFR");
}
#[test]
fn visibility_category_boundaries_are_inclusive_per_faa() {
use FlightCategory::{Ifr, Lifr, Mvfr, Vfr};
let no_ceiling = None; assert_eq!(cat(no_ceiling, Some(0.5)), Some(Lifr));
assert_eq!(cat(no_ceiling, Some(1.0)), Some(Ifr), "1 sm is IFR");
assert_eq!(cat(no_ceiling, Some(2.9)), Some(Ifr));
assert_eq!(cat(no_ceiling, Some(3.0)), Some(Mvfr), "3 sm is MVFR");
assert_eq!(cat(no_ceiling, Some(5.0)), Some(Mvfr), "5 sm is MVFR");
assert_eq!(cat(no_ceiling, Some(5.1)), Some(Vfr), ">5 sm is VFR");
}
#[test]
fn missing_visibility_is_uncategorizable() {
assert_eq!(cat(Some(5000.0), None), None);
assert_eq!(cat(None, None), None);
assert_eq!(cat(None, Some(f64::NAN)), None);
}
#[test]
fn ceiling_is_the_lowest_broken_overcast_or_obscured_layer() {
let obs = MetarObservation::new().with_clouds(vec![
CloudLayer::new(CloudCover::Few, Some(2000.0)),
CloudLayer::new(CloudCover::Scattered, Some(4000.0)),
CloudLayer::new(CloudCover::Broken, Some(6000.0)),
CloudLayer::new(CloudCover::Overcast, Some(9000.0)),
]);
assert_eq!(obs.ceiling_ft(), Some(6000.0));
let clear = MetarObservation::new().with_clouds(vec![
CloudLayer::new(CloudCover::Few, Some(25000.0)),
CloudLayer::new(CloudCover::Scattered, Some(30000.0)),
]);
assert_eq!(clear.ceiling_ft(), None, "no BKN/OVC = no ceiling");
let vv = MetarObservation::new()
.with_clouds(vec![CloudLayer::new(CloudCover::Obscured, Some(200.0))]);
assert_eq!(
vv.ceiling_ft(),
Some(200.0),
"vertical visibility is a ceiling"
);
}
#[test]
fn observation_category_matches_awc_reported_on_real_reports() {
let ord = MetarObservation::new()
.with_visibility_sm(Some(10.0))
.with_clouds(vec![
CloudLayer::new(CloudCover::Broken, Some(11000.0)),
CloudLayer::new(CloudCover::Overcast, Some(25000.0)),
])
.with_reported_category(FlightCategory::parse("VFR"));
assert_eq!(ord.flight_category(), Some(FlightCategory::Vfr));
assert_eq!(ord.flight_category(), ord.reported_category);
let ifr = MetarObservation::new()
.with_visibility_sm(Some(2.0))
.with_clouds(vec![CloudLayer::new(CloudCover::Overcast, Some(800.0))])
.with_reported_category(FlightCategory::parse("IFR"));
assert_eq!(ifr.flight_category(), Some(FlightCategory::Ifr));
assert_eq!(ifr.flight_category(), ifr.reported_category);
}
#[test]
fn visibility_parses_numbers_plus_and_fractions() {
assert_eq!(parse_visibility_sm("10+"), Some(10.0));
assert_eq!(parse_visibility_sm("3"), Some(3.0));
assert_eq!(parse_visibility_sm("6.0"), Some(6.0));
assert_eq!(parse_visibility_sm("1/2"), Some(0.5));
assert_eq!(parse_visibility_sm("1 1/2"), Some(1.5));
assert_eq!(parse_visibility_sm("M1/4"), Some(0.25), "M = less than");
assert_eq!(parse_visibility_sm("garbage"), None);
}
#[test]
fn altimeter_converts_hectopascals_to_inhg() {
let obs = MetarObservation::new().with_altimeter_hpa(Some(1013.25));
let inhg = obs.altimeter_inhg().expect("inhg");
assert!((inhg - 29.92).abs() < 0.01, "got {inhg}");
}
#[test]
fn category_ordering_puts_lifr_worst() {
use FlightCategory::{Ifr, Lifr, Mvfr, Vfr};
assert!(Lifr < Ifr && Ifr < Mvfr && Mvfr < Vfr);
assert_eq!(Lifr.min(Vfr), Lifr, "min selects the more restrictive");
}
#[test]
fn ceiling_layer_of_unknown_height_is_not_unlimited() {
let obs = MetarObservation::new()
.with_visibility_sm(Some(10.0))
.with_clouds(vec![CloudLayer::new(CloudCover::Broken, None)]);
assert!(obs.has_unknown_height_ceiling());
assert_eq!(obs.ceiling_ft(), None, "no known height");
assert_eq!(
obs.flight_category(),
None,
"unknown-height ceiling is uncategorizable, never VFR"
);
}
#[test]
fn unknown_height_ceiling_still_reports_lifr_when_visibility_forces_it() {
let obs = MetarObservation::new()
.with_visibility_sm(Some(0.5))
.with_clouds(vec![CloudLayer::new(CloudCover::Overcast, None)]);
assert_eq!(obs.flight_category(), Some(FlightCategory::Lifr));
}
#[test]
fn a_nan_cloud_base_cannot_mask_a_real_low_ceiling() {
let obs = MetarObservation::new().with_clouds(vec![
CloudLayer::new(CloudCover::Overcast, Some(f64::NAN)),
CloudLayer::new(CloudCover::Broken, Some(700.0)),
]);
assert_eq!(obs.ceiling_ft(), Some(700.0));
}
#[test]
fn negative_or_garbage_visibility_is_unknown_not_a_value() {
assert_eq!(parse_visibility_sm("-1"), None);
assert_eq!(parse_visibility_sm("-1/2"), None);
assert_eq!(
parse_visibility_sm("1/0"),
None,
"no division by zero value"
);
}