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