Skip to main content

solar_positioning/
time.rs

1//! Time-related calculations for solar positioning.
2//!
3//! This module provides Julian date calculations and ΔT (Delta T) estimation
4//! following the algorithms from NREL SPA and Espenak & Meeus.
5
6#![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
14/// Seconds per day (86,400)
15const SECONDS_PER_DAY: f64 = 86_400.0;
16
17/// Julian Day Number for J2000.0 epoch (2000-01-01 12:00:00 UTC)
18const J2000_JDN: f64 = 2_451_545.0;
19
20/// Days per Julian century
21const DAYS_PER_CENTURY: f64 = 36_525.0;
22
23/// Validates UTC date/time components against both field ranges and the calendar.
24pub(crate) fn validate_utc_components(
25    year: i32,
26    month: u32,
27    day: u32,
28    hour: u32,
29    minute: u32,
30    second: f64,
31) -> Result<()> {
32    if !(1..=12).contains(&month) {
33        return Err(Error::InvalidDateTime {
34            message: "month must be between 1 and 12",
35        });
36    }
37    if !(1..=31).contains(&day) {
38        return Err(Error::InvalidDateTime {
39            message: "day must be between 1 and 31",
40        });
41    }
42    if hour > 23 {
43        return Err(Error::InvalidDateTime {
44            message: "hour must be between 0 and 23",
45        });
46    }
47    if minute > 59 {
48        return Err(Error::InvalidDateTime {
49            message: "minute must be between 0 and 59",
50        });
51    }
52    if !(0.0..60.0).contains(&second) {
53        return Err(Error::InvalidDateTime {
54            message: "second must be between 0 and 59.999...",
55        });
56    }
57    if year == 1582 && month == 10 && (5..=14).contains(&day) {
58        return Err(Error::InvalidDateTime {
59            message: "dates 1582-10-05 through 1582-10-14 do not exist in Gregorian calendar",
60        });
61    }
62    if day > days_in_month(year, month) {
63        return Err(Error::InvalidDateTime {
64            message: "day is out of range for month",
65        });
66    }
67
68    Ok(())
69}
70
71/// Julian date representation for astronomical calculations.
72///
73/// Follows the SPA algorithm described in Reda & Andreas (2003).
74/// Supports both Julian Date (JD) and Julian Ephemeris Date (JDE) calculations.
75#[derive(Debug, Clone, Copy, PartialEq)]
76pub struct JulianDate {
77    /// Julian Date (JD) - referenced to UT1
78    jd: f64,
79    /// Delta T in seconds - difference between TT and UT1
80    delta_t: f64,
81}
82
83impl JulianDate {
84    /// Creates a new Julian date from a timezone-aware chrono `DateTime`.
85    ///
86    /// Converts datetime to UTC for proper Julian Date calculation.
87    ///
88    /// # Arguments
89    /// * `datetime` - Timezone-aware date and time
90    /// * `delta_t` - ΔT in seconds (difference between TT and UT1)
91    ///
92    /// # Returns
93    /// Returns `Ok(JulianDate)` on success.
94    ///
95    /// # Errors
96    /// Returns error if the date/time components are invalid (e.g., invalid month, day, hour).
97    #[cfg(feature = "chrono")]
98    #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
99    pub fn from_datetime<Tz: TimeZone>(
100        datetime: &chrono::DateTime<Tz>,
101        delta_t: f64,
102    ) -> Result<Self> {
103        // Convert the entire datetime to UTC for proper Julian Date calculation
104        let utc_datetime = datetime.with_timezone(&chrono::Utc);
105        Self::from_utc(
106            utc_datetime.year(),
107            utc_datetime.month(),
108            utc_datetime.day(),
109            utc_datetime.hour(),
110            utc_datetime.minute(),
111            f64::from(utc_datetime.second()) + f64::from(utc_datetime.nanosecond()) / 1e9,
112            delta_t,
113        )
114    }
115
116    /// Creates a new Julian date from year, month, day, hour, minute, and second in UTC.
117    ///
118    /// # Arguments
119    /// * `year` - Year (can be negative for BCE years)
120    /// * `month` - Month (1-12)
121    /// * `day` - Day of month (1-31)
122    /// * `hour` - Hour (0-23)
123    /// * `minute` - Minute (0-59)
124    /// * `second` - Second (0-59, can include fractional seconds)
125    /// * `delta_t` - ΔT in seconds (difference between TT and UT1)
126    ///
127    /// # Returns
128    /// Julian date or error if the date is invalid
129    ///
130    /// # Errors
131    /// Returns error if any date/time component is outside valid ranges (month 1-12, day 1-31,
132    /// hour 0-23, minute 0-59, second 0-59.999) or if `delta_t` is not finite.
133    ///
134    /// # Example
135    /// ```
136    /// # use solar_positioning::time::JulianDate;
137    /// let jd = JulianDate::from_utc(2023, 6, 21, 12, 0, 0.0, 69.0).unwrap();
138    /// assert!(jd.julian_date() > 2_460_000.0);
139    /// ```
140    pub fn from_utc(
141        year: i32,
142        month: u32,
143        day: u32,
144        hour: u32,
145        minute: u32,
146        second: f64,
147        delta_t: f64,
148    ) -> Result<Self> {
149        validate_utc_components(year, month, day, hour, minute, second)?;
150        if !delta_t.is_finite() {
151            return Err(Error::InvalidDateTime {
152                message: "delta_t must be finite",
153            });
154        }
155
156        let jd = calculate_julian_date(year, month, day, hour, minute, second);
157        Ok(Self { jd, delta_t })
158    }
159
160    /// Creates a Julian date assuming ΔT = 0.
161    ///
162    /// # Arguments
163    /// * `year` - Year (can be negative for BCE years)
164    /// * `month` - Month (1-12)
165    /// * `day` - Day of month (1-31)
166    /// * `hour` - Hour (0-23)
167    /// * `minute` - Minute (0-59)
168    /// * `second` - Second (0-59, can include fractional seconds)
169    ///
170    /// # Returns
171    /// Returns `Ok(JulianDate)` with ΔT = 0 on success.
172    ///
173    /// # Errors
174    /// Returns error if the date/time components are outside valid ranges.
175    pub fn from_utc_simple(
176        year: i32,
177        month: u32,
178        day: u32,
179        hour: u32,
180        minute: u32,
181        second: f64,
182    ) -> Result<Self> {
183        Self::from_utc(year, month, day, hour, minute, second, 0.0)
184    }
185
186    /// Gets the Julian Date (JD) value.
187    ///
188    /// # Returns
189    /// Julian Date referenced to UT1
190    #[must_use]
191    pub const fn julian_date(&self) -> f64 {
192        self.jd
193    }
194
195    /// Gets the ΔT value in seconds.
196    ///
197    /// # Returns
198    /// ΔT (Delta T) in seconds
199    #[must_use]
200    pub const fn delta_t(&self) -> f64 {
201        self.delta_t
202    }
203
204    /// Calculates the Julian Ephemeris Day (JDE).
205    ///
206    /// JDE = JD + ΔT/86400
207    ///
208    /// # Returns
209    /// Julian Ephemeris Day
210    #[must_use]
211    pub fn julian_ephemeris_day(&self) -> f64 {
212        self.jd + self.delta_t / SECONDS_PER_DAY
213    }
214
215    /// Calculates the Julian Century (JC) from J2000.0.
216    ///
217    /// JC = (JD - 2451545.0) / 36525
218    ///
219    /// # Returns
220    /// Julian centuries since J2000.0 epoch
221    #[must_use]
222    pub fn julian_century(&self) -> f64 {
223        (self.jd - J2000_JDN) / DAYS_PER_CENTURY
224    }
225
226    /// Calculates the Julian Ephemeris Century (JCE) from J2000.0.
227    ///
228    /// JCE = (JDE - 2451545.0) / 36525
229    ///
230    /// # Returns
231    /// Julian ephemeris centuries since J2000.0 epoch
232    #[must_use]
233    pub fn julian_ephemeris_century(&self) -> f64 {
234        (self.julian_ephemeris_day() - J2000_JDN) / DAYS_PER_CENTURY
235    }
236
237    /// Calculates the Julian Ephemeris Millennium (JME) from J2000.0.
238    ///
239    /// JME = JCE / 10
240    ///
241    /// # Returns
242    /// Julian ephemeris millennia since J2000.0 epoch
243    #[must_use]
244    pub fn julian_ephemeris_millennium(&self) -> f64 {
245        self.julian_ephemeris_century() / 10.0
246    }
247
248    /// Add days to the Julian date (like Java constructor: new `JulianDate(jd.julianDate()` + i - 1, 0))
249    pub(crate) fn add_days(self, days: f64) -> Self {
250        Self {
251            jd: self.jd + days,
252            delta_t: self.delta_t,
253        }
254    }
255}
256
257/// Calculates Julian Date from UTC date/time components.
258///
259/// This follows the algorithm from Reda & Andreas (2003), which is based on
260/// Meeus, "Astronomical Algorithms", 2nd edition.
261fn calculate_julian_date(
262    year: i32,
263    month: u32,
264    day: u32,
265    hour: u32,
266    minute: u32,
267    second: f64,
268) -> f64 {
269    let mut y = year;
270    let mut m = i32::try_from(month).expect("month should be valid i32");
271
272    // Adjust for January and February being treated as months 13 and 14 of previous year
273    if m < 3 {
274        y -= 1;
275        m += 12;
276    }
277
278    // Calculate fractional day
279    let d = f64::from(day) + (f64::from(hour) + (f64::from(minute) + second / 60.0) / 60.0) / 24.0;
280
281    // Basic Julian Date calculation
282    let mut jd =
283        floor(365.25 * (f64::from(y) + 4716.0)) + floor(30.6001 * f64::from(m + 1)) + d - 1524.5;
284
285    // Gregorian calendar correction (after October 15, 1582)
286    // JDN 2299161 corresponds to October 15, 1582
287    if jd >= 2_299_161.0 {
288        let a = floor(f64::from(y) / 100.0);
289        let b = 2.0 - a + floor(a / 4.0);
290        jd += b;
291    }
292
293    jd
294}
295
296fn days_in_month(year: i32, month: u32) -> u32 {
297    let is_leap_year = if year > 1582 || (year == 1582 && month > 10) {
298        (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
299    } else {
300        year % 4 == 0
301    };
302
303    match month {
304        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
305        4 | 6 | 9 | 11 => 30,
306        2 if is_leap_year => 29,
307        2 => 28,
308        _ => unreachable!("month already validated"),
309    }
310}
311
312/// ΔT (Delta T) estimation functions.
313///
314/// ΔT represents the difference between Terrestrial Time (TT) and Universal Time (UT1).
315/// These estimates are based on Espenak and Meeus polynomial fits updated in 2014.
316pub struct DeltaT;
317
318impl DeltaT {
319    /// Estimates ΔT for a given decimal year.
320    ///
321    /// Based on polynomial fits from Espenak & Meeus, updated 2014.
322    /// See: <https://www.eclipsewise.com/help/deltatpoly2014.html>
323    ///
324    /// # Arguments
325    /// * `decimal_year` - Year with fractional part (e.g., 2024.5 for mid-2024)
326    ///
327    /// # Returns
328    /// Estimated ΔT in seconds
329    ///
330    /// # Errors
331    /// Returns error for years outside the valid range (-500 to 3000 CE)
332    ///
333    /// # Example
334    /// ```
335    /// # use solar_positioning::time::DeltaT;
336    /// let delta_t = DeltaT::estimate(2024.0).unwrap();
337    /// assert!(delta_t > 60.0 && delta_t < 80.0); // Reasonable range for 2024
338    /// ```
339    #[allow(clippy::too_many_lines)] // Comprehensive polynomial fit across historical periods
340    pub fn estimate(decimal_year: f64) -> Result<f64> {
341        let year = decimal_year;
342
343        if !year.is_finite() {
344            return Err(Error::InvalidDateTime {
345                message: "year must be finite",
346            });
347        }
348
349        if year < -500.0 {
350            return Err(Error::InvalidDateTime {
351                message: "ΔT estimates not available before year -500",
352            });
353        }
354
355        let delta_t = if year < 500.0 {
356            let u = year / 100.0;
357            polynomial(
358                &[
359                    10583.6,
360                    -1014.41,
361                    33.78311,
362                    -5.952053,
363                    -0.1798452,
364                    0.022174192,
365                    0.0090316521,
366                ],
367                u,
368            )
369        } else if year < 1600.0 {
370            let u = (year - 1000.0) / 100.0;
371            polynomial(
372                &[
373                    1574.2,
374                    -556.01,
375                    71.23472,
376                    0.319781,
377                    -0.8503463,
378                    -0.005050998,
379                    0.0083572073,
380                ],
381                u,
382            )
383        } else if year < 1700.0 {
384            let t = year - 1600.0;
385            polynomial(&[120.0, -0.9808, -0.01532, 1.0 / 7129.0], t)
386        } else if year < 1800.0 {
387            let t = year - 1700.0;
388            polynomial(
389                &[8.83, 0.1603, -0.0059285, 0.00013336, -1.0 / 1_174_000.0],
390                t,
391            )
392        } else if year < 1860.0 {
393            let t = year - 1800.0;
394            polynomial(
395                &[
396                    13.72,
397                    -0.332447,
398                    0.0068612,
399                    0.0041116,
400                    -0.00037436,
401                    0.0000121272,
402                    -0.0000001699,
403                    0.000000000875,
404                ],
405                t,
406            )
407        } else if year < 1900.0 {
408            let t = year - 1860.0;
409            polynomial(
410                &[
411                    7.62,
412                    0.5737,
413                    -0.251754,
414                    0.01680668,
415                    -0.0004473624,
416                    1.0 / 233_174.0,
417                ],
418                t,
419            )
420        } else if year < 1920.0 {
421            let t = year - 1900.0;
422            polynomial(&[-2.79, 1.494119, -0.0598939, 0.0061966, -0.000197], t)
423        } else if year < 1941.0 {
424            let t = year - 1920.0;
425            polynomial(&[21.20, 0.84493, -0.076100, 0.0020936], t)
426        } else if year < 1961.0 {
427            let t = year - 1950.0;
428            polynomial(&[29.07, 0.407, -1.0 / 233.0, 1.0 / 2547.0], t)
429        } else if year < 1986.0 {
430            let t = year - 1975.0;
431            polynomial(&[45.45, 1.067, -1.0 / 260.0, -1.0 / 718.0], t)
432        } else if year < 2005.0 {
433            let t = year - 2000.0;
434            polynomial(
435                &[
436                    63.86,
437                    0.3345,
438                    -0.060374,
439                    0.0017275,
440                    0.000651814,
441                    0.00002373599,
442                ],
443                t,
444            )
445        } else if year < 2015.0 {
446            let t = year - 2005.0;
447            polynomial(&[64.69, 0.2930], t)
448        } else if year <= 3000.0 {
449            let t = year - 2015.0;
450            polynomial(&[67.62, 0.3645, 0.0039755], t)
451        } else {
452            return Err(Error::InvalidDateTime {
453                message: "ΔT estimates not available beyond year 3000",
454            });
455        };
456
457        Ok(delta_t)
458    }
459
460    /// Estimates ΔT from year and month.
461    ///
462    /// Calculates decimal year as: year + (month - 0.5) / 12
463    ///
464    /// # Arguments
465    /// * `year` - Year
466    /// * `month` - Month (1-12)
467    ///
468    /// # Returns
469    /// Returns estimated ΔT in seconds.
470    ///
471    /// # Errors
472    /// Returns error if month is outside the range 1-12.
473    ///
474    /// # Panics
475    /// This function does not panic.
476    pub fn estimate_from_date(year: i32, month: u32) -> Result<f64> {
477        if !(1..=12).contains(&month) {
478            return Err(Error::InvalidDateTime {
479                message: "month must be between 1 and 12",
480            });
481        }
482
483        let decimal_year = f64::from(year) + (f64::from(month) - 0.5) / 12.0;
484        Self::estimate(decimal_year)
485    }
486
487    /// Estimates ΔT from any date-like type.
488    ///
489    /// Convenience method that extracts the year and month from any chrono type
490    /// that implements `Datelike` (`DateTime`, `NaiveDateTime`, `NaiveDate`, etc.).
491    ///
492    /// # Arguments
493    /// * `date` - Any date-like type
494    ///
495    /// # Returns
496    /// Returns estimated ΔT in seconds.
497    ///
498    /// # Errors
499    /// Returns error if the date components are invalid.
500    ///
501    /// # Example
502    /// ```
503    /// # use solar_positioning::time::DeltaT;
504    /// # use chrono::{DateTime, FixedOffset, NaiveDate};
505    ///
506    /// // Works with DateTime
507    /// let datetime = "2024-06-21T12:00:00-07:00".parse::<DateTime<FixedOffset>>().unwrap();
508    /// let delta_t = DeltaT::estimate_from_date_like(datetime).unwrap();
509    /// assert!(delta_t > 60.0 && delta_t < 80.0);
510    ///
511    /// // Also works with NaiveDate
512    /// let date = NaiveDate::from_ymd_opt(2024, 6, 21).unwrap();
513    /// let delta_t2 = DeltaT::estimate_from_date_like(date).unwrap();
514    /// assert_eq!(delta_t, delta_t2);
515    #[cfg(feature = "chrono")]
516    #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
517    #[allow(clippy::needless_pass_by_value)]
518    pub fn estimate_from_date_like<D: Datelike>(date: D) -> Result<f64> {
519        Self::estimate_from_date(date.year(), date.month())
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526
527    const EPSILON: f64 = 1e-10;
528
529    #[test]
530    fn test_julian_date_creation() {
531        let jd = JulianDate::from_utc(2000, 1, 1, 12, 0, 0.0, 0.0).unwrap();
532
533        // J2000.0 epoch should be exactly 2451545.0
534        assert!((jd.julian_date() - J2000_JDN).abs() < EPSILON);
535        assert_eq!(jd.delta_t(), 0.0);
536    }
537
538    #[test]
539    fn test_julian_date_invalid_day_validation() {
540        assert!(JulianDate::from_utc(2024, 2, 30, 0, 0, 0.0, 0.0).is_err());
541        assert!(JulianDate::from_utc(2024, 2, 29, 0, 0, 0.0, 0.0).is_ok());
542        assert!(JulianDate::from_utc(1900, 2, 29, 0, 0, 0.0, 0.0).is_err());
543        assert!(JulianDate::from_utc(1500, 2, 29, 0, 0, 0.0, 0.0).is_ok());
544        assert!(JulianDate::from_utc(1582, 10, 10, 0, 0, 0.0, 0.0).is_err());
545        assert!(JulianDate::from_utc(1582, 10, 4, 0, 0, 0.0, 0.0).is_ok());
546        assert!(JulianDate::from_utc(1582, 10, 15, 0, 0, 0.0, 0.0).is_ok());
547    }
548
549    #[test]
550    fn test_julian_date_validation() {
551        assert!(JulianDate::from_utc(2024, 13, 1, 0, 0, 0.0, 0.0).is_err()); // Invalid month
552        assert!(JulianDate::from_utc(2024, 1, 32, 0, 0, 0.0, 0.0).is_err()); // Invalid day
553        assert!(JulianDate::from_utc(2024, 1, 1, 24, 0, 0.0, 0.0).is_err()); // Invalid hour
554        assert!(JulianDate::from_utc(2024, 1, 1, 0, 60, 0.0, 0.0).is_err()); // Invalid minute
555        assert!(JulianDate::from_utc(2024, 1, 1, 0, 0, 60.0, 0.0).is_err()); // Invalid second
556        assert!(JulianDate::from_utc(2024, 1, 1, 0, 0, 0.0, f64::NAN).is_err()); // Non-finite delta_t
557        assert!(JulianDate::from_utc(2024, 1, 1, 0, 0, 0.0, f64::INFINITY).is_err());
558        // Non-finite delta_t
559    }
560
561    #[test]
562    fn test_julian_centuries() {
563        let jd = JulianDate::from_utc(2000, 1, 1, 12, 0, 0.0, 0.0).unwrap();
564
565        // J2000.0 should give JC = 0
566        assert!(jd.julian_century().abs() < EPSILON);
567        assert!(jd.julian_ephemeris_century().abs() < EPSILON);
568        assert!(jd.julian_ephemeris_millennium().abs() < EPSILON);
569    }
570
571    #[test]
572    fn test_julian_ephemeris_day() {
573        let delta_t = 69.0; // seconds
574        let jd = JulianDate::from_utc(2023, 6, 21, 12, 0, 0.0, delta_t).unwrap();
575
576        let jde = jd.julian_ephemeris_day();
577        let expected = jd.julian_date() + delta_t / SECONDS_PER_DAY;
578
579        assert!((jde - expected).abs() < EPSILON);
580    }
581
582    #[test]
583    fn test_gregorian_calendar_correction() {
584        // Test dates before and after Gregorian calendar adoption
585        // October 4, 1582 was followed by October 15, 1582
586        let julian_date = JulianDate::from_utc(1582, 10, 4, 12, 0, 0.0, 0.0).unwrap();
587        let gregorian_date = JulianDate::from_utc(1582, 10, 15, 12, 0, 0.0, 0.0).unwrap();
588
589        // The calendar dates are 11 days apart, but in Julian Day Numbers they should be 1 day apart
590        // because the 10-day gap was artificial
591        let diff = gregorian_date.julian_date() - julian_date.julian_date();
592        assert!(
593            (diff - 1.0).abs() < 1e-6,
594            "Expected 1 day difference in JD, got {diff}"
595        );
596
597        // Test that the Gregorian correction is applied correctly
598        // Dates after October 15, 1582 should have the correction
599        let pre_gregorian = JulianDate::from_utc(1582, 10, 1, 12, 0, 0.0, 0.0).unwrap();
600        let post_gregorian = JulianDate::from_utc(1583, 1, 1, 12, 0, 0.0, 0.0).unwrap();
601
602        // Verify that both exist and the calculation doesn't panic
603        assert!(pre_gregorian.julian_date() > 2_000_000.0);
604        assert!(post_gregorian.julian_date() > pre_gregorian.julian_date());
605    }
606
607    #[test]
608    fn test_delta_t_modern_estimates() {
609        // Test some known ranges
610        let delta_t_2000 = DeltaT::estimate(2000.0).unwrap();
611        let delta_t_2020 = DeltaT::estimate(2020.0).unwrap();
612
613        assert!(delta_t_2000 > 60.0 && delta_t_2000 < 70.0);
614        assert!(delta_t_2020 > 65.0 && delta_t_2020 < 75.0);
615        assert!(delta_t_2020 > delta_t_2000); // ΔT is generally increasing
616    }
617
618    #[test]
619    fn test_delta_t_historical_estimates() {
620        let delta_t_1900 = DeltaT::estimate(1900.0).unwrap();
621        let delta_t_1950 = DeltaT::estimate(1950.0).unwrap();
622
623        assert!(delta_t_1900 < 0.0); // Negative in early 20th century
624        assert!(delta_t_1950 > 25.0 && delta_t_1950 < 35.0);
625    }
626
627    #[test]
628    fn test_delta_t_boundary_conditions() {
629        // Test edge cases
630        assert!(DeltaT::estimate(-500.0).is_ok());
631        assert!(DeltaT::estimate(3000.0).is_ok());
632        assert!(DeltaT::estimate(-501.0).is_err());
633        assert!(DeltaT::estimate(3001.0).is_err()); // Should fail beyond 3000
634    }
635
636    #[test]
637    fn test_delta_t_from_date() {
638        let delta_t = DeltaT::estimate_from_date(2024, 6).unwrap();
639        let delta_t_decimal = DeltaT::estimate(2024.5 - 1.0 / 24.0).unwrap(); // June = month 6, so (6-0.5)/12 ≈ 0.458
640
641        // Should be very close
642        assert!((delta_t - delta_t_decimal).abs() < 0.01);
643
644        // Test invalid month
645        assert!(DeltaT::estimate_from_date(2024, 13).is_err());
646        assert!(DeltaT::estimate_from_date(2024, 0).is_err());
647    }
648
649    #[test]
650    #[cfg(feature = "chrono")]
651    fn test_delta_t_from_date_like() {
652        use chrono::{DateTime, FixedOffset, NaiveDate, Utc};
653
654        // Test with DateTime<FixedOffset>
655        let datetime_fixed = "2024-06-15T12:00:00-07:00"
656            .parse::<DateTime<FixedOffset>>()
657            .unwrap();
658        let delta_t_fixed = DeltaT::estimate_from_date_like(datetime_fixed).unwrap();
659
660        // Test with DateTime<Utc>
661        let datetime_utc = "2024-06-15T19:00:00Z".parse::<DateTime<Utc>>().unwrap();
662        let delta_t_utc = DeltaT::estimate_from_date_like(datetime_utc).unwrap();
663
664        // Test with NaiveDate
665        let naive_date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
666        let delta_t_naive_date = DeltaT::estimate_from_date_like(naive_date).unwrap();
667
668        // Test with NaiveDateTime
669        let naive_datetime = naive_date.and_hms_opt(12, 0, 0).unwrap();
670        let delta_t_naive_datetime = DeltaT::estimate_from_date_like(naive_datetime).unwrap();
671
672        // Should all be identical since we only use year/month
673        assert_eq!(delta_t_fixed, delta_t_utc);
674        assert_eq!(delta_t_fixed, delta_t_naive_date);
675        assert_eq!(delta_t_fixed, delta_t_naive_datetime);
676
677        // Should match estimate_from_date
678        let delta_t_date = DeltaT::estimate_from_date(2024, 6).unwrap();
679        assert_eq!(delta_t_fixed, delta_t_date);
680
681        // Verify reasonable range for 2024
682        assert!(delta_t_fixed > 60.0 && delta_t_fixed < 80.0);
683    }
684
685    #[test]
686    fn test_specific_julian_dates() {
687        // Test some well-known dates
688
689        // Unix epoch: 1970-01-01 00:00:00 UTC
690        let unix_epoch = JulianDate::from_utc(1970, 1, 1, 0, 0, 0.0, 0.0).unwrap();
691        assert!((unix_epoch.julian_date() - 2_440_587.5).abs() < 1e-6);
692
693        // Y2K: 2000-01-01 00:00:00 UTC
694        let y2k = JulianDate::from_utc(2000, 1, 1, 0, 0, 0.0, 0.0).unwrap();
695        assert!((y2k.julian_date() - 2_451_544.5).abs() < 1e-6);
696    }
697}