use crate::error::{check_azimuth, check_pressure, check_temperature, check_zenith_angle};
use crate::math::{floor, rem_euclid};
use crate::Result;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Horizon {
SunriseSunset,
CivilTwilight,
NauticalTwilight,
AstronomicalTwilight,
Custom(f64),
}
impl Horizon {
#[must_use]
pub const fn elevation_angle(&self) -> f64 {
match self {
Self::SunriseSunset => -0.83337, Self::CivilTwilight => -6.0,
Self::NauticalTwilight => -12.0,
Self::AstronomicalTwilight => -18.0,
Self::Custom(angle) => *angle,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RefractionCorrection {
pressure: f64,
temperature: f64,
}
impl RefractionCorrection {
pub fn new(pressure: f64, temperature: f64) -> Result<Self> {
check_pressure(pressure)?;
check_temperature(temperature)?;
Ok(Self {
pressure,
temperature,
})
}
#[must_use]
pub const fn standard() -> Self {
Self {
pressure: 1013.25,
temperature: 15.0,
}
}
#[must_use]
pub const fn pressure(&self) -> f64 {
self.pressure
}
#[must_use]
pub const fn temperature(&self) -> f64 {
self.temperature
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SolarPosition {
azimuth: f64,
zenith_angle: f64,
}
impl SolarPosition {
pub fn new(azimuth: f64, zenith_angle: f64) -> Result<Self> {
let normalized_azimuth = check_azimuth(azimuth)?;
let validated_zenith = check_zenith_angle(zenith_angle)?;
Ok(Self {
azimuth: normalized_azimuth,
zenith_angle: validated_zenith,
})
}
#[must_use]
pub const fn azimuth(&self) -> f64 {
self.azimuth
}
#[must_use]
pub const fn zenith_angle(&self) -> f64 {
self.zenith_angle
}
#[must_use]
pub fn elevation_angle(&self) -> f64 {
90.0 - self.zenith_angle
}
#[must_use]
pub fn is_sun_up(&self) -> bool {
self.elevation_angle() > 0.0
}
#[must_use]
pub fn is_sun_down(&self) -> bool {
self.elevation_angle() <= 0.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HoursUtc(f64);
impl HoursUtc {
#[must_use]
pub const fn from_hours(hours: f64) -> Self {
Self(hours)
}
#[must_use]
pub const fn hours(&self) -> f64 {
self.0
}
#[must_use]
pub fn day_and_hours(&self) -> (i32, f64) {
let hours = self.0;
(floor(hours / 24.0) as i32, rem_euclid(hours, 24.0))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "std",
doc = "Default generic parameter is `()`; chrono helpers return `SunriseResult<chrono::DateTime<Tz>>`."
)]
pub enum SunriseResult<T = ()> {
RegularDay {
sunrise: T,
transit: T,
sunset: T,
},
AllDay {
transit: T,
},
AllNight {
transit: T,
},
}
impl<T> SunriseResult<T> {
pub const fn transit(&self) -> &T {
match self {
Self::RegularDay { transit, .. }
| Self::AllDay { transit }
| Self::AllNight { transit } => transit,
}
}
pub const fn is_regular_day(&self) -> bool {
matches!(self, Self::RegularDay { .. })
}
pub const fn is_polar_day(&self) -> bool {
matches!(self, Self::AllDay { .. })
}
pub const fn is_polar_night(&self) -> bool {
matches!(self, Self::AllNight { .. })
}
pub const fn sunrise(&self) -> Option<&T> {
if let Self::RegularDay { sunrise, .. } = self {
Some(sunrise)
} else {
None
}
}
pub const fn sunset(&self) -> Option<&T> {
if let Self::RegularDay { sunset, .. } = self {
Some(sunset)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_horizon_elevation_angles() {
assert_eq!(Horizon::SunriseSunset.elevation_angle(), -0.83337);
assert_eq!(Horizon::CivilTwilight.elevation_angle(), -6.0);
assert_eq!(Horizon::NauticalTwilight.elevation_angle(), -12.0);
assert_eq!(Horizon::AstronomicalTwilight.elevation_angle(), -18.0);
assert_eq!(Horizon::Custom(-3.0).elevation_angle(), -3.0);
}
#[test]
fn test_solar_position_creation() {
let pos = SolarPosition::new(180.0, 45.0).unwrap();
assert_eq!(pos.azimuth(), 180.0);
assert_eq!(pos.zenith_angle(), 45.0);
assert_eq!(pos.elevation_angle(), 45.0);
assert!(pos.is_sun_up());
assert!(!pos.is_sun_down());
let pos = SolarPosition::new(-90.0, 90.0).unwrap();
assert_eq!(pos.azimuth(), 270.0);
assert_eq!(pos.elevation_angle(), 0.0);
assert!(SolarPosition::new(0.0, -1.0).is_err());
assert!(SolarPosition::new(0.0, 181.0).is_err());
}
#[test]
fn test_solar_position_sun_state() {
let above_horizon = SolarPosition::new(180.0, 30.0).unwrap();
assert!(above_horizon.is_sun_up());
assert!(!above_horizon.is_sun_down());
let on_horizon = SolarPosition::new(180.0, 90.0).unwrap();
assert!(!on_horizon.is_sun_up());
assert!(on_horizon.is_sun_down());
let below_horizon = SolarPosition::new(180.0, 120.0).unwrap();
assert!(!below_horizon.is_sun_up());
assert!(below_horizon.is_sun_down());
}
#[test]
fn test_sunrise_result_regular_day() {
use chrono::{DateTime, Utc};
let sunrise = "2023-06-21T05:30:00Z".parse::<DateTime<Utc>>().unwrap();
let transit = "2023-06-21T12:00:00Z".parse::<DateTime<Utc>>().unwrap();
let sunset = "2023-06-21T18:30:00Z".parse::<DateTime<Utc>>().unwrap();
let result = SunriseResult::RegularDay {
sunrise,
transit,
sunset,
};
assert!(result.is_regular_day());
assert!(!result.is_polar_day());
assert!(!result.is_polar_night());
assert_eq!(result.transit(), &transit);
assert_eq!(result.sunrise(), Some(&sunrise));
assert_eq!(result.sunset(), Some(&sunset));
}
#[test]
fn test_sunrise_result_polar_day() {
use chrono::{DateTime, Utc};
let transit = "2023-06-21T12:00:00Z".parse::<DateTime<Utc>>().unwrap();
let result = SunriseResult::AllDay { transit };
assert!(!result.is_regular_day());
assert!(result.is_polar_day());
assert!(!result.is_polar_night());
assert_eq!(result.transit(), &transit);
assert_eq!(result.sunrise(), None);
assert_eq!(result.sunset(), None);
}
#[test]
fn test_sunrise_result_polar_night() {
use chrono::{DateTime, Utc};
let transit = "2023-12-21T12:00:00Z".parse::<DateTime<Utc>>().unwrap();
let result = SunriseResult::AllNight { transit };
assert!(!result.is_regular_day());
assert!(!result.is_polar_day());
assert!(result.is_polar_night());
assert_eq!(result.transit(), &transit);
assert_eq!(result.sunrise(), None);
assert_eq!(result.sunset(), None);
}
#[test]
fn test_refraction_correction() {
let standard = RefractionCorrection::standard();
assert_eq!(standard.pressure(), 1013.25);
assert_eq!(standard.temperature(), 15.0);
let custom = RefractionCorrection::new(1000.0, 20.0).unwrap();
assert_eq!(custom.pressure(), 1000.0);
assert_eq!(custom.temperature(), 20.0);
assert!(RefractionCorrection::new(-1.0, 15.0).is_err()); assert!(RefractionCorrection::new(1013.25, -300.0).is_err()); assert!(RefractionCorrection::new(3000.0, 15.0).is_err()); assert!(RefractionCorrection::new(1013.25, 150.0).is_err()); }
}