solar_calendar_events/
lib.rs

1// Copyright (c) 2023-2024, Johan Thorén.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, Utc};
9use thiserror::Error;
10
11/// Represents errors that can occur when calculating the date and time of an annual solar event.
12#[derive(Error, Debug, Eq, PartialEq)]
13pub enum AnnualSolarEventError {
14    /// Error when unable to create a valid date with the given year, month, and day.
15    #[error("Unable to set the date: {0}")]
16    InvalidDateError(i32, u32, u32),
17
18    /// Error for an invalid month number.
19    #[error("Invalid month number: {0}")]
20    MonthOutOfRange(i32),
21
22    /// Error when unable to create a valid NaiveTime object with the given hour, minute, and
23    /// second.
24    #[error("Unable to create NaiveTime object from hour {0}, minute {1}, second {2}")]
25    NaiveTimeError(u32, u32, u32),
26
27    /// Error for failing to parse a floating-point number.
28    #[error("Unable to parse float: {0}")]
29    ParseFloatError(#[from] std::num::ParseFloatError),
30
31    /// Error when the specified year is out of range (1900–2100).
32    #[error("Year out of range: {0}, must be between 1900 and 2100")]
33    YearOutOfRange(i32),
34}
35
36/// Utility functions for internal calculations related to annual solar events.
37mod time_utils {
38    use super::{AnnualSolarEventError, JulianDayNumber};
39
40    /// Calculates the month and year based on intermediate values from Julian Day calculations.
41    ///
42    /// # Arguments
43    /// * `e` - The intermediate value `e` from Julian Day calculations.
44    /// * `c` - The intermediate value `c` from Julian Day calculations.
45    ///
46    /// # Returns
47    /// A tuple containing the month (u32) and year (i32).
48    ///
49    /// # Errors
50    /// Returns an error if the month is out of range (1-12).
51    pub fn calculate_month_and_year(e: i32, c: i32) -> Result<(u32, i32), AnnualSolarEventError> {
52        let signed_month = if e < 14 { e - 1 } else { e - 13 };
53        let month = match signed_month {
54            1..=12 => signed_month as u32,
55            _ => return Err(AnnualSolarEventError::MonthOutOfRange(signed_month)),
56        };
57        let year = if month > 2 { c - 4716 } else { c - 4715 };
58        Ok((month, year))
59    }
60
61    /// Calculates the day and fraction of the day from intermediate values in Julian Day
62    /// calculations.
63    ///
64    /// # Arguments
65    /// * `f` - The fractional part of the Julian Day.
66    /// * `b` - The intermediate value `b` from Julian Day calculations.
67    /// * `d` - The intermediate value `d` from Julian Day calculations.
68    /// * `e` - The intermediate value `e` from Julian Day calculations.
69    ///
70    /// # Returns
71    /// A tuple containing the day (u32) and the fractional part of the day (f64).
72    pub fn calculate_day(f: f64, b: i32, d: i32, e: i32) -> (u32, f64) {
73        let day_with_decimal: f64 =
74            f + (b as f64 - d as f64 - ((e as f64 * 30.600_1) as i32 as f64));
75        let day: u32 = day_with_decimal as u32;
76        let fraction_of_day: f64 = day_with_decimal - day as f64;
77        (day, fraction_of_day)
78    }
79
80    /// Calculates the hour, minute, second, and determines if the day should move forward based
81    /// on the fractional day.
82    ///
83    /// # Arguments
84    /// * `fraction_of_day` - The fractional part of the day.
85    ///
86    /// # Returns
87    /// A tuple containing the hour (u32), minute (u32), second (u32), and a boolean indicating if
88    /// the day should move forward.
89    ///
90    /// # Errors
91    /// Returns an error if the rounding or parsing fails.
92    pub fn calculate_hour_minute_second(
93        fraction_of_day: f64,
94    ) -> Result<(u32, u32, u32, bool), AnnualSolarEventError> {
95        let hour_with_decimal: f64 = 24.0 * fraction_of_day;
96        let mut hour: u32 = hour_with_decimal as u32;
97        let fraction_of_hour: f64 = (hour_with_decimal - hour as f64).to_five_decimals()?;
98        let minute_with_decimal: f64 = 60.0 * fraction_of_hour;
99        let mut minute: u32 = minute_with_decimal as u32;
100        let fraction_of_minute: f64 = 0.01 + minute_with_decimal - minute as f64;
101        let mut second: u32 = (60.0 * fraction_of_minute) as u32;
102        let mut move_day_forward = false;
103
104        if second == 60 {
105            minute += 1;
106            second = 0;
107        }
108        if minute == 60 {
109            hour += 1;
110            minute = 0;
111        }
112        if hour == 24 {
113            hour = 0;
114            move_day_forward = true;
115        }
116
117        Ok((hour, minute, second, move_day_forward))
118    }
119}
120
121/// Trait for working with Julian Day numbers and converting them to `DateTime<Utc>`.
122pub trait JulianDateTimeUtc {
123    /// Converts a Julian Day number to a `DateTime<Utc>`.
124    ///
125    /// # Arguments
126    /// * `julian_day` - The Julian Day number to convert.
127    ///
128    /// # Returns
129    /// A `DateTime<Utc>` representing the date and time of the Julian Day number.
130    ///
131    /// # Errors
132    /// Returns an error if the conversion fails due to invalid date or time components.
133    fn from_julian_day(julian_day: f64) -> Result<Self, AnnualSolarEventError>
134    where
135        Self: Sized;
136}
137
138/// Trait representing the characteristics of an annual solar event (e.g., Equinox or Solstice).
139pub trait AnnualSolarEvent {
140    /// Creates an instance of the solar event for a given year.
141    ///
142    /// # Arguments
143    /// * `year` - The year for which to calculate the solar event.
144    ///
145    /// # Returns
146    /// An instance of the solar event for the specified year.
147    ///
148    /// # Errors
149    /// Returns an error if the year is out of range (1900-2100) or if the date and time cannot be
150    /// calculated.
151    ///
152    /// # Example
153    /// ```
154    /// use solar_calendar_events::{AnnualSolarEvent, MarchEquinox};
155    ///
156    /// let event = MarchEquinox::for_year(2021).unwrap();
157    ///
158    /// assert_eq!(event.year(), 2021);
159    ///
160    /// let out_of_range_event = MarchEquinox::for_year(1899);
161    ///
162    /// assert!(out_of_range_event.is_err());
163    ///
164    /// assert_eq!(
165    ///     out_of_range_event.err(),
166    ///     Some(solar_calendar_events::AnnualSolarEventError::YearOutOfRange(1899))
167    /// );
168    /// ```
169    fn for_year(year: i32) -> Result<Self, AnnualSolarEventError>
170    where
171        Self: Sized;
172
173    /// Returns the date and time of the solar event as a `DateTime<Utc>`.
174    ///
175    /// # Returns
176    /// A `DateTime<Utc>` representing the date and time of the solar event.
177    fn date_time(&self) -> DateTime<Utc>;
178
179    /// Returns the Julian Day Number of the solar event.
180    ///
181    /// # Returns
182    /// The Julian Day Number of the solar event as a floating-point number.
183    fn julian_day(&self) -> f64;
184
185    /// Returns the year for which the solar event is calculated.
186    fn year(&self) -> i32;
187
188    /// Validates whether the given year is within the valid range (1900-2100).
189    ///
190    /// # Arguments
191    /// * `year` - The year to validate.
192    ///
193    /// # Returns
194    /// An `Ok(())` if the year is within the valid range, otherwise an error.
195    ///
196    /// # Errors
197    /// Returns an error if the year is out of range (1900-2100).
198    fn year_in_range(year: i32) -> Result<(), AnnualSolarEventError> {
199        if !(1_900..=2_100).contains(&year) {
200            return Err(AnnualSolarEventError::YearOutOfRange(year));
201        }
202        Ok(())
203    }
204
205    /// Returns constants needed to calculate the Julian Day Number for the solar event.
206    ///
207    /// # Returns
208    /// A tuple containing the base, factor, and coefficients for the Julian Day calculation.
209    fn julian_day_constants() -> (f64, f64, f64, f64, f64);
210
211    /// Calculates the Julian Day Number for the event in a given year.
212    ///
213    /// # Arguments
214    /// * `year` - The year for which to calculate the Julian Day Number.
215    ///
216    /// # Returns
217    /// The Julian Day Number as a floating-point number for the event in the specified year.
218    fn calculate_julian_day(year: i32) -> f64 {
219        let (base, factor, m2_coeff, m3_coeff, m4_coeff) = Self::julian_day_constants();
220
221        let m = (year as f64 - 2000.0) / 1000.0;
222        let m2 = m * m;
223        let m3 = m2 * m;
224        let m4 = m3 * m;
225
226        let f: f64 = base + factor * m + m2_coeff * m2 + m3_coeff * m3 + m4_coeff * m4;
227
228        match f.to_five_decimals() {
229            Ok(jd) => jd,
230            Err(_) => f,
231        }
232    }
233
234    /// Converts a Julian Day number to a `DateTime<Utc>`.
235    ///
236    /// # Arguments
237    /// * `jd` - The Julian Day number to convert.
238    ///
239    /// # Returns
240    /// A `DateTime<Utc>` representing the date and time of the Julian Day number.
241    ///
242    /// # Errors
243    /// Returns an error if the conversion fails due to invalid date or time components.
244    fn utc_from_julian(jd: f64) -> Result<DateTime<Utc>, AnnualSolarEventError> {
245        DateTime::<Utc>::from_julian_day(jd)
246    }
247}
248
249/// Trait for working with floating-point numbers to round them to five decimal places.
250trait JulianDayNumber {
251    /// Rounds the value to five decimal places.
252    ///
253    /// Returns an error if the rounding or parsing fails.
254    fn to_five_decimals(&self) -> Result<f64, AnnualSolarEventError>;
255}
256
257impl JulianDayNumber for f64 {
258    fn to_five_decimals(&self) -> Result<Self, AnnualSolarEventError> {
259        let s = format!("{:.5}", self);
260        s.parse().map_err(AnnualSolarEventError::ParseFloatError)
261    }
262}
263
264impl JulianDateTimeUtc for DateTime<Utc> {
265    /// Converts a Julian Day number to a `DateTime<Utc>`.
266    ///
267    /// Returns an error if the conversion fails due to invalid date or time components.
268    fn from_julian_day(jdn: f64) -> Result<DateTime<Utc>, AnnualSolarEventError> {
269        let j: f64 = jdn.to_five_decimals()? + 0.5;
270        let z: i32 = j as i32;
271        let f: f64 = j - z as f64;
272        let a: i32 = if z < 2_299_161 {
273            z
274        } else {
275            let alpha: i32 = ((z as f64 - 1_867_216.25) / 36_524.25) as i32;
276            z + 1 + (alpha - ((alpha as f64 / 4.0) as i32))
277        };
278        let b: i32 = a + 1_524;
279        let c: i32 = ((b as f64 - 122.1) / 365.25) as i32;
280        let d: i32 = (365.25 * c as f64) as i32;
281        let e: i32 = ((b - d) as f64 / 30.6) as i32;
282        let (month, year) = time_utils::calculate_month_and_year(e, c)?;
283        let (day, fraction_of_day) = time_utils::calculate_day(f, b, d, e);
284        let (hour, minute, second, move_day_forward) =
285            time_utils::calculate_hour_minute_second(fraction_of_day)?;
286
287        let naive_date = match NaiveDate::from_ymd_opt(year, month, day) {
288            Some(d) => {
289                if move_day_forward {
290                    d + TimeDelta::days(1)
291                } else {
292                    d
293                }
294            }
295            None => return Err(AnnualSolarEventError::InvalidDateError(year, month, day)),
296        };
297
298        let naive_time: NaiveTime = match NaiveTime::from_hms_opt(hour, minute, second) {
299            Some(t) => t,
300            None => return Err(AnnualSolarEventError::NaiveTimeError(hour, minute, second)),
301        };
302
303        Ok(DateTime::from_naive_utc_and_offset(
304            NaiveDateTime::new(naive_date, naive_time),
305            Utc,
306        ))
307    }
308}
309
310/// Represents the March Equinox for a specific year.
311#[derive(Debug)]
312pub struct MarchEquinox {
313    julian_day: f64,
314    date_time: DateTime<Utc>,
315}
316
317impl AnnualSolarEvent for MarchEquinox {
318    fn for_year(year: i32) -> Result<Self, AnnualSolarEventError> {
319        Self::year_in_range(year)?;
320        let julian_day = Self::calculate_julian_day(year);
321        let date_time = Self::utc_from_julian(julian_day)?;
322        Ok(Self {
323            julian_day,
324            date_time,
325        })
326    }
327
328    fn date_time(&self) -> DateTime<Utc> {
329        self.date_time
330    }
331
332    fn julian_day(&self) -> f64 {
333        self.julian_day
334    }
335
336    fn year(&self) -> i32 {
337        self.date_time.year()
338    }
339
340    fn julian_day_constants() -> (f64, f64, f64, f64, f64) {
341        (
342            2_451_623.809_84,
343            365_242.374_04,
344            0.051_69,
345            -0.004_11,
346            -0.000_57,
347        )
348    }
349}
350
351/// Represents the June Solstice for a specific year.
352#[derive(Debug)]
353pub struct JuneSolstice {
354    julian_day: f64,
355    date_time: DateTime<Utc>,
356}
357
358impl AnnualSolarEvent for JuneSolstice {
359    fn for_year(year: i32) -> Result<Self, AnnualSolarEventError> {
360        Self::year_in_range(year)?;
361        let julian_day = Self::calculate_julian_day(year);
362        let date_time = Self::utc_from_julian(julian_day)?;
363        Ok(Self {
364            julian_day,
365            date_time,
366        })
367    }
368
369    fn date_time(&self) -> DateTime<Utc> {
370        self.date_time
371    }
372
373    fn julian_day(&self) -> f64 {
374        self.julian_day
375    }
376
377    fn year(&self) -> i32 {
378        self.date_time.year()
379    }
380
381    fn julian_day_constants() -> (f64, f64, f64, f64, f64) {
382        (
383            2_451_716.567_67,
384            365_241.626_03,
385            0.003_25,
386            0.008_88,
387            0.000_30,
388        )
389    }
390}
391
392/// Represents the September Equinox for a specific year.
393#[derive(Debug)]
394pub struct SeptemberEquinox {
395    julian_day: f64,
396    date_time: DateTime<Utc>,
397}
398
399impl AnnualSolarEvent for SeptemberEquinox {
400    fn for_year(year: i32) -> Result<Self, AnnualSolarEventError> {
401        Self::year_in_range(year)?;
402        let julian_day = Self::calculate_julian_day(year);
403        let date_time = Self::utc_from_julian(julian_day)?;
404
405        Ok(Self {
406            julian_day,
407            date_time,
408        })
409    }
410
411    fn date_time(&self) -> DateTime<Utc> {
412        self.date_time
413    }
414
415    fn julian_day(&self) -> f64 {
416        self.julian_day
417    }
418
419    fn year(&self) -> i32 {
420        self.date_time.year()
421    }
422
423    fn julian_day_constants() -> (f64, f64, f64, f64, f64) {
424        (
425            2_451_810.217_15,
426            365_242.017_67,
427            0.003_37,
428            -0.000_78,
429            -0.115_75,
430        )
431    }
432}
433
434/// Represents the December Solstice for a specific year.
435#[derive(Debug)]
436pub struct DecemberSolstice {
437    julian_day: f64,
438    date_time: DateTime<Utc>,
439}
440
441impl AnnualSolarEvent for DecemberSolstice {
442    fn for_year(year: i32) -> Result<Self, AnnualSolarEventError> {
443        Self::year_in_range(year)?;
444        let julian_day = Self::calculate_julian_day(year);
445        let date_time = Self::utc_from_julian(julian_day)?;
446
447        Ok(Self {
448            julian_day,
449            date_time,
450        })
451    }
452
453    fn date_time(&self) -> DateTime<Utc> {
454        self.date_time
455    }
456
457    fn julian_day(&self) -> f64 {
458        self.julian_day
459    }
460
461    fn year(&self) -> i32 {
462        self.date_time.year()
463    }
464
465    fn julian_day_constants() -> (f64, f64, f64, f64, f64) {
466        (
467            2_451_900.059_52,
468            365_242.740_49,
469            0.000_32,
470            -0.062_23,
471            -0.008_23,
472        )
473    }
474}
475
476/// Contains all four solar events (March Equinox, June Solstice, September Equinox, and December
477/// Solstice) for a given year.
478#[derive(Debug)]
479pub struct AnnualSolarEvents {
480    march_equinox: MarchEquinox,
481    june_solstice: JuneSolstice,
482    september_equinox: SeptemberEquinox,
483    december_solstice: DecemberSolstice,
484}
485
486impl AnnualSolarEvents {
487    /// Creates a new `AnnualSolarEvents` instance for the specified year, which contains all four
488    /// solar events.
489    ///
490    /// Returns an error if the year is outside the valid range.
491    pub fn for_year(year: i32) -> Result<Self, AnnualSolarEventError> {
492        Ok(Self {
493            march_equinox: MarchEquinox::for_year(year)?,
494            june_solstice: JuneSolstice::for_year(year)?,
495            september_equinox: SeptemberEquinox::for_year(year)?,
496            december_solstice: DecemberSolstice::for_year(year)?,
497        })
498    }
499
500    /// Returns a reference to the March Equinox event.
501    pub fn march_equinox(&self) -> &MarchEquinox {
502        &self.march_equinox
503    }
504
505    /// Returns a reference to the June Solstice event.
506    pub fn june_solstice(&self) -> &JuneSolstice {
507        &self.june_solstice
508    }
509
510    /// Returns a reference to the September Equinox event.
511    pub fn september_equinox(&self) -> &SeptemberEquinox {
512        &self.september_equinox
513    }
514
515    /// Returns a reference to the December Solstice event.
516    pub fn december_solstice(&self) -> &DecemberSolstice {
517        &self.december_solstice
518    }
519
520    /// Returns the year of these solar events.
521    pub fn year(&self) -> i32 {
522        self.march_equinox.year()
523    }
524}