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