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    pub fn from_datetime<Tz: TimeZone>(
51        datetime: &chrono::DateTime<Tz>,
52        delta_t: f64,
53    ) -> Result<Self> {
54        // Convert the entire datetime to UTC for proper Julian Date calculation
55        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    /// Creates a new Julian date from year, month, day, hour, minute, and second in UTC.
68    ///
69    /// # Arguments
70    /// * `year` - Year (can be negative for BCE years)
71    /// * `month` - Month (1-12)
72    /// * `day` - Day of month (1-31)
73    /// * `hour` - Hour (0-23)
74    /// * `minute` - Minute (0-59)
75    /// * `second` - Second (0-59, can include fractional seconds)
76    /// * `delta_t` - ΔT in seconds (difference between TT and UT1)
77    ///
78    /// # Returns
79    /// Julian date or error if the date is invalid
80    ///
81    /// # Errors
82    /// Returns error if any date/time component is outside valid ranges (month 1-12, day 1-31, hour 0-23, minute 0-59, second 0-59.999).
83    ///
84    /// # Example
85    /// ```
86    /// # use solar_positioning::time::JulianDate;
87    /// let jd = JulianDate::from_utc(2023, 6, 21, 12, 0, 0.0, 69.0).unwrap();
88    /// assert!(jd.julian_date() > 2_460_000.0);
89    /// ```
90    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        // Validate input ranges
100        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    /// Creates a Julian date assuming ΔT = 0.
127    ///
128    /// # Arguments
129    /// * `year` - Year (can be negative for BCE years)
130    /// * `month` - Month (1-12)
131    /// * `day` - Day of month (1-31)
132    /// * `hour` - Hour (0-23)
133    /// * `minute` - Minute (0-59)
134    /// * `second` - Second (0-59, can include fractional seconds)
135    ///
136    /// # Returns
137    /// Returns `Ok(JulianDate)` with ΔT = 0 on success.
138    ///
139    /// # Errors
140    /// Returns error if the date/time components are outside valid ranges.
141    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    /// Gets the Julian Date (JD) value.
153    ///
154    /// # Returns
155    /// Julian Date referenced to UT1
156    #[must_use]
157    pub const fn julian_date(&self) -> f64 {
158        self.jd
159    }
160
161    /// Gets the ΔT value in seconds.
162    ///
163    /// # Returns
164    /// ΔT (Delta T) in seconds
165    #[must_use]
166    pub const fn delta_t(&self) -> f64 {
167        self.delta_t
168    }
169
170    /// Calculates the Julian Ephemeris Day (JDE).
171    ///
172    /// JDE = JD + ΔT/86400
173    ///
174    /// # Returns
175    /// Julian Ephemeris Day
176    #[must_use]
177    pub fn julian_ephemeris_day(&self) -> f64 {
178        self.jd + self.delta_t / SECONDS_PER_DAY
179    }
180
181    /// Calculates the Julian Century (JC) from J2000.0.
182    ///
183    /// JC = (JD - 2451545.0) / 36525
184    ///
185    /// # Returns
186    /// Julian centuries since J2000.0 epoch
187    #[must_use]
188    pub fn julian_century(&self) -> f64 {
189        (self.jd - J2000_JDN) / DAYS_PER_CENTURY
190    }
191
192    /// Calculates the Julian Ephemeris Century (JCE) from J2000.0.
193    ///
194    /// JCE = (JDE - 2451545.0) / 36525
195    ///
196    /// # Returns
197    /// Julian ephemeris centuries since J2000.0 epoch
198    #[must_use]
199    pub fn julian_ephemeris_century(&self) -> f64 {
200        (self.julian_ephemeris_day() - J2000_JDN) / DAYS_PER_CENTURY
201    }
202
203    /// Calculates the Julian Ephemeris Millennium (JME) from J2000.0.
204    ///
205    /// JME = JCE / 10
206    ///
207    /// # Returns
208    /// Julian ephemeris millennia since J2000.0 epoch
209    #[must_use]
210    pub fn julian_ephemeris_millennium(&self) -> f64 {
211        self.julian_ephemeris_century() / 10.0
212    }
213
214    /// Add days to the Julian date (like Java constructor: new `JulianDate(jd.julianDate()` + i - 1, 0))
215    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
223/// Calculates Julian Date from UTC date/time components.
224///
225/// This follows the algorithm from Reda & Andreas (2003), which is based on
226/// Meeus, "Astronomical Algorithms", 2nd edition.
227fn 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    // Adjust for January and February being treated as months 13 and 14 of previous year
239    if m < 3 {
240        y -= 1;
241        m += 12;
242    }
243
244    // Calculate fractional day
245    let d = f64::from(day) + (f64::from(hour) + (f64::from(minute) + second / 60.0) / 60.0) / 24.0;
246
247    // Basic Julian Date calculation
248    let mut jd =
249        floor(365.25 * (f64::from(y) + 4716.0)) + floor(30.6001 * f64::from(m + 1)) + d - 1524.5;
250
251    // Gregorian calendar correction (after October 15, 1582)
252    // JDN 2299161 corresponds to October 15, 1582
253    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
297/// ΔT (Delta T) estimation functions.
298///
299/// ΔT represents the difference between Terrestrial Time (TT) and Universal Time (UT1).
300/// These estimates are based on Espenak and Meeus polynomial fits updated in 2014.
301pub struct DeltaT;
302
303impl DeltaT {
304    /// Estimates ΔT for a given decimal year.
305    ///
306    /// Based on polynomial fits from Espenak & Meeus, updated 2014.
307    /// See: <https://www.eclipsewise.com/help/deltatpoly2014.html>
308    ///
309    /// # Arguments
310    /// * `decimal_year` - Year with fractional part (e.g., 2024.5 for mid-2024)
311    ///
312    /// # Returns
313    /// Estimated ΔT in seconds
314    ///
315    /// # Errors
316    /// Returns error for years outside the valid range (-500 to 3000 CE)
317    ///
318    /// # Example
319    /// ```
320    /// # use solar_positioning::time::DeltaT;
321    /// let delta_t = DeltaT::estimate(2024.0).unwrap();
322    /// assert!(delta_t > 60.0 && delta_t < 80.0); // Reasonable range for 2024
323    /// ```
324    #[allow(clippy::too_many_lines)] // Comprehensive polynomial fit across historical periods
325    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    /// Estimates ΔT from year and month.
441    ///
442    /// Calculates decimal year as: year + (month - 0.5) / 12
443    ///
444    /// # Arguments
445    /// * `year` - Year
446    /// * `month` - Month (1-12)
447    ///
448    /// # Returns
449    /// Returns estimated ΔT in seconds.
450    ///
451    /// # Errors
452    /// Returns error if month is outside the range 1-12.
453    ///
454    /// # Panics
455    /// This function does not panic.
456    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    /// Estimates ΔT from any date-like type.
466    ///
467    /// Convenience method that extracts the year and month from any chrono type
468    /// that implements `Datelike` (`DateTime`, `NaiveDateTime`, `NaiveDate`, etc.).
469    ///
470    /// # Arguments
471    /// * `date` - Any date-like type
472    ///
473    /// # Returns
474    /// Returns estimated ΔT in seconds.
475    ///
476    /// # Errors
477    /// Returns error if the date components are invalid.
478    ///
479    /// # Example
480    /// ```
481    /// # use solar_positioning::time::DeltaT;
482    /// # use chrono::{DateTime, FixedOffset, NaiveDate};
483    ///
484    /// // Works with DateTime
485    /// let datetime = "2024-06-21T12:00:00-07:00".parse::<DateTime<FixedOffset>>().unwrap();
486    /// let delta_t = DeltaT::estimate_from_date_like(datetime).unwrap();
487    /// assert!(delta_t > 60.0 && delta_t < 80.0);
488    ///
489    /// // Also works with NaiveDate
490    /// let date = NaiveDate::from_ymd_opt(2024, 6, 21).unwrap();
491    /// let delta_t2 = DeltaT::estimate_from_date_like(date).unwrap();
492    /// assert_eq!(delta_t, delta_t2);
493    #[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        // J2000.0 epoch should be exactly 2451545.0
511        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()); // Invalid month
529        assert!(JulianDate::from_utc(2024, 1, 32, 0, 0, 0.0, 0.0).is_err()); // Invalid day
530        assert!(JulianDate::from_utc(2024, 1, 1, 24, 0, 0.0, 0.0).is_err()); // Invalid hour
531        assert!(JulianDate::from_utc(2024, 1, 1, 0, 60, 0.0, 0.0).is_err()); // Invalid minute
532        assert!(JulianDate::from_utc(2024, 1, 1, 0, 0, 60.0, 0.0).is_err()); // Invalid second
533    }
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        // J2000.0 should give JC = 0
540        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; // seconds
548        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        // Test dates before and after Gregorian calendar adoption
559        // October 4, 1582 was followed by October 15, 1582
560        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        // The calendar dates are 11 days apart, but in Julian Day Numbers they should be 1 day apart
564        // because the 10-day gap was artificial
565        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        // Test that the Gregorian correction is applied correctly
572        // Dates after October 15, 1582 should have the correction
573        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        // Verify that both exist and the calculation doesn't panic
577        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        // Test some known ranges
584        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); // ΔT is generally increasing
590    }
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); // Negative in early 20th century
598        assert!(delta_t_1950 > 25.0 && delta_t_1950 < 35.0);
599    }
600
601    #[test]
602    fn test_delta_t_boundary_conditions() {
603        // Test edge cases
604        assert!(DeltaT::estimate(-500.0).is_ok());
605        assert!(DeltaT::estimate(3000.0).is_ok());
606        assert!(DeltaT::estimate(-501.0).is_ok()); // Should work for ancient dates
607        assert!(DeltaT::estimate(3001.0).is_err()); // Should fail beyond 3000
608    }
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(); // June = month 6, so (6-0.5)/12 ≈ 0.458
614
615        // Should be very close
616        assert!((delta_t - delta_t_decimal).abs() < 0.01);
617
618        // Test invalid month
619        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        // Test with DateTime<FixedOffset>
628        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        // Test with DateTime<Utc>
634        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        // Test with NaiveDate
638        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        // Test with NaiveDateTime
642        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        // Should all be identical since we only use year/month
646        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        // Should match estimate_from_date
651        let delta_t_date = DeltaT::estimate_from_date(2024, 6).unwrap();
652        assert_eq!(delta_t_fixed, delta_t_date);
653
654        // Verify reasonable range for 2024
655        assert!(delta_t_fixed > 60.0 && delta_t_fixed < 80.0);
656    }
657
658    #[test]
659    fn test_specific_julian_dates() {
660        // Test some well-known dates
661
662        // Unix epoch: 1970-01-01 00:00:00 UTC
663        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        // Y2K: 2000-01-01 00:00:00 UTC
667        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}