#![deny(missing_docs)]
extern crate regex;
use regex::Regex;
use std::num::ParseIntError;
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Time {
pub date: u8,
pub hour: u8,
pub minute: u8,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum WindSpeed {
Knot(u32),
MetresPerSecond(u32),
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum WindDirection {
Heading(u32),
Variable,
Above,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum Visibility {
LessThanMetres(u32),
LessThanStatuteMiles(u32),
Metres(u32),
StatuteMiles(u32),
CavOK,
NoSignificantClouds,
SkyClear,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum Pressure {
Hectopascals(u32),
InchesMercury(u32),
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum CloudLayer {
Few(CloudType, u32),
Scattered(CloudType, u32),
Broken(CloudType, u32),
Overcast(CloudType, u32),
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum CloudType {
Normal,
Cumulonimbus,
ToweringCumulus,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Wind {
pub dir: WindDirection,
pub speed: WindSpeed,
pub varying: Option<(u32, u32)>,
pub gusting: Option<WindSpeed>,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Metar {
pub station: String,
pub time: Time,
pub wind: Wind,
pub visibility: Visibility,
pub cloud_layers: Vec<CloudLayer>,
pub vert_visibility: Option<u32>,
pub temperature: i32,
pub dewpoint: i32,
pub pressure: Pressure,
pub remarks: Option<String>,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum MetarError {
TimeParseError(ParseIntError),
WindDirectionError(ParseIntError),
WindSpeedError(ParseIntError),
WindGustError(ParseIntError),
WindVaryingError(ParseIntError),
VisibilityError(ParseIntError),
CloudFloorError(ParseIntError),
VerticalVisibilityError(ParseIntError),
AirPressureError(ParseIntError),
TemperatureError(ParseIntError),
DewpointError(ParseIntError),
InvalidMetarError(String),
}
impl Metar {
pub fn parse(data: String) -> Result<Self, MetarError> {
let mut time = Time {
date: 0,
hour: 0,
minute: 0,
};
let mut wind = Wind {
dir: WindDirection::Heading(0),
speed: WindSpeed::Knot(0),
varying: None,
gusting: None,
};
let mut visibility = Visibility::Metres(10000);
let mut cloud_layers = Vec::new();
let mut vert_visibility = None;
let mut temperature = 0;
let mut dewpoint = 0;
let mut pressure = Pressure::Hectopascals(0);
let mut remarks = None;
let re = Regex::new(r"(?P<station>[A-Z0-9]{4}) (?P<time>[0-9]{6}Z) (?P<data>NIL|(?:AUTO )?(?P<wind_dir>[0-9]{3}|VRB|ABV)(?P<wind_speed>[0-9]{2})(?:G(?P<wind_gusts>[0-9]{2}))?(?P<wind_unit>KT|MPS) (?:(?P<wind_varying_from>[0-9]{3})V(?P<wind_varying_to>[0-9]{3}) )?(?P<visibility>CAVOK|NSC|SKC|M?[0-9]{2}SM|M?[0-9]{4}) (?P<rvr>(?:R[0-9]{2}[LCR]?/[PM]?[0-9]{4}(?:V[0-9]{4})?[DUN]? )*)(?P<wx>(?:(?:VC|\-|\+)?(?:TS|SH|FZ|BL|DR|MI|BC|PR|DZ|RA|SN|SG|PL|IC|GR|GS|UP|FG|BR|SA|DU|HZ|FU|VA|PO|SQ|FC|DS|SS) ?)*)(?P<cloud>NCD|(?:(?:FEW|SCT|BKN|OVC)[0-9]{3}(?:CB|TCU)? )*)(?:VV(?P<vert_visibility>[0-9]{3}) )?(?P<temperature>M?[0-9]{2})/(?P<dewpoint>M?[0-9]{2}) (?P<pressure>(?:Q|A)[0-9]{4}))(?: RMK (?P<remarks>.*))?").unwrap();
let parts = re.captures(&data);
if parts.is_none() {
return Err(MetarError::InvalidMetarError(data));
}
let parts = parts.unwrap();
let station = parts["station"].to_string();
let time_s = parts["time"].to_string();
time.date = match time_s[0..2].parse::<u8>() {
Ok(v) => v,
Err(e) => return Err(MetarError::TimeParseError(e)),
};
time.hour = match time_s[2..4].parse::<u8>() {
Ok(v) => v,
Err(e) => return Err(MetarError::TimeParseError(e)),
};
time.minute = match time_s[4..6].parse::<u8>() {
Ok(v) => v,
Err(e) => return Err(MetarError::TimeParseError(e)),
};
if &parts["data"] == "NIL" {
return Ok(Metar {
station,
time,
wind,
visibility,
cloud_layers,
vert_visibility,
temperature,
dewpoint,
pressure,
remarks,
});
}
let hdg = &parts["wind_dir"];
if hdg == "VRB" {
wind.dir = WindDirection::Variable;
} else if hdg == "ABV" {
wind.dir = WindDirection::Above;
} else {
wind.dir = WindDirection::Heading(match hdg.parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::WindDirectionError(e)),
});
}
let speed = match parts["wind_speed"].parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::WindSpeedError(e)),
};;
let mut gusting: Option<u32> = None;
if let Some(part) = parts.name("wind_gusts") {
gusting = Some(match part.as_str().parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::WindGustError(e)),
});
}
if parts["wind_unit"].ends_with("KT") {
wind.speed = WindSpeed::Knot(speed);
if let Some(g) = gusting {
wind.gusting = Some(WindSpeed::Knot(g));
}
} else {
wind.speed = WindSpeed::MetresPerSecond(speed);
if let Some(g) = gusting {
wind.gusting = Some(WindSpeed::MetresPerSecond(g));
}
}
if let Some(part) = parts.name("wind_varying_from") {
let from = match part.as_str().parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::WindVaryingError(e)),
};
let to = match parts["wind_varying_to"].parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::WindVaryingError(e)),
};
wind.varying = Some((from, to));
}
let visibility_p = &parts["visibility"];
if visibility_p == "CAVOK" {
visibility = Visibility::CavOK;
} else if visibility_p == "NSC" {
visibility = Visibility::NoSignificantClouds;
} else if visibility_p == "SKC" {
visibility = Visibility::SkyClear;
} else if visibility_p.starts_with("M") {
if visibility_p.ends_with("SM") {
visibility = Visibility::LessThanStatuteMiles(match visibility_p[1..3].parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::VisibilityError(e)),
});
} else {
visibility = Visibility::LessThanMetres(match visibility_p[1..5].parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::VisibilityError(e)),
});
}
} else {
if visibility_p.ends_with("SM") {
visibility = Visibility::StatuteMiles(match visibility_p[0..2].parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::VisibilityError(e)),
});
} else {
visibility = Visibility::Metres(match visibility_p[0..4].parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::VisibilityError(e)),
});
}
}
if let Some(clouds_s) = parts.name("clouds") {
let clouds_p: Vec<_> = clouds_s.as_str().split(" ").collect();
for cloud in clouds_p {
let part = cloud.trim();
if part == "NCD" {
break;
}
let mut typ = CloudType::Normal;
if part.ends_with("TCU") {
typ = CloudType::ToweringCumulus;
} else if part.ends_with("CB") {
typ = CloudType::Cumulonimbus;
}
let floor = match part[3..6].parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::CloudFloorError(e)),
};
if part.starts_with("FEW") {
cloud_layers.push(CloudLayer::Few(typ, floor));
} else if part.starts_with("SCT") {
cloud_layers.push(CloudLayer::Scattered(typ, floor));
} else if part.starts_with("BKN") {
cloud_layers.push(CloudLayer::Broken(typ, floor));
} else if part.starts_with("OVC") {
cloud_layers.push(CloudLayer::Overcast(typ, floor));
}
}
}
if let Some(part) = parts.name("vert_visibility") {
vert_visibility = match part.as_str().parse::<u32>() {
Ok(v) => Some(v),
Err(e) => return Err(MetarError::VerticalVisibilityError(e)),
};
}
let temp = &parts["temperature"];
let dewp = &parts["dewpoint"];
if temp.starts_with("M") {
temperature = -1 * match temp[1..].parse::<i32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::TemperatureError(e)),
};
} else {
temperature = match temp.parse::<i32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::TemperatureError(e)),
};
}
if dewp.starts_with("M") {
dewpoint = -1 * match dewp[1..].parse::<i32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::DewpointError(e)),
};
} else {
dewpoint = match dewp.parse::<i32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::DewpointError(e)),
};
}
if parts["pressure"].starts_with("Q") {
pressure = Pressure::Hectopascals(match parts["pressure"][1..].parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::AirPressureError(e)),
});
} else if parts["pressure"].starts_with("A") {
pressure = Pressure::InchesMercury(match parts["pressure"][1..].parse::<u32>() {
Ok(v) => v,
Err(e) => return Err(MetarError::AirPressureError(e)),
});
}
if let Some(part) = parts.name("remarks") {
remarks = Some(part.as_str().to_string());
} else {
remarks = None;
}
Ok(Metar {
station,
time,
wind,
visibility,
cloud_layers,
vert_visibility,
temperature,
dewpoint,
pressure,
remarks,
})
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_metar_1() {
let metar = "EGHI 282120Z 19015KT 140V220 6000 RA SCT006 BKN009 16/14 Q1006".to_string();
let r = super::Metar::parse(metar).unwrap_or_else(|e| {
eprintln!("{:#?}", e);
assert!(false);
std::process::exit(1);
});
assert_eq!(r.station, "EGHI");
assert_eq!(r.time.date, 28);
assert_eq!(r.time.hour, 21);
assert_eq!(r.time.minute, 20);
assert_eq!(r.wind.dir, super::WindDirection::Heading(190));
assert_eq!(r.wind.speed, super::WindSpeed::Knot(15));
assert_eq!(r.wind.varying, Some((140, 220)));
assert_eq!(r.visibility, super::Visibility::Metres(6000));
assert_eq!(r.temperature, 16);
assert_eq!(r.dewpoint, 14);
assert_eq!(r.pressure, super::Pressure::Hectopascals(1006));
}
#[test]
fn test_metar_2() {
let metar = "EGHI 062050Z 31006KT 270V340 CAVOK 13/07 Q1017".to_string();
let r = super::Metar::parse(metar).unwrap_or_else(|e| {
eprintln!("{:#?}", e);
assert!(false);
std::process::exit(1);
});
assert_eq!(r.station, "EGHI");
assert_eq!(r.time.date, 06);
assert_eq!(r.time.hour, 20);
assert_eq!(r.time.minute, 50);
assert_eq!(r.wind.dir, super::WindDirection::Heading(310));
assert_eq!(r.wind.speed, super::WindSpeed::Knot(6));
assert_eq!(r.wind.varying, Some((270, 340)));
assert_eq!(r.visibility, super::Visibility::CavOK);
assert_eq!(r.temperature, 13);
assert_eq!(r.dewpoint, 7);
assert_eq!(r.pressure, super::Pressure::Hectopascals(1017));
}
#[test]
fn test_metar_3() {
let metar = "EGHI 071520Z 19013KT 160V220 3000 -RADZ BR BKN006 15/14 Q1012".to_string();
let r = super::Metar::parse(metar).unwrap_or_else(|e| {
eprintln!("{:#?}", e);
assert!(false);
std::process::exit(1);
});
assert_eq!(r.station, "EGHI");
assert_eq!(r.time.date, 07);
assert_eq!(r.time.hour, 15);
assert_eq!(r.time.minute, 20);
assert_eq!(r.wind.dir, super::WindDirection::Heading(190));
assert_eq!(r.wind.speed, super::WindSpeed::Knot(13));
assert_eq!(r.wind.varying, Some((160, 220)));
assert_eq!(r.visibility, super::Visibility::Metres(3000));
assert_eq!(r.temperature, 15);
assert_eq!(r.dewpoint, 14);
assert_eq!(r.pressure, super::Pressure::Hectopascals(1012));
}
#[test]
fn test_metar_4() {
let metar = "EGHI 071750Z 21010KT 3500 -RADZ BR BKN004 16/15 Q1011".to_string();
let r = super::Metar::parse(metar).unwrap_or_else(|e| {
eprintln!("{:#?}", e);
assert!(false);
std::process::exit(1);
});
assert_eq!(r.station, "EGHI");
assert_eq!(r.time.date, 07);
assert_eq!(r.time.hour, 17);
assert_eq!(r.time.minute, 50);
assert_eq!(r.wind.dir, super::WindDirection::Heading(210));
assert_eq!(r.wind.speed, super::WindSpeed::Knot(10));
assert_eq!(r.wind.varying, None);
assert_eq!(r.visibility, super::Visibility::Metres(3500));
assert_eq!(r.temperature, 16);
assert_eq!(r.dewpoint, 15);
assert_eq!(r.pressure, super::Pressure::Hectopascals(1011));
}
#[test]
fn test_metar_5() {
let metar = "EGHI 080650Z VRB03KT CAVOK 12/10 Q1009".to_string();
let r = super::Metar::parse(metar).unwrap_or_else(|e| {
eprintln!("{:#?}", e);
assert!(false);
std::process::exit(1);
});
assert_eq!(r.station, "EGHI");
assert_eq!(r.time.date, 08);
assert_eq!(r.time.hour, 06);
assert_eq!(r.time.minute, 50);
assert_eq!(r.wind.dir, super::WindDirection::Variable);
assert_eq!(r.wind.speed, super::WindSpeed::Knot(3));
assert_eq!(r.wind.varying, None);
assert_eq!(r.visibility, super::Visibility::CavOK);
assert_eq!(r.temperature, 12);
assert_eq!(r.dewpoint, 10);
assert_eq!(r.pressure, super::Pressure::Hectopascals(1009));
}
#[test]
fn test_metar_6() {
let metar = "EGHI 081650Z 23010KT 9999 VCSH FEW018 FEW025TCU 15/11 Q1006".to_string();
let r = super::Metar::parse(metar).unwrap_or_else(|e| {
eprintln!("{:#?}", e);
assert!(false);
std::process::exit(1);
});
assert_eq!(r.station, "EGHI");
assert_eq!(r.time.date, 08);
assert_eq!(r.time.hour, 16);
assert_eq!(r.time.minute, 50);
assert_eq!(r.wind.dir, super::WindDirection::Heading(230));
assert_eq!(r.wind.speed, super::WindSpeed::Knot(10));
assert_eq!(r.wind.varying, None);
assert_eq!(r.visibility, super::Visibility::Metres(9999));
assert_eq!(r.temperature, 15);
assert_eq!(r.dewpoint, 11);
assert_eq!(r.pressure, super::Pressure::Hectopascals(1006));
}
#[test]
fn test_metar_7() {
let metar = "EGHI 110750Z 22017G28KT 190V250 6000 -RA FEW007 BKN010 15/14 Q1008 RERA".to_string();
let r = super::Metar::parse(metar).unwrap_or_else(|e| {
eprintln!("{:#?}", e);
assert!(false);
std::process::exit(1);
});
assert_eq!(r.station, "EGHI");
assert_eq!(r.time.date, 11);
assert_eq!(r.time.hour, 07);
assert_eq!(r.time.minute, 50);
assert_eq!(r.wind.dir, super::WindDirection::Heading(220));
assert_eq!(r.wind.speed, super::WindSpeed::Knot(17));
assert_eq!(r.wind.gusting, Some(super::WindSpeed::Knot(28)));
assert_eq!(r.wind.varying, Some((190, 250)));
assert_eq!(r.visibility, super::Visibility::Metres(6000));
assert_eq!(r.temperature, 15);
assert_eq!(r.dewpoint, 14);
assert_eq!(r.pressure, super::Pressure::Hectopascals(1008));
}
}