//! Climatological region lookup. 1:1 port of //! `lib/microwaveprop/propagation/region.ex`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Region { GulfCoast, Southeast, SouthernPlains, CornBelt, Northeast, DesertSouthwest, PacificNorthwest, MountainWest, Other, } /// Lat min (inclusive), lat max (exclusive), lon min (inclusive), lon max (exclusive). struct BBox(Region, f64, f64, f64, f64); // Ordered to match Elixir's `Enum.find_value` short-circuit. const REGIONS: &[BBox] = &[ BBox(Region::GulfCoast, 25.0, 32.0, -100.0, -80.0), BBox(Region::Southeast, 30.0, 37.0, -90.0, -75.0), BBox(Region::SouthernPlains, 32.0, 38.0, -105.0, -93.0), BBox(Region::CornBelt, 38.0, 48.0, -100.0, -82.0), BBox(Region::Northeast, 38.0, 48.0, -82.0, -67.0), BBox(Region::DesertSouthwest, 30.0, 38.0, -120.0, -105.0), BBox(Region::PacificNorthwest, 42.0, 50.0, -125.0, -115.0), BBox(Region::MountainWest, 38.0, 48.0, -115.0, -100.0), ]; pub fn for_point(lat: f64, lon: f64) -> Region { for bbox in REGIONS { let BBox(name, lat_min, lat_max, lon_min, lon_max) = *bbox; if lat >= lat_min && lat < lat_max && lon >= lon_min && lon < lon_max { return name; } } Region::Other } /// Multiplier applied to the band's `seasonal_base` score. Unknown /// region/month combinations return 1.0. Range [0.7, 1.3]. pub fn seasonal_adjustment(region: Region, month: u8) -> f64 { match (region, month) { (Region::GulfCoast, 6) => 0.95, (Region::GulfCoast, 7) => 0.95, (Region::GulfCoast, 8) => 1.15, (Region::GulfCoast, 9) => 1.10, (Region::CornBelt, 6) => 1.05, (Region::CornBelt, 7) => 0.85, (Region::CornBelt, 8) => 0.80, (Region::CornBelt, 9) => 0.95, (Region::Southeast, 7) => 0.97, (Region::Southeast, 8) => 1.08, (Region::SouthernPlains, 8) => 1.05, (Region::DesertSouthwest, 7) => 1.10, (Region::DesertSouthwest, 8) => 1.10, (Region::DesertSouthwest, 9) => 1.05, (Region::PacificNorthwest, 6) => 1.15, (Region::PacificNorthwest, 7) => 1.20, (Region::PacificNorthwest, 8) => 1.20, (Region::PacificNorthwest, 9) => 1.10, _ => 1.0, } } #[cfg(test)] mod tests { use super::*; #[test] fn gulf_coast_points() { // Houston ≈ (29.8, -95.4) — Gulf Coast bbox. assert_eq!(for_point(29.8, -95.4), Region::GulfCoast); } #[test] fn corn_belt_points() { // Des Moines ≈ (41.6, -93.6) — Corn Belt bbox. assert_eq!(for_point(41.6, -93.6), Region::CornBelt); } #[test] fn non_conus_returns_other() { // Alaska (61.2, -149.9) — outside every bbox. assert_eq!(for_point(61.2, -149.9), Region::Other); } #[test] fn adjustments_match_elixir_table() { assert_eq!(seasonal_adjustment(Region::GulfCoast, 8), 1.15); assert_eq!(seasonal_adjustment(Region::CornBelt, 7), 0.85); assert_eq!(seasonal_adjustment(Region::PacificNorthwest, 7), 1.20); assert_eq!(seasonal_adjustment(Region::Other, 8), 1.0); assert_eq!(seasonal_adjustment(Region::CornBelt, 1), 1.0); } #[test] fn precedence_matches_elixir_ordering() { // (30.5, -90.0) is inside BOTH gulf_coast (25..32, -100..-80) and // southeast (30..37, -90..-75). Elixir hits gulf_coast first. assert_eq!(for_point(30.5, -90.0), Region::GulfCoast); } }