1#![allow(clippy::unreadable_literal)]
7#![allow(clippy::many_single_char_names)]
8
9use crate::math::{floor, polynomial};
10use crate::{Error, Result};
11#[cfg(feature = "chrono")]
12use chrono::{Datelike, TimeZone, Timelike};
13
14const SECONDS_PER_DAY: f64 = 86_400.0;
16
17const J2000_JDN: f64 = 2_451_545.0;
19
20const DAYS_PER_CENTURY: f64 = 36_525.0;
22
23#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct JulianDate {
29 jd: f64,
31 delta_t: f64,
33}
34
35impl JulianDate {
36 #[cfg(feature = "chrono")]
50 pub fn from_datetime<Tz: TimeZone>(
51 datetime: &chrono::DateTime<Tz>,
52 delta_t: f64,
53 ) -> Result<Self> {
54 let utc_datetime = datetime.with_timezone(&chrono::Utc);
56 Self::from_utc(
57 utc_datetime.year(),
58 utc_datetime.month(),
59 utc_datetime.day(),
60 utc_datetime.hour(),
61 utc_datetime.minute(),
62 f64::from(utc_datetime.second()) + f64::from(utc_datetime.nanosecond()) / 1e9,
63 delta_t,
64 )
65 }
66
67 pub fn from_utc(
91 year: i32,
92 month: u32,
93 day: u32,
94 hour: u32,
95 minute: u32,
96 second: f64,
97 delta_t: f64,
98 ) -> Result<Self> {
99 if !(1..=12).contains(&month) {
101 return Err(Error::invalid_datetime("month must be between 1 and 12"));
102 }
103 if !(1..=31).contains(&day) {
104 return Err(Error::invalid_datetime("day must be between 1 and 31"));
105 }
106 if hour > 23 {
107 return Err(Error::invalid_datetime("hour must be between 0 and 23"));
108 }
109 if minute > 59 {
110 return Err(Error::invalid_datetime("minute must be between 0 and 59"));
111 }
112 if !(0.0..60.0).contains(&second) {
113 return Err(Error::invalid_datetime(
114 "second must be between 0 and 59.999...",
115 ));
116 }
117
118 if day > days_in_month(year, month, day)? {
119 return Err(Error::invalid_datetime("day is out of range for month"));
120 }
121
122 let jd = calculate_julian_date(year, month, day, hour, minute, second);
123 Ok(Self { jd, delta_t })
124 }
125
126 pub fn from_utc_simple(
142 year: i32,
143 month: u32,
144 day: u32,
145 hour: u32,
146 minute: u32,
147 second: f64,
148 ) -> Result<Self> {
149 Self::from_utc(year, month, day, hour, minute, second, 0.0)
150 }
151
152 #[must_use]
157 pub const fn julian_date(&self) -> f64 {
158 self.jd
159 }
160
161 #[must_use]
166 pub const fn delta_t(&self) -> f64 {
167 self.delta_t
168 }
169
170 #[must_use]
177 pub fn julian_ephemeris_day(&self) -> f64 {
178 self.jd + self.delta_t / SECONDS_PER_DAY
179 }
180
181 #[must_use]
188 pub fn julian_century(&self) -> f64 {
189 (self.jd - J2000_JDN) / DAYS_PER_CENTURY
190 }
191
192 #[must_use]
199 pub fn julian_ephemeris_century(&self) -> f64 {
200 (self.julian_ephemeris_day() - J2000_JDN) / DAYS_PER_CENTURY
201 }
202
203 #[must_use]
210 pub fn julian_ephemeris_millennium(&self) -> f64 {
211 self.julian_ephemeris_century() / 10.0
212 }
213
214 pub(crate) fn add_days(self, days: f64) -> Self {
216 Self {
217 jd: self.jd + days,
218 delta_t: self.delta_t,
219 }
220 }
221}
222
223fn calculate_julian_date(
228 year: i32,
229 month: u32,
230 day: u32,
231 hour: u32,
232 minute: u32,
233 second: f64,
234) -> f64 {
235 let mut y = year;
236 let mut m = i32::try_from(month).expect("month should be valid i32");
237
238 if m < 3 {
240 y -= 1;
241 m += 12;
242 }
243
244 let d = f64::from(day) + (f64::from(hour) + (f64::from(minute) + second / 60.0) / 60.0) / 24.0;
246
247 let mut jd =
249 floor(365.25 * (f64::from(y) + 4716.0)) + floor(30.6001 * f64::from(m + 1)) + d - 1524.5;
250
251 if jd >= 2_299_161.0 {
254 let a = floor(f64::from(y) / 100.0);
255 let b = 2.0 - a + floor(a / 4.0);
256 jd += b;
257 }
258
259 jd
260}
261
262const fn is_gregorian_date(year: i32, month: u32, day: u32) -> bool {
263 year > 1582 || (year == 1582 && (month > 10 || (month == 10 && day >= 15)))
264}
265
266const fn is_leap_year(year: i32, is_gregorian: bool) -> bool {
267 if is_gregorian {
268 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
269 } else {
270 year % 4 == 0
271 }
272}
273
274fn days_in_month(year: i32, month: u32, day: u32) -> Result<u32> {
275 if year == 1582 && month == 10 && (5..=14).contains(&day) {
276 return Err(Error::invalid_datetime(
277 "dates 1582-10-05 through 1582-10-14 do not exist in Gregorian calendar",
278 ));
279 }
280
281 let is_gregorian = is_gregorian_date(year, month, day);
282 let days = match month {
283 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
284 4 | 6 | 9 | 11 => 30,
285 2 => {
286 if is_leap_year(year, is_gregorian) {
287 29
288 } else {
289 28
290 }
291 }
292 _ => unreachable!("month already validated"),
293 };
294 Ok(days)
295}
296
297pub struct DeltaT;
302
303impl DeltaT {
304 #[allow(clippy::too_many_lines)] pub fn estimate(decimal_year: f64) -> Result<f64> {
326 let year = decimal_year;
327
328 if !year.is_finite() {
329 return Err(Error::invalid_datetime("year must be finite"));
330 }
331
332 let delta_t = if year < -500.0 {
333 let u = (year - 1820.0) / 100.0;
334 polynomial(&[-20.0, 0.0, 32.0], u)
335 } else if year < 500.0 {
336 let u = year / 100.0;
337 polynomial(
338 &[
339 10583.6,
340 -1014.41,
341 33.78311,
342 -5.952053,
343 -0.1798452,
344 0.022174192,
345 0.0090316521,
346 ],
347 u,
348 )
349 } else if year < 1600.0 {
350 let u = (year - 1000.0) / 100.0;
351 polynomial(
352 &[
353 1574.2,
354 -556.01,
355 71.23472,
356 0.319781,
357 -0.8503463,
358 -0.005050998,
359 0.0083572073,
360 ],
361 u,
362 )
363 } else if year < 1700.0 {
364 let t = year - 1600.0;
365 polynomial(&[120.0, -0.9808, -0.01532, 1.0 / 7129.0], t)
366 } else if year < 1800.0 {
367 let t = year - 1700.0;
368 polynomial(
369 &[8.83, 0.1603, -0.0059285, 0.00013336, -1.0 / 1_174_000.0],
370 t,
371 )
372 } else if year < 1860.0 {
373 let t = year - 1800.0;
374 polynomial(
375 &[
376 13.72,
377 -0.332447,
378 0.0068612,
379 0.0041116,
380 -0.00037436,
381 0.0000121272,
382 -0.0000001699,
383 0.000000000875,
384 ],
385 t,
386 )
387 } else if year < 1900.0 {
388 let t = year - 1860.0;
389 polynomial(
390 &[
391 7.62,
392 0.5737,
393 -0.251754,
394 0.01680668,
395 -0.0004473624,
396 1.0 / 233_174.0,
397 ],
398 t,
399 )
400 } else if year < 1920.0 {
401 let t = year - 1900.0;
402 polynomial(&[-2.79, 1.494119, -0.0598939, 0.0061966, -0.000197], t)
403 } else if year < 1941.0 {
404 let t = year - 1920.0;
405 polynomial(&[21.20, 0.84493, -0.076100, 0.0020936], t)
406 } else if year < 1961.0 {
407 let t = year - 1950.0;
408 polynomial(&[29.07, 0.407, -1.0 / 233.0, 1.0 / 2547.0], t)
409 } else if year < 1986.0 {
410 let t = year - 1975.0;
411 polynomial(&[45.45, 1.067, -1.0 / 260.0, -1.0 / 718.0], t)
412 } else if year < 2005.0 {
413 let t = year - 2000.0;
414 polynomial(
415 &[
416 63.86,
417 0.3345,
418 -0.060374,
419 0.0017275,
420 0.000651814,
421 0.00002373599,
422 ],
423 t,
424 )
425 } else if year < 2015.0 {
426 let t = year - 2005.0;
427 polynomial(&[64.69, 0.2930], t)
428 } else if year <= 3000.0 {
429 let t = year - 2015.0;
430 polynomial(&[67.62, 0.3645, 0.0039755], t)
431 } else {
432 return Err(Error::invalid_datetime(
433 "ΔT estimates not available beyond year 3000",
434 ));
435 };
436
437 Ok(delta_t)
438 }
439
440 pub fn estimate_from_date(year: i32, month: u32) -> Result<f64> {
457 if !(1..=12).contains(&month) {
458 return Err(Error::invalid_datetime("month must be between 1 and 12"));
459 }
460
461 let decimal_year = f64::from(year) + (f64::from(month) - 0.5) / 12.0;
462 Self::estimate(decimal_year)
463 }
464
465 #[cfg(feature = "chrono")]
494 #[allow(clippy::needless_pass_by_value)]
495 pub fn estimate_from_date_like<D: Datelike>(date: D) -> Result<f64> {
496 Self::estimate_from_date(date.year(), date.month())
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 const EPSILON: f64 = 1e-10;
505
506 #[test]
507 fn test_julian_date_creation() {
508 let jd = JulianDate::from_utc(2000, 1, 1, 12, 0, 0.0, 0.0).unwrap();
509
510 assert!((jd.julian_date() - J2000_JDN).abs() < EPSILON);
512 assert_eq!(jd.delta_t(), 0.0);
513 }
514
515 #[test]
516 fn test_julian_date_invalid_day_validation() {
517 assert!(JulianDate::from_utc(2024, 2, 30, 0, 0, 0.0, 0.0).is_err());
518 assert!(JulianDate::from_utc(2024, 2, 29, 0, 0, 0.0, 0.0).is_ok());
519 assert!(JulianDate::from_utc(1900, 2, 29, 0, 0, 0.0, 0.0).is_err());
520 assert!(JulianDate::from_utc(1500, 2, 29, 0, 0, 0.0, 0.0).is_ok());
521 assert!(JulianDate::from_utc(1582, 10, 10, 0, 0, 0.0, 0.0).is_err());
522 assert!(JulianDate::from_utc(1582, 10, 4, 0, 0, 0.0, 0.0).is_ok());
523 assert!(JulianDate::from_utc(1582, 10, 15, 0, 0, 0.0, 0.0).is_ok());
524 }
525
526 #[test]
527 fn test_julian_date_validation() {
528 assert!(JulianDate::from_utc(2024, 13, 1, 0, 0, 0.0, 0.0).is_err()); assert!(JulianDate::from_utc(2024, 1, 32, 0, 0, 0.0, 0.0).is_err()); assert!(JulianDate::from_utc(2024, 1, 1, 24, 0, 0.0, 0.0).is_err()); assert!(JulianDate::from_utc(2024, 1, 1, 0, 60, 0.0, 0.0).is_err()); assert!(JulianDate::from_utc(2024, 1, 1, 0, 0, 60.0, 0.0).is_err()); }
534
535 #[test]
536 fn test_julian_centuries() {
537 let jd = JulianDate::from_utc(2000, 1, 1, 12, 0, 0.0, 0.0).unwrap();
538
539 assert!(jd.julian_century().abs() < EPSILON);
541 assert!(jd.julian_ephemeris_century().abs() < EPSILON);
542 assert!(jd.julian_ephemeris_millennium().abs() < EPSILON);
543 }
544
545 #[test]
546 fn test_julian_ephemeris_day() {
547 let delta_t = 69.0; let jd = JulianDate::from_utc(2023, 6, 21, 12, 0, 0.0, delta_t).unwrap();
549
550 let jde = jd.julian_ephemeris_day();
551 let expected = jd.julian_date() + delta_t / SECONDS_PER_DAY;
552
553 assert!((jde - expected).abs() < EPSILON);
554 }
555
556 #[test]
557 fn test_gregorian_calendar_correction() {
558 let julian_date = JulianDate::from_utc(1582, 10, 4, 12, 0, 0.0, 0.0).unwrap();
561 let gregorian_date = JulianDate::from_utc(1582, 10, 15, 12, 0, 0.0, 0.0).unwrap();
562
563 let diff = gregorian_date.julian_date() - julian_date.julian_date();
566 assert!(
567 (diff - 1.0).abs() < 1e-6,
568 "Expected 1 day difference in JD, got {diff}"
569 );
570
571 let pre_gregorian = JulianDate::from_utc(1582, 10, 1, 12, 0, 0.0, 0.0).unwrap();
574 let post_gregorian = JulianDate::from_utc(1583, 1, 1, 12, 0, 0.0, 0.0).unwrap();
575
576 assert!(pre_gregorian.julian_date() > 2_000_000.0);
578 assert!(post_gregorian.julian_date() > pre_gregorian.julian_date());
579 }
580
581 #[test]
582 fn test_delta_t_modern_estimates() {
583 let delta_t_2000 = DeltaT::estimate(2000.0).unwrap();
585 let delta_t_2020 = DeltaT::estimate(2020.0).unwrap();
586
587 assert!(delta_t_2000 > 60.0 && delta_t_2000 < 70.0);
588 assert!(delta_t_2020 > 65.0 && delta_t_2020 < 75.0);
589 assert!(delta_t_2020 > delta_t_2000); }
591
592 #[test]
593 fn test_delta_t_historical_estimates() {
594 let delta_t_1900 = DeltaT::estimate(1900.0).unwrap();
595 let delta_t_1950 = DeltaT::estimate(1950.0).unwrap();
596
597 assert!(delta_t_1900 < 0.0); assert!(delta_t_1950 > 25.0 && delta_t_1950 < 35.0);
599 }
600
601 #[test]
602 fn test_delta_t_boundary_conditions() {
603 assert!(DeltaT::estimate(-500.0).is_ok());
605 assert!(DeltaT::estimate(3000.0).is_ok());
606 assert!(DeltaT::estimate(-501.0).is_ok()); assert!(DeltaT::estimate(3001.0).is_err()); }
609
610 #[test]
611 fn test_delta_t_from_date() {
612 let delta_t = DeltaT::estimate_from_date(2024, 6).unwrap();
613 let delta_t_decimal = DeltaT::estimate(2024.5 - 1.0 / 24.0).unwrap(); assert!((delta_t - delta_t_decimal).abs() < 0.01);
617
618 assert!(DeltaT::estimate_from_date(2024, 13).is_err());
620 assert!(DeltaT::estimate_from_date(2024, 0).is_err());
621 }
622
623 #[test]
624 fn test_delta_t_from_date_like() {
625 use chrono::{DateTime, FixedOffset, NaiveDate, Utc};
626
627 let datetime_fixed = "2024-06-15T12:00:00-07:00"
629 .parse::<DateTime<FixedOffset>>()
630 .unwrap();
631 let delta_t_fixed = DeltaT::estimate_from_date_like(datetime_fixed).unwrap();
632
633 let datetime_utc = "2024-06-15T19:00:00Z".parse::<DateTime<Utc>>().unwrap();
635 let delta_t_utc = DeltaT::estimate_from_date_like(datetime_utc).unwrap();
636
637 let naive_date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
639 let delta_t_naive_date = DeltaT::estimate_from_date_like(naive_date).unwrap();
640
641 let naive_datetime = naive_date.and_hms_opt(12, 0, 0).unwrap();
643 let delta_t_naive_datetime = DeltaT::estimate_from_date_like(naive_datetime).unwrap();
644
645 assert_eq!(delta_t_fixed, delta_t_utc);
647 assert_eq!(delta_t_fixed, delta_t_naive_date);
648 assert_eq!(delta_t_fixed, delta_t_naive_datetime);
649
650 let delta_t_date = DeltaT::estimate_from_date(2024, 6).unwrap();
652 assert_eq!(delta_t_fixed, delta_t_date);
653
654 assert!(delta_t_fixed > 60.0 && delta_t_fixed < 80.0);
656 }
657
658 #[test]
659 fn test_specific_julian_dates() {
660 let unix_epoch = JulianDate::from_utc(1970, 1, 1, 0, 0, 0.0, 0.0).unwrap();
664 assert!((unix_epoch.julian_date() - 2_440_587.5).abs() < 1e-6);
665
666 let y2k = JulianDate::from_utc(2000, 1, 1, 0, 0, 0.0, 0.0).unwrap();
668 assert!((y2k.julian_date() - 2_451_544.5).abs() < 1e-6);
669 }
670}