solar_positioning/
types.rs1use crate::error::{check_azimuth, check_zenith_angle};
4use crate::{Error, Result};
5
6#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum Horizon {
12 SunriseSunset,
14 CivilTwilight,
16 NauticalTwilight,
18 AstronomicalTwilight,
20 Custom(f64),
22}
23
24impl Horizon {
25 #[must_use]
32 pub const fn elevation_angle(&self) -> f64 {
33 match self {
34 Self::SunriseSunset => -0.83337, Self::CivilTwilight => -6.0,
36 Self::NauticalTwilight => -12.0,
37 Self::AstronomicalTwilight => -18.0,
38 Self::Custom(angle) => *angle,
39 }
40 }
41
42 pub fn custom(elevation_degrees: f64) -> Result<Self> {
53 if !(-90.0..=90.0).contains(&elevation_degrees) {
54 return Err(Error::invalid_elevation_angle(elevation_degrees));
55 }
56 Ok(Self::Custom(elevation_degrees))
57 }
58}
59
60impl Eq for Horizon {}
61
62impl std::hash::Hash for Horizon {
63 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
64 match self {
65 Self::SunriseSunset => 0.hash(state),
66 Self::CivilTwilight => 1.hash(state),
67 Self::NauticalTwilight => 2.hash(state),
68 Self::AstronomicalTwilight => 3.hash(state),
69 Self::Custom(angle) => {
70 4.hash(state);
71 angle.to_bits().hash(state);
73 }
74 }
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87pub struct SolarPosition {
88 azimuth: f64,
90 zenith_angle: f64,
92}
93
94impl SolarPosition {
95 pub fn new(azimuth: f64, zenith_angle: f64) -> Result<Self> {
116 let normalized_azimuth = check_azimuth(azimuth)?;
117 let validated_zenith = check_zenith_angle(zenith_angle)?;
118
119 Ok(Self {
120 azimuth: normalized_azimuth,
121 zenith_angle: validated_zenith,
122 })
123 }
124
125 #[must_use]
130 pub const fn azimuth(&self) -> f64 {
131 self.azimuth
132 }
133
134 #[must_use]
139 pub const fn zenith_angle(&self) -> f64 {
140 self.zenith_angle
141 }
142
143 #[must_use]
150 pub fn elevation_angle(&self) -> f64 {
151 90.0 - self.zenith_angle
152 }
153
154 #[must_use]
159 pub fn is_sun_up(&self) -> bool {
160 self.elevation_angle() > 0.0
161 }
162
163 #[must_use]
168 pub fn is_sun_down(&self) -> bool {
169 self.elevation_angle() <= 0.0
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
178#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
179pub enum SunriseResult<T = chrono::DateTime<chrono::Utc>> {
180 RegularDay {
182 sunrise: T,
184 transit: T,
186 sunset: T,
188 },
189 AllDay {
191 transit: T,
193 },
194 AllNight {
196 transit: T,
198 },
199}
200
201impl<T> SunriseResult<T> {
202 pub const fn transit(&self) -> &T {
207 match self {
208 Self::RegularDay { transit, .. }
209 | Self::AllDay { transit }
210 | Self::AllNight { transit } => transit,
211 }
212 }
213
214 pub const fn is_regular_day(&self) -> bool {
219 matches!(self, Self::RegularDay { .. })
220 }
221
222 pub const fn is_polar_day(&self) -> bool {
227 matches!(self, Self::AllDay { .. })
228 }
229
230 pub const fn is_polar_night(&self) -> bool {
235 matches!(self, Self::AllNight { .. })
236 }
237
238 pub const fn sunrise(&self) -> Option<&T> {
243 if let Self::RegularDay { sunrise, .. } = self {
244 Some(sunrise)
245 } else {
246 None
247 }
248 }
249
250 pub const fn sunset(&self) -> Option<&T> {
255 if let Self::RegularDay { sunset, .. } = self {
256 Some(sunset)
257 } else {
258 None
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_horizon_elevation_angles() {
269 assert_eq!(Horizon::SunriseSunset.elevation_angle(), -0.83337);
270 assert_eq!(Horizon::CivilTwilight.elevation_angle(), -6.0);
271 assert_eq!(Horizon::NauticalTwilight.elevation_angle(), -12.0);
272 assert_eq!(Horizon::AstronomicalTwilight.elevation_angle(), -18.0);
273
274 let custom = Horizon::custom(-3.0).unwrap();
275 assert_eq!(custom.elevation_angle(), -3.0);
276
277 assert!(Horizon::custom(-95.0).is_err());
278 assert!(Horizon::custom(95.0).is_err());
279 }
280
281 #[test]
282 fn test_solar_position_creation() {
283 let pos = SolarPosition::new(180.0, 45.0).unwrap();
284 assert_eq!(pos.azimuth(), 180.0);
285 assert_eq!(pos.zenith_angle(), 45.0);
286 assert_eq!(pos.elevation_angle(), 45.0);
287 assert!(pos.is_sun_up());
288 assert!(!pos.is_sun_down());
289
290 let pos = SolarPosition::new(-90.0, 90.0).unwrap();
292 assert_eq!(pos.azimuth(), 270.0);
293 assert_eq!(pos.elevation_angle(), 0.0);
294
295 assert!(SolarPosition::new(0.0, -1.0).is_err());
297 assert!(SolarPosition::new(0.0, 181.0).is_err());
298 }
299
300 #[test]
301 fn test_solar_position_sun_state() {
302 let above_horizon = SolarPosition::new(180.0, 30.0).unwrap();
303 assert!(above_horizon.is_sun_up());
304 assert!(!above_horizon.is_sun_down());
305
306 let on_horizon = SolarPosition::new(180.0, 90.0).unwrap();
307 assert!(!on_horizon.is_sun_up());
308 assert!(on_horizon.is_sun_down());
309
310 let below_horizon = SolarPosition::new(180.0, 120.0).unwrap();
311 assert!(!below_horizon.is_sun_up());
312 assert!(below_horizon.is_sun_down());
313 }
314
315 #[test]
316 fn test_sunrise_result_regular_day() {
317 use chrono::{DateTime, Utc};
318
319 let sunrise = "2023-06-21T05:30:00Z".parse::<DateTime<Utc>>().unwrap();
320 let transit = "2023-06-21T12:00:00Z".parse::<DateTime<Utc>>().unwrap();
321 let sunset = "2023-06-21T18:30:00Z".parse::<DateTime<Utc>>().unwrap();
322
323 let result = SunriseResult::RegularDay {
324 sunrise,
325 transit,
326 sunset,
327 };
328
329 assert!(result.is_regular_day());
330 assert!(!result.is_polar_day());
331 assert!(!result.is_polar_night());
332 assert_eq!(result.transit(), &transit);
333 assert_eq!(result.sunrise(), Some(&sunrise));
334 assert_eq!(result.sunset(), Some(&sunset));
335 }
336
337 #[test]
338 fn test_sunrise_result_polar_day() {
339 use chrono::{DateTime, Utc};
340
341 let transit = "2023-06-21T12:00:00Z".parse::<DateTime<Utc>>().unwrap();
342 let result = SunriseResult::AllDay { transit };
343
344 assert!(!result.is_regular_day());
345 assert!(result.is_polar_day());
346 assert!(!result.is_polar_night());
347 assert_eq!(result.transit(), &transit);
348 assert_eq!(result.sunrise(), None);
349 assert_eq!(result.sunset(), None);
350 }
351
352 #[test]
353 fn test_sunrise_result_polar_night() {
354 use chrono::{DateTime, Utc};
355
356 let transit = "2023-12-21T12:00:00Z".parse::<DateTime<Utc>>().unwrap();
357 let result = SunriseResult::AllNight { transit };
358
359 assert!(!result.is_regular_day());
360 assert!(!result.is_polar_day());
361 assert!(result.is_polar_night());
362 assert_eq!(result.transit(), &transit);
363 assert_eq!(result.sunrise(), None);
364 assert_eq!(result.sunset(), None);
365 }
366}