1#![allow(clippy::unreadable_literal)]
7#![allow(clippy::many_single_char_names)]
8
9use crate::math::{floor, polynomial};
10use crate::{Error, Result};
11use chrono::{Datelike, TimeZone, Timelike};
12
13const SECONDS_PER_DAY: f64 = 86_400.0;
15
16const J2000_JDN: f64 = 2_451_545.0;
18
19const DAYS_PER_CENTURY: f64 = 36_525.0;
21
22#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct JulianDate {
28 jd: f64,
30 delta_t: f64,
32}
33
34impl JulianDate {
35 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 let jd = calculate_julian_date(year, month, day, hour, minute, second);
120 Ok(Self { jd, delta_t })
121 }
122
123 pub fn from_utc_simple(
134 year: i32,
135 month: u32,
136 day: u32,
137 hour: u32,
138 minute: u32,
139 second: f64,
140 ) -> Result<Self> {
141 Self::from_utc(year, month, day, hour, minute, second, 0.0)
142 }
143
144 #[must_use]
149 pub const fn julian_date(&self) -> f64 {
150 self.jd
151 }
152
153 #[must_use]
158 pub const fn delta_t(&self) -> f64 {
159 self.delta_t
160 }
161
162 #[must_use]
169 pub fn julian_ephemeris_day(&self) -> f64 {
170 self.jd + self.delta_t / SECONDS_PER_DAY
171 }
172
173 #[must_use]
180 pub fn julian_century(&self) -> f64 {
181 (self.jd - J2000_JDN) / DAYS_PER_CENTURY
182 }
183
184 #[must_use]
191 pub fn julian_ephemeris_century(&self) -> f64 {
192 (self.julian_ephemeris_day() - J2000_JDN) / DAYS_PER_CENTURY
193 }
194
195 #[must_use]
202 pub fn julian_ephemeris_millennium(&self) -> f64 {
203 self.julian_ephemeris_century() / 10.0
204 }
205
206 pub(crate) fn add_days(self, days: f64) -> Self {
208 Self {
209 jd: self.jd + days,
210 delta_t: self.delta_t,
211 }
212 }
213}
214
215fn calculate_julian_date(
220 year: i32,
221 month: u32,
222 day: u32,
223 hour: u32,
224 minute: u32,
225 second: f64,
226) -> f64 {
227 let mut y = year;
228 let mut m = i32::try_from(month).expect("month should be valid i32");
229
230 if m < 3 {
232 y -= 1;
233 m += 12;
234 }
235
236 let d = f64::from(day) + (f64::from(hour) + (f64::from(minute) + second / 60.0) / 60.0) / 24.0;
238
239 let mut jd =
241 floor(365.25 * (f64::from(y) + 4716.0)) + floor(30.6001 * f64::from(m + 1)) + d - 1524.5;
242
243 if jd >= 2_299_161.0 {
246 let a = floor(f64::from(y) / 100.0);
247 let b = 2.0 - a + floor(a / 4.0);
248 jd += b;
249 }
250
251 jd
252}
253
254pub struct DeltaT;
259
260impl DeltaT {
261 #[allow(clippy::too_many_lines)] pub fn estimate(decimal_year: f64) -> Result<f64> {
283 let year = decimal_year;
284
285 if !year.is_finite() {
286 return Err(Error::invalid_datetime("year must be finite"));
287 }
288
289 let delta_t = if year < -500.0 {
290 let u = (year - 1820.0) / 100.0;
291 polynomial(&[-20.0, 0.0, 32.0], u)
292 } else if year < 500.0 {
293 let u = year / 100.0;
294 polynomial(
295 &[
296 10583.6,
297 -1014.41,
298 33.78311,
299 -5.952053,
300 -0.1798452,
301 0.022174192,
302 0.0090316521,
303 ],
304 u,
305 )
306 } else if year < 1600.0 {
307 let u = (year - 1000.0) / 100.0;
308 polynomial(
309 &[
310 1574.2,
311 -556.01,
312 71.23472,
313 0.319781,
314 -0.8503463,
315 -0.005050998,
316 0.0083572073,
317 ],
318 u,
319 )
320 } else if year < 1700.0 {
321 let t = year - 1600.0;
322 polynomial(&[120.0, -0.9808, -0.01532, 1.0 / 7129.0], t)
323 } else if year < 1800.0 {
324 let t = year - 1700.0;
325 polynomial(
326 &[8.83, 0.1603, -0.0059285, 0.00013336, -1.0 / 1_174_000.0],
327 t,
328 )
329 } else if year < 1860.0 {
330 let t = year - 1800.0;
331 polynomial(
332 &[
333 13.72,
334 -0.332447,
335 0.0068612,
336 0.0041116,
337 -0.00037436,
338 0.0000121272,
339 -0.0000001699,
340 0.000000000875,
341 ],
342 t,
343 )
344 } else if year < 1900.0 {
345 let t = year - 1860.0;
346 polynomial(
347 &[
348 7.62,
349 0.5737,
350 -0.251754,
351 0.01680668,
352 -0.0004473624,
353 1.0 / 233_174.0,
354 ],
355 t,
356 )
357 } else if year < 1920.0 {
358 let t = year - 1900.0;
359 polynomial(&[-2.79, 1.494119, -0.0598939, 0.0061966, -0.000197], t)
360 } else if year < 1941.0 {
361 let t = year - 1920.0;
362 polynomial(&[21.20, 0.84493, -0.076100, 0.0020936], t)
363 } else if year < 1961.0 {
364 let t = year - 1950.0;
365 polynomial(&[29.07, 0.407, -1.0 / 233.0, 1.0 / 2547.0], t)
366 } else if year < 1986.0 {
367 let t = year - 1975.0;
368 polynomial(&[45.45, 1.067, -1.0 / 260.0, -1.0 / 718.0], t)
369 } else if year < 2005.0 {
370 let t = year - 2000.0;
371 polynomial(
372 &[
373 63.86,
374 0.3345,
375 -0.060374,
376 0.0017275,
377 0.000651814,
378 0.00002373599,
379 ],
380 t,
381 )
382 } else if year < 2015.0 {
383 let t = year - 2005.0;
384 polynomial(&[64.69, 0.2930], t)
385 } else if year <= 3000.0 {
386 let t = year - 2015.0;
387 polynomial(&[67.62, 0.3645, 0.0039755], t)
388 } else {
389 return Err(Error::invalid_datetime(
390 "ΔT estimates not available beyond year 3000",
391 ));
392 };
393
394 Ok(delta_t)
395 }
396
397 pub fn estimate_from_date(year: i32, month: u32) -> Result<f64> {
411 if !(1..=12).contains(&month) {
412 return Err(Error::invalid_datetime("month must be between 1 and 12"));
413 }
414
415 let decimal_year = f64::from(year) + (f64::from(month) - 0.5) / 12.0;
416 Self::estimate(decimal_year)
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 const EPSILON: f64 = 1e-10;
425
426 #[test]
427 fn test_julian_date_creation() {
428 let jd = JulianDate::from_utc(2000, 1, 1, 12, 0, 0.0, 0.0).unwrap();
429
430 assert!((jd.julian_date() - J2000_JDN).abs() < EPSILON);
432 assert_eq!(jd.delta_t(), 0.0);
433 }
434
435 #[test]
436 fn test_julian_date_validation() {
437 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()); }
443
444 #[test]
445 fn test_julian_centuries() {
446 let jd = JulianDate::from_utc(2000, 1, 1, 12, 0, 0.0, 0.0).unwrap();
447
448 assert!(jd.julian_century().abs() < EPSILON);
450 assert!(jd.julian_ephemeris_century().abs() < EPSILON);
451 assert!(jd.julian_ephemeris_millennium().abs() < EPSILON);
452 }
453
454 #[test]
455 fn test_julian_ephemeris_day() {
456 let delta_t = 69.0; let jd = JulianDate::from_utc(2023, 6, 21, 12, 0, 0.0, delta_t).unwrap();
458
459 let jde = jd.julian_ephemeris_day();
460 let expected = jd.julian_date() + delta_t / SECONDS_PER_DAY;
461
462 assert!((jde - expected).abs() < EPSILON);
463 }
464
465 #[test]
466 fn test_gregorian_calendar_correction() {
467 let julian_date = JulianDate::from_utc(1582, 10, 4, 12, 0, 0.0, 0.0).unwrap();
470 let gregorian_date = JulianDate::from_utc(1582, 10, 15, 12, 0, 0.0, 0.0).unwrap();
471
472 let diff = gregorian_date.julian_date() - julian_date.julian_date();
475 assert!(
476 (diff - 1.0).abs() < 1e-6,
477 "Expected 1 day difference in JD, got {diff}"
478 );
479
480 let pre_gregorian = JulianDate::from_utc(1582, 10, 1, 12, 0, 0.0, 0.0).unwrap();
483 let post_gregorian = JulianDate::from_utc(1583, 1, 1, 12, 0, 0.0, 0.0).unwrap();
484
485 assert!(pre_gregorian.julian_date() > 2_000_000.0);
487 assert!(post_gregorian.julian_date() > pre_gregorian.julian_date());
488 }
489
490 #[test]
491 fn test_delta_t_modern_estimates() {
492 let delta_t_2000 = DeltaT::estimate(2000.0).unwrap();
494 let delta_t_2020 = DeltaT::estimate(2020.0).unwrap();
495
496 assert!(delta_t_2000 > 60.0 && delta_t_2000 < 70.0);
497 assert!(delta_t_2020 > 65.0 && delta_t_2020 < 75.0);
498 assert!(delta_t_2020 > delta_t_2000); }
500
501 #[test]
502 fn test_delta_t_historical_estimates() {
503 let delta_t_1900 = DeltaT::estimate(1900.0).unwrap();
504 let delta_t_1950 = DeltaT::estimate(1950.0).unwrap();
505
506 assert!(delta_t_1900 < 0.0); assert!(delta_t_1950 > 25.0 && delta_t_1950 < 35.0);
508 }
509
510 #[test]
511 fn test_delta_t_boundary_conditions() {
512 assert!(DeltaT::estimate(-500.0).is_ok());
514 assert!(DeltaT::estimate(3000.0).is_ok());
515 assert!(DeltaT::estimate(-501.0).is_ok()); assert!(DeltaT::estimate(3001.0).is_err()); }
518
519 #[test]
520 fn test_delta_t_from_date() {
521 let delta_t = DeltaT::estimate_from_date(2024, 6).unwrap();
522 let delta_t_decimal = DeltaT::estimate(2024.5 - 1.0 / 24.0).unwrap(); assert!((delta_t - delta_t_decimal).abs() < 0.01);
526
527 assert!(DeltaT::estimate_from_date(2024, 13).is_err());
529 assert!(DeltaT::estimate_from_date(2024, 0).is_err());
530 }
531
532 #[test]
533 fn test_specific_julian_dates() {
534 let unix_epoch = JulianDate::from_utc(1970, 1, 1, 0, 0, 0.0, 0.0).unwrap();
538 assert!((unix_epoch.julian_date() - 2_440_587.5).abs() < 1e-6);
539
540 let y2k = JulianDate::from_utc(2000, 1, 1, 0, 0, 0.0, 0.0).unwrap();
542 assert!((y2k.julian_date() - 2_451_544.5).abs() < 1e-6);
543 }
544}