use std::{fmt, io::Write};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", content = "val"))]
pub enum Altitude {
Gnd,
FeetAmsl(i32),
FeetAgl(i32),
FlightLevel(u16),
Unlimited,
Other(String),
}
impl fmt::Display for Altitude {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Gnd => write!(f, "GND"),
Self::FeetAmsl(ft) => write!(f, "{ft} ft AMSL"),
Self::FeetAgl(ft) => write!(f, "{ft} ft AGL"),
Self::FlightLevel(ft) => write!(f, "FL{ft}"),
Self::Unlimited => write!(f, "Unlimited"),
Self::Other(val) => write!(f, "?({val})"),
}
}
}
fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
let prefix_len = prefix.len();
if s.is_char_boundary(prefix_len) && s[..prefix_len].eq_ignore_ascii_case(prefix) {
return Some(&s[prefix_len..]);
}
None
}
fn is_amsl_suffix(s: &str) -> bool {
s.is_empty() || s.eq_ignore_ascii_case("amsl") || s.eq_ignore_ascii_case("msl")
}
fn is_agl_suffix(s: &str) -> bool {
s.eq_ignore_ascii_case("agl") || s.eq_ignore_ascii_case("gnd") || s.eq_ignore_ascii_case("sfc")
}
impl Altitude {
pub fn write<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
match self {
Self::Gnd => write!(writer, "GND"),
Self::FeetAmsl(n) => write!(writer, "{n}ft AMSL"),
Self::FeetAgl(n) => write!(writer, "{n}ft AGL"),
Self::FlightLevel(n) => write!(writer, "FL{n}"),
Self::Unlimited => write!(writer, "UNLIM"),
Self::Other(s) => write!(writer, "{s}"),
}
}
fn m2ft(val: i32) -> Result<i32, &'static str> {
if val > 654_553_015 {
return Err("m2ft out of bounds (too large)");
} else if val < -654_553_016 {
return Err("m2ft out of bounds (too small)");
}
let m = f64::from(val);
let feet = m / 0.3048;
Ok(feet.round() as i32)
}
pub fn parse(data: &str) -> Result<Self, String> {
let eq = |a: &str, b| a.eq_ignore_ascii_case(b);
if eq(data, "gnd") || eq(data, "sfc") || data == "0" {
return Ok(Self::Gnd);
}
if eq(data, "unl") || eq(data, "unlim") || eq(data, "unltd") || eq(data, "unlimited") {
return Ok(Self::Unlimited);
}
if let Some(after_fl) = strip_prefix_ci(data, "fl") {
return match after_fl.trim().parse::<u16>() {
Ok(val) => Ok(Self::FlightLevel(val)),
Err(_) => Ok(Self::Other(data.to_string())),
};
}
let pos = data
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(data.len());
let (number, rest) = data.split_at(pos);
let Ok(mut val) = number.parse::<i32>() else {
return Ok(Self::Other(data.to_string()));
};
let rest = rest.trim();
if is_amsl_suffix(rest) {
return Ok(Self::FeetAmsl(val));
}
if is_agl_suffix(rest) {
return Ok(Self::FeetAgl(val));
}
let space_pos = rest.find(char::is_whitespace).unwrap_or(rest.len());
let (unit, reference) = rest.split_at(space_pos);
let reference = reference.trim();
if eq(unit, "m") {
val = Self::m2ft(val)?;
} else if !eq(unit, "ft") {
return Ok(Self::Other(data.to_string()));
}
if is_amsl_suffix(reference) {
return Ok(Self::FeetAmsl(val));
}
if is_agl_suffix(reference) {
return Ok(Self::FeetAgl(val));
}
Ok(Self::Other(data.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn m2ft() {
assert_eq!(Altitude::m2ft(0).unwrap(), 0);
assert_eq!(Altitude::m2ft(1).unwrap(), 3);
assert_eq!(Altitude::m2ft(2).unwrap(), 7);
assert_eq!(Altitude::m2ft(100).unwrap(), 328);
assert_eq!(Altitude::m2ft(654_553_015).unwrap(), 2_147_483_645);
assert_eq!(Altitude::m2ft(-654_553_016).unwrap(), -2_147_483_648);
assert!(Altitude::m2ft(654_553_016).is_err());
assert!(Altitude::m2ft(-654_553_017).is_err());
}
#[test]
fn parse_gnd() {
assert_eq!(Altitude::parse("gnd").unwrap(), Altitude::Gnd);
assert_eq!(Altitude::parse("Gnd").unwrap(), Altitude::Gnd);
assert_eq!(Altitude::parse("GND").unwrap(), Altitude::Gnd);
assert_eq!(Altitude::parse("sfc").unwrap(), Altitude::Gnd);
assert_eq!(Altitude::parse("Sfc").unwrap(), Altitude::Gnd);
assert_eq!(Altitude::parse("SFC").unwrap(), Altitude::Gnd);
}
#[test]
fn parse_amsl() {
assert_eq!(Altitude::parse("42 ft").unwrap(), Altitude::FeetAmsl(42));
assert_eq!(Altitude::parse("42 FT").unwrap(), Altitude::FeetAmsl(42));
assert_eq!(Altitude::parse("42ft").unwrap(), Altitude::FeetAmsl(42));
assert_eq!(Altitude::parse("42 ft").unwrap(), Altitude::FeetAmsl(42));
assert_eq!(
Altitude::parse("42 ft AMSL").unwrap(),
Altitude::FeetAmsl(42)
);
}
#[test]
fn parse_agl() {
assert_eq!(Altitude::parse("42 ft agl").unwrap(), Altitude::FeetAgl(42));
assert_eq!(Altitude::parse("42FT Agl").unwrap(), Altitude::FeetAgl(42));
assert_eq!(Altitude::parse("42 ft GND").unwrap(), Altitude::FeetAgl(42));
assert_eq!(Altitude::parse("42 GND").unwrap(), Altitude::FeetAgl(42));
assert_eq!(Altitude::parse("42SFC").unwrap(), Altitude::FeetAgl(42));
}
#[test]
fn parse_fl() {
assert_eq!(Altitude::parse("fl50").unwrap(), Altitude::FlightLevel(50));
assert_eq!(
Altitude::parse("FL 180").unwrap(),
Altitude::FlightLevel(180)
);
assert_eq!(
Altitude::parse("FL130").unwrap(),
Altitude::FlightLevel(130)
);
}
#[test]
fn parse_msl_amsl_equivalence() {
assert_eq!(
Altitude::parse("1000 MSL").unwrap(),
Altitude::FeetAmsl(1000)
);
assert_eq!(
Altitude::parse("1000 AMSL").unwrap(),
Altitude::FeetAmsl(1000)
);
assert_eq!(
Altitude::parse("1000msl").unwrap(),
Altitude::FeetAmsl(1000)
);
assert_eq!(
Altitude::parse("1000ft MSL").unwrap(),
Altitude::FeetAmsl(1000)
);
assert_eq!(
Altitude::parse("1000 ft AMSL").unwrap(),
Altitude::FeetAmsl(1000)
);
}
#[test]
fn parse_meters_amsl() {
assert_eq!(Altitude::parse("100m").unwrap(), Altitude::FeetAmsl(328));
assert_eq!(Altitude::parse("100 m").unwrap(), Altitude::FeetAmsl(328));
assert_eq!(
Altitude::parse("100m MSL").unwrap(),
Altitude::FeetAmsl(328)
);
assert_eq!(
Altitude::parse("100 m AMSL").unwrap(),
Altitude::FeetAmsl(328)
);
}
#[test]
fn parse_meters_agl() {
assert_eq!(Altitude::parse("100m agl").unwrap(), Altitude::FeetAgl(328));
assert_eq!(
Altitude::parse("100 m AGL").unwrap(),
Altitude::FeetAgl(328)
);
assert_eq!(Altitude::parse("100m gnd").unwrap(), Altitude::FeetAgl(328));
assert_eq!(
Altitude::parse("100 m SFC").unwrap(),
Altitude::FeetAgl(328)
);
}
#[test]
fn parse_whitespace_variations() {
assert_eq!(Altitude::parse("1000ft").unwrap(), Altitude::FeetAmsl(1000));
assert_eq!(Altitude::parse("100m").unwrap(), Altitude::FeetAmsl(328));
assert_eq!(
Altitude::parse("1000 ft").unwrap(),
Altitude::FeetAmsl(1000)
);
assert_eq!(Altitude::parse("100 m").unwrap(), Altitude::FeetAmsl(328));
assert_eq!(
Altitude::parse("1000 ft").unwrap(),
Altitude::FeetAmsl(1000)
);
assert_eq!(
Altitude::parse("1000 ft AMSL").unwrap(),
Altitude::FeetAmsl(1000)
);
assert_eq!(
Altitude::parse("1000ft MSL ").unwrap(),
Altitude::FeetAmsl(1000)
);
}
#[test]
fn parse_case_variations() {
assert_eq!(Altitude::parse("1000FT").unwrap(), Altitude::FeetAmsl(1000));
assert_eq!(Altitude::parse("1000Ft").unwrap(), Altitude::FeetAmsl(1000));
assert_eq!(Altitude::parse("1000fT").unwrap(), Altitude::FeetAmsl(1000));
assert_eq!(Altitude::parse("100M").unwrap(), Altitude::FeetAmsl(328));
assert_eq!(
Altitude::parse("1000 Msl").unwrap(),
Altitude::FeetAmsl(1000)
);
assert_eq!(
Altitude::parse("1000 aMsL").unwrap(),
Altitude::FeetAmsl(1000)
);
assert_eq!(
Altitude::parse("1000 AgL").unwrap(),
Altitude::FeetAgl(1000)
);
}
#[test]
fn parse_unparseable() {
assert_eq!(
Altitude::parse("something random").unwrap(),
Altitude::Other("something random".to_string())
);
assert_eq!(
Altitude::parse("1000xyz").unwrap(),
Altitude::Other("1000xyz".to_string())
);
}
fn write_altitude(altitude: &Altitude) -> String {
let mut buf = Vec::new();
altitude.write(&mut buf).unwrap();
String::from_utf8(buf).unwrap()
}
#[test]
fn write() {
assert_eq!(write_altitude(&Altitude::Gnd), "GND");
assert_eq!(write_altitude(&Altitude::FeetAmsl(5000)), "5000ft AMSL");
assert_eq!(write_altitude(&Altitude::FeetAmsl(-200)), "-200ft AMSL");
assert_eq!(write_altitude(&Altitude::FeetAgl(1500)), "1500ft AGL");
assert_eq!(write_altitude(&Altitude::FlightLevel(195)), "FL195");
assert_eq!(write_altitude(&Altitude::Unlimited), "UNLIM");
assert_eq!(
write_altitude(&Altitude::Other("custom".to_string())),
"custom"
);
}
}