1use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, Utc};
9use thiserror::Error;
10
11#[derive(Error, Debug, Eq, PartialEq)]
13pub enum AnnualSolarEventError {
14 #[error("Unable to set the date: {0}")]
16 InvalidDateError(i32, u32, u32),
17
18 #[error("Invalid month number: {0}")]
20 MonthOutOfRange(i32),
21
22 #[error("Unable to create NaiveTime object from hour {0}, minute {1}, second {2}")]
25 NaiveTimeError(u32, u32, u32),
26
27 #[error("Unable to parse float: {0}")]
29 ParseFloatError(#[from] std::num::ParseFloatError),
30
31 #[error("Year out of range: {0}, must be between 1900 and 2100")]
33 YearOutOfRange(i32),
34}
35
36mod time_utils {
38 use super::{AnnualSolarEventError, JulianDayNumber};
39
40 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 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 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
121pub trait JulianDateTimeUtc {
123 fn from_julian_day(julian_day: f64) -> Result<Self, AnnualSolarEventError>
134 where
135 Self: Sized;
136}
137
138pub trait AnnualSolarEvent {
140 fn for_year(year: i32) -> Result<Self, AnnualSolarEventError>
170 where
171 Self: Sized;
172
173 fn date_time(&self) -> DateTime<Utc>;
178
179 fn julian_day(&self) -> f64;
184
185 fn year(&self) -> i32;
187
188 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 fn julian_day_constants() -> (f64, f64, f64, f64, f64);
210
211 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 fn utc_from_julian(jd: f64) -> Result<DateTime<Utc>, AnnualSolarEventError> {
245 DateTime::<Utc>::from_julian_day(jd)
246 }
247}
248
249trait JulianDayNumber {
251 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 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#[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#[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#[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#[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#[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 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 pub fn march_equinox(&self) -> &MarchEquinox {
502 &self.march_equinox
503 }
504
505 pub fn june_solstice(&self) -> &JuneSolstice {
507 &self.june_solstice
508 }
509
510 pub fn september_equinox(&self) -> &SeptemberEquinox {
512 &self.september_equinox
513 }
514
515 pub fn december_solstice(&self) -> &DecemberSolstice {
517 &self.december_solstice
518 }
519
520 pub fn year(&self) -> i32 {
522 self.march_equinox.year()
523 }
524}