1use crate::error::{check_azimuth, check_pressure, check_temperature, check_zenith_angle};
4use crate::{Error, Result};
5
6#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum Horizon {
11 SunriseSunset,
13 CivilTwilight,
15 NauticalTwilight,
17 AstronomicalTwilight,
19 Custom(f64),
21}
22
23impl Horizon {
24 #[must_use]
28 pub const fn elevation_angle(&self) -> f64 {
29 match self {
30 Self::SunriseSunset => -0.83337, Self::CivilTwilight => -6.0,
32 Self::NauticalTwilight => -12.0,
33 Self::AstronomicalTwilight => -18.0,
34 Self::Custom(angle) => *angle,
35 }
36 }
37
38 pub fn custom(elevation_degrees: f64) -> Result<Self> {
43 if !(-90.0..=90.0).contains(&elevation_degrees) {
44 return Err(Error::invalid_elevation_angle(elevation_degrees));
45 }
46 Ok(Self::Custom(elevation_degrees))
47 }
48}
49
50impl Eq for Horizon {}
51
52impl core::hash::Hash for Horizon {
53 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
54 match self {
55 Self::SunriseSunset => 0.hash(state),
56 Self::CivilTwilight => 1.hash(state),
57 Self::NauticalTwilight => 2.hash(state),
58 Self::AstronomicalTwilight => 3.hash(state),
59 Self::Custom(angle) => {
60 4.hash(state);
61 angle.to_bits().hash(state);
63 }
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq)]
87pub struct RefractionCorrection {
88 pressure: f64,
90 temperature: f64,
92}
93
94impl RefractionCorrection {
95 pub fn new(pressure: f64, temperature: f64) -> Result<Self> {
108 check_pressure(pressure)?;
109 check_temperature(temperature)?;
110 Ok(Self {
111 pressure,
112 temperature,
113 })
114 }
115
116 #[must_use]
130 pub const fn standard() -> Self {
131 Self {
132 pressure: 1013.25,
133 temperature: 15.0,
134 }
135 }
136
137 #[must_use]
139 pub const fn pressure(&self) -> f64 {
140 self.pressure
141 }
142
143 #[must_use]
145 pub const fn temperature(&self) -> f64 {
146 self.temperature
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq)]
158pub struct SolarPosition {
159 azimuth: f64,
161 zenith_angle: f64,
163}
164
165impl SolarPosition {
166 pub fn new(azimuth: f64, zenith_angle: f64) -> Result<Self> {
180 let normalized_azimuth = check_azimuth(azimuth)?;
181 let validated_zenith = check_zenith_angle(zenith_angle)?;
182
183 Ok(Self {
184 azimuth: normalized_azimuth,
185 zenith_angle: validated_zenith,
186 })
187 }
188
189 #[must_use]
191 pub const fn azimuth(&self) -> f64 {
192 self.azimuth
193 }
194
195 #[must_use]
197 pub const fn zenith_angle(&self) -> f64 {
198 self.zenith_angle
199 }
200
201 #[must_use]
205 pub fn elevation_angle(&self) -> f64 {
206 90.0 - self.zenith_angle
207 }
208
209 #[must_use]
211 pub fn is_sun_up(&self) -> bool {
212 self.elevation_angle() > 0.0
213 }
214
215 #[must_use]
217 pub fn is_sun_down(&self) -> bool {
218 self.elevation_angle() <= 0.0
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
227#[cfg_attr(
228 feature = "std",
229 doc = "Default generic parameter is `chrono::DateTime<chrono::Utc>` when `std` feature is enabled."
230)]
231pub enum SunriseResult<T = ()> {
232 RegularDay {
234 sunrise: T,
236 transit: T,
238 sunset: T,
240 },
241 AllDay {
243 transit: T,
245 },
246 AllNight {
248 transit: T,
250 },
251}
252
253impl<T> SunriseResult<T> {
254 pub const fn transit(&self) -> &T {
256 match self {
257 Self::RegularDay { transit, .. }
258 | Self::AllDay { transit }
259 | Self::AllNight { transit } => transit,
260 }
261 }
262
263 pub const fn is_regular_day(&self) -> bool {
265 matches!(self, Self::RegularDay { .. })
266 }
267
268 pub const fn is_polar_day(&self) -> bool {
270 matches!(self, Self::AllDay { .. })
271 }
272
273 pub const fn is_polar_night(&self) -> bool {
275 matches!(self, Self::AllNight { .. })
276 }
277
278 pub const fn sunrise(&self) -> Option<&T> {
280 if let Self::RegularDay { sunrise, .. } = self {
281 Some(sunrise)
282 } else {
283 None
284 }
285 }
286
287 pub const fn sunset(&self) -> Option<&T> {
289 if let Self::RegularDay { sunset, .. } = self {
290 Some(sunset)
291 } else {
292 None
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn test_horizon_elevation_angles() {
303 assert_eq!(Horizon::SunriseSunset.elevation_angle(), -0.83337);
304 assert_eq!(Horizon::CivilTwilight.elevation_angle(), -6.0);
305 assert_eq!(Horizon::NauticalTwilight.elevation_angle(), -12.0);
306 assert_eq!(Horizon::AstronomicalTwilight.elevation_angle(), -18.0);
307
308 let custom = Horizon::custom(-3.0).unwrap();
309 assert_eq!(custom.elevation_angle(), -3.0);
310
311 assert!(Horizon::custom(-95.0).is_err());
312 assert!(Horizon::custom(95.0).is_err());
313 }
314
315 #[test]
316 fn test_solar_position_creation() {
317 let pos = SolarPosition::new(180.0, 45.0).unwrap();
318 assert_eq!(pos.azimuth(), 180.0);
319 assert_eq!(pos.zenith_angle(), 45.0);
320 assert_eq!(pos.elevation_angle(), 45.0);
321 assert!(pos.is_sun_up());
322 assert!(!pos.is_sun_down());
323
324 let pos = SolarPosition::new(-90.0, 90.0).unwrap();
326 assert_eq!(pos.azimuth(), 270.0);
327 assert_eq!(pos.elevation_angle(), 0.0);
328
329 assert!(SolarPosition::new(0.0, -1.0).is_err());
331 assert!(SolarPosition::new(0.0, 181.0).is_err());
332 }
333
334 #[test]
335 fn test_solar_position_sun_state() {
336 let above_horizon = SolarPosition::new(180.0, 30.0).unwrap();
337 assert!(above_horizon.is_sun_up());
338 assert!(!above_horizon.is_sun_down());
339
340 let on_horizon = SolarPosition::new(180.0, 90.0).unwrap();
341 assert!(!on_horizon.is_sun_up());
342 assert!(on_horizon.is_sun_down());
343
344 let below_horizon = SolarPosition::new(180.0, 120.0).unwrap();
345 assert!(!below_horizon.is_sun_up());
346 assert!(below_horizon.is_sun_down());
347 }
348
349 #[test]
350 fn test_sunrise_result_regular_day() {
351 use chrono::{DateTime, Utc};
352
353 let sunrise = "2023-06-21T05:30:00Z".parse::<DateTime<Utc>>().unwrap();
354 let transit = "2023-06-21T12:00:00Z".parse::<DateTime<Utc>>().unwrap();
355 let sunset = "2023-06-21T18:30:00Z".parse::<DateTime<Utc>>().unwrap();
356
357 let result = SunriseResult::RegularDay {
358 sunrise,
359 transit,
360 sunset,
361 };
362
363 assert!(result.is_regular_day());
364 assert!(!result.is_polar_day());
365 assert!(!result.is_polar_night());
366 assert_eq!(result.transit(), &transit);
367 assert_eq!(result.sunrise(), Some(&sunrise));
368 assert_eq!(result.sunset(), Some(&sunset));
369 }
370
371 #[test]
372 fn test_sunrise_result_polar_day() {
373 use chrono::{DateTime, Utc};
374
375 let transit = "2023-06-21T12:00:00Z".parse::<DateTime<Utc>>().unwrap();
376 let result = SunriseResult::AllDay { transit };
377
378 assert!(!result.is_regular_day());
379 assert!(result.is_polar_day());
380 assert!(!result.is_polar_night());
381 assert_eq!(result.transit(), &transit);
382 assert_eq!(result.sunrise(), None);
383 assert_eq!(result.sunset(), None);
384 }
385
386 #[test]
387 fn test_sunrise_result_polar_night() {
388 use chrono::{DateTime, Utc};
389
390 let transit = "2023-12-21T12:00:00Z".parse::<DateTime<Utc>>().unwrap();
391 let result = SunriseResult::AllNight { transit };
392
393 assert!(!result.is_regular_day());
394 assert!(!result.is_polar_day());
395 assert!(result.is_polar_night());
396 assert_eq!(result.transit(), &transit);
397 assert_eq!(result.sunrise(), None);
398 assert_eq!(result.sunset(), None);
399 }
400
401 #[test]
402 fn test_refraction_correction() {
403 let standard = RefractionCorrection::standard();
405 assert_eq!(standard.pressure(), 1013.25);
406 assert_eq!(standard.temperature(), 15.0);
407
408 let custom = RefractionCorrection::new(1000.0, 20.0).unwrap();
410 assert_eq!(custom.pressure(), 1000.0);
411 assert_eq!(custom.temperature(), 20.0);
412
413 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()); }
419}