use chrono::{DateTime, Datelike, Timelike, Utc};
pub const JD2000: f64 = 2451545.0;
pub fn julian_date(datetime: DateTime<Utc>) -> f64 {
let year = datetime.year();
let month = datetime.month();
let day = datetime.day() as f64;
let mut y = year;
let mut m = month as i32;
if m <= 2 {
y -= 1;
m += 12;
}
let a = (y as f64 / 100.0).floor();
let b = 2.0 - a + (a / 4.0).floor();
let hour = datetime.hour() as f64;
let minute = datetime.minute() as f64;
let second = datetime.second() as f64;
let frac_day = (hour + (minute / 60.0) + (second / 3600.0)) / 24.0;
(365.25 * (y as f64 + 4716.0)).floor()
+ (30.6001 * ((m + 1) as f64)).floor()
+ day
+ frac_day
+ b
- 1524.5
}
pub fn j2000_days(datetime: DateTime<Utc>) -> f64 {
julian_date(datetime) - JD2000
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{TimeZone, Utc};
#[test]
fn test_calendar_reform_period_1582() {
let test_cases = [
(1582, 10, 1, 12, 0, 0, 2299147.0, "Oct 1, 1582"),
(1582, 10, 3, 12, 0, 0, 2299149.0, "Oct 3, 1582"),
(1582, 10, 4, 12, 0, 0, 2299150.0, "Last Julian day"),
(1582, 10, 5, 12, 0, 0, 2299151.0, "Invalid historical date"),
(1582, 10, 10, 12, 0, 0, 2299156.0, "Invalid historical date"),
(1582, 10, 14, 12, 0, 0, 2299160.0, "Invalid historical date"),
(1582, 10, 15, 12, 0, 0, 2299161.0, "First Gregorian day"),
(1582, 10, 16, 12, 0, 0, 2299162.0, "Day after reform"),
(1582, 10, 31, 12, 0, 0, 2299177.0, "Later Oct 1582"),
(1582, 11, 1, 12, 0, 0, 2299178.0, "Nov 1582"),
(1583, 1, 1, 12, 0, 0, 2299239.0, "Jan 1583"),
];
for (year, month, day, hour, min, sec, expected_jd, description) in test_cases {
let dt = Utc.with_ymd_and_hms(year, month, day, hour, min, sec).unwrap();
let calculated_jd = julian_date(dt);
let diff_seconds = (calculated_jd - expected_jd).abs() * 86400.0;
assert!(
diff_seconds < 0.001,
"Failed for {}: expected JD {}, got {}, diff = {:.6} seconds",
description, expected_jd, calculated_jd, diff_seconds
);
}
}
#[test]
fn test_calendar_transition_gap() {
let oct_4 = Utc.with_ymd_and_hms(1582, 10, 4, 12, 0, 0).unwrap();
let oct_15 = Utc.with_ymd_and_hms(1582, 10, 15, 12, 0, 0).unwrap();
let jd_4 = julian_date(oct_4);
let jd_15 = julian_date(oct_15);
let gap = jd_15 - jd_4;
assert!((gap - 11.0).abs() < 0.001,
"Calendar transition gap should be 11 days, got {:.3}", gap);
}
#[test]
fn test_j2000_epoch() {
let j2000 = Utc.with_ymd_and_hms(2000, 1, 1, 12, 0, 0).unwrap();
let jd = julian_date(j2000);
assert!((jd - JD2000).abs() < 1e-9,
"J2000.0 epoch should be exactly {}, got {}", JD2000, jd);
}
#[test]
fn test_j2000_days() {
let test_date = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let days = j2000_days(test_date);
let jd = julian_date(test_date);
assert!((jd - (JD2000 + days)).abs() < 1e-9,
"j2000_days calculation inconsistent with julian_date");
}
}