use crate::qtty::{Angular, Deg, Quantity, Unit};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TwilightPhase {
Day,
Civil,
Nautical,
Astronomical,
Dark,
}
impl std::fmt::Display for TwilightPhase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Day => write!(f, "Day"),
Self::Civil => write!(f, "Civil twilight"),
Self::Nautical => write!(f, "Nautical twilight"),
Self::Astronomical => write!(f, "Astronomical twilight"),
Self::Dark => write!(f, "Dark"),
}
}
}
pub fn twilight_classification<U>(sun_altitude: Quantity<U>) -> TwilightPhase
where
U: Unit<Dim = Angular>,
{
let alt = sun_altitude.to::<Deg>().value();
if alt > 0.0 {
TwilightPhase::Day
} else if alt > -6.0 {
TwilightPhase::Civil
} else if alt > -12.0 {
TwilightPhase::Nautical
} else if alt > -18.0 {
TwilightPhase::Astronomical
} else {
TwilightPhase::Dark
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::qtty::Degrees;
#[test]
fn positive_altitude_is_day() {
assert_eq!(
twilight_classification(Degrees::new(0.001)),
TwilightPhase::Day
);
assert_eq!(
twilight_classification(Degrees::new(45.0)),
TwilightPhase::Day
);
assert_eq!(
twilight_classification(Degrees::new(90.0)),
TwilightPhase::Day
);
}
#[test]
fn mid_civil_is_civil() {
assert_eq!(
twilight_classification(Degrees::new(-3.0)),
TwilightPhase::Civil
);
}
#[test]
fn mid_nautical_is_nautical() {
assert_eq!(
twilight_classification(Degrees::new(-9.0)),
TwilightPhase::Nautical
);
}
#[test]
fn mid_astronomical_is_astronomical() {
assert_eq!(
twilight_classification(Degrees::new(-15.0)),
TwilightPhase::Astronomical
);
}
#[test]
fn deep_negative_is_dark() {
assert_eq!(
twilight_classification(Degrees::new(-30.0)),
TwilightPhase::Dark
);
assert_eq!(
twilight_classification(Degrees::new(-90.0)),
TwilightPhase::Dark
);
}
#[test]
fn boundary_zero_degrees_is_civil() {
assert_eq!(
twilight_classification(Degrees::new(0.0)),
TwilightPhase::Civil,
"0° is the inclusive upper bound of Civil (Day requires strictly > 0°)"
);
}
#[test]
fn just_above_zero_is_day() {
assert_eq!(
twilight_classification(Degrees::new(0.001)),
TwilightPhase::Day
);
}
#[test]
fn just_below_zero_is_civil() {
assert_eq!(
twilight_classification(Degrees::new(-0.001)),
TwilightPhase::Civil,
"Just below 0° enters Civil"
);
}
#[test]
fn boundary_minus_6_is_nautical() {
assert_eq!(
twilight_classification(Degrees::new(-6.0)),
TwilightPhase::Nautical,
"-6° is the inclusive upper bound of Nautical"
);
}
#[test]
fn just_below_minus_6_is_nautical() {
assert_eq!(
twilight_classification(Degrees::new(-6.001)),
TwilightPhase::Nautical,
"Just below -6° enters Nautical"
);
}
#[test]
fn boundary_minus_12_is_astronomical() {
assert_eq!(
twilight_classification(Degrees::new(-12.0)),
TwilightPhase::Astronomical,
"-12° is the inclusive upper bound of Astronomical"
);
}
#[test]
fn just_below_minus_12_is_astronomical() {
assert_eq!(
twilight_classification(Degrees::new(-12.001)),
TwilightPhase::Astronomical,
"Just below -12° enters Astronomical"
);
}
#[test]
fn boundary_minus_18_is_dark() {
assert_eq!(
twilight_classification(Degrees::new(-18.0)),
TwilightPhase::Dark,
"-18° is the inclusive upper bound of Dark"
);
}
#[test]
fn just_below_minus_18_is_dark() {
assert_eq!(
twilight_classification(Degrees::new(-18.001)),
TwilightPhase::Dark,
"Just below -18° enters Dark"
);
}
#[test]
fn task_boundary_zero() {
assert_eq!(
twilight_classification(Degrees::new(0.0)),
TwilightPhase::Civil
);
}
#[test]
fn task_boundary_minus_6() {
assert_eq!(
twilight_classification(Degrees::new(-6.0)),
TwilightPhase::Nautical
);
}
#[test]
fn task_boundary_minus_12() {
assert_eq!(
twilight_classification(Degrees::new(-12.0)),
TwilightPhase::Astronomical
);
}
#[test]
fn task_boundary_minus_18() {
assert_eq!(
twilight_classification(Degrees::new(-18.0)),
TwilightPhase::Dark
);
}
#[test]
fn task_plus_0_001_is_day() {
assert_eq!(
twilight_classification(Degrees::new(0.001)),
TwilightPhase::Day
);
}
#[test]
fn task_minus_18_001_is_dark() {
assert_eq!(
twilight_classification(Degrees::new(-18.001)),
TwilightPhase::Dark
);
}
#[test]
fn radians_input_classifies_correctly() {
use crate::qtty::Radians;
assert_eq!(
twilight_classification(Radians::new(-9.0_f64.to_radians())),
TwilightPhase::Nautical
);
assert_eq!(
twilight_classification(Radians::new(-18.0_f64.to_radians())),
TwilightPhase::Dark
);
assert_eq!(
twilight_classification(Radians::new(45.0_f64.to_radians())),
TwilightPhase::Day
);
}
#[test]
fn eq_and_clone() {
assert_eq!(TwilightPhase::Civil, TwilightPhase::Civil.clone());
assert_ne!(TwilightPhase::Civil, TwilightPhase::Nautical);
}
#[test]
fn debug_contains_variant_name() {
assert!(format!("{:?}", TwilightPhase::Dark).contains("Dark"));
assert!(format!("{:?}", TwilightPhase::Astronomical).contains("Astronomical"));
}
#[test]
fn display_is_human_readable() {
assert_eq!(TwilightPhase::Day.to_string(), "Day");
assert_eq!(TwilightPhase::Civil.to_string(), "Civil twilight");
assert_eq!(TwilightPhase::Nautical.to_string(), "Nautical twilight");
assert_eq!(
TwilightPhase::Astronomical.to_string(),
"Astronomical twilight"
);
assert_eq!(TwilightPhase::Dark.to_string(), "Dark");
}
#[test]
fn hash_works_in_hashset() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(TwilightPhase::Day);
set.insert(TwilightPhase::Civil);
set.insert(TwilightPhase::Day); assert_eq!(set.len(), 2);
}
}