use super::{Lunisolar::Chinese, Lunisolar::Dangi, *};
fn greg(jdn: i64) -> (i64, i64, i64) {
let a = jdn + 32044;
let b = (4 * a + 3).div_euclid(146097);
let c = a - (146097 * b).div_euclid(4);
let d = (4 * c + 3).div_euclid(1461);
let e = c - (1461 * d).div_euclid(4);
let m = (5 * e + 2).div_euclid(153);
(
100 * b + d - 4800 + m.div_euclid(10),
m + 3 - 12 * m.div_euclid(10),
e - (153 * m + 2).div_euclid(5) + 1,
)
}
fn jdn_of(y: i64, m: i64, d: i64) -> i64 {
rd_from_gregorian(y, m, d) + RD_EPOCH_JDN
}
#[test]
fn chinese_new_year_dates_of_record() {
let cases = [
(1900, 1900, 1, 31),
(1949, 1949, 1, 29),
(1970, 1970, 2, 6),
(2000, 2000, 2, 5),
(2020, 2020, 1, 25),
(2023, 2023, 1, 22),
(2024, 2024, 2, 10),
(2025, 2025, 1, 29),
(2033, 2033, 1, 31),
(2100, 2100, 2, 9),
];
for (year, gy, gm, gd) in cases {
let jdn = to_jdn(Chinese, year, 1, 1, false).expect("new year exists");
assert_eq!(greg(jdn), (gy, gm, gd), "Chinese New Year {year}");
}
}
#[test]
fn chinese_round_trips_across_the_exact_range() {
let mut checked = 0u32;
let mut leap_months = 0u32;
let mut sweep = |from: (i64, i64, i64), to: (i64, i64, i64), stride: i64| {
let mut jdn = jdn_of(from.0, from.1, from.2);
let end = jdn_of(to.0, to.1, to.2);
while jdn <= end {
let d = from_jdn(Chinese, jdn).expect("in range");
assert!(
(1..=12).contains(&d.month),
"month {} at {:?}",
d.month,
greg(jdn)
);
assert!(
(1..=30).contains(&d.day),
"day {} at {:?}",
d.day,
greg(jdn)
);
if d.leap {
leap_months += 1;
}
let back = to_jdn(Chinese, d.year, d.month, d.day, d.leap).unwrap_or_else(|| {
panic!(
"no round-trip for {:?} at {:?}",
(d.year, d.month, d.day, d.leap),
greg(jdn)
)
});
assert_eq!(back, jdn, "round-trip at {:?}", greg(jdn));
checked += 1;
jdn += stride;
}
};
sweep((2010, 1, 1), (2025, 12, 31), 1);
sweep((1900, 1, 31), (2100, 12, 31), 13);
assert!(checked > 11_000, "only {checked} days checked");
let pct = f64::from(leap_months) * 100.0 / f64::from(checked);
assert!(
(2.0..5.0).contains(&pct),
"{pct:.1}% of sampled days were in leap months"
);
}
#[test]
fn chinese_leap_months_of_record() {
let cases = [
(2001, 4),
(2004, 2),
(2006, 7),
(2009, 5),
(2012, 4),
(2014, 9),
(2017, 6),
(2020, 4),
(2023, 2),
(2025, 6),
];
for (year, want) in cases {
let found = (1..=12)
.find(|&m| to_jdn(Chinese, year, m, 1, true).is_some())
.unwrap_or(0);
assert_eq!(found, want, "leap month of {year}");
}
for year in [2002, 2005, 2010, 2019, 2024] {
assert!(
(1..=12).all(|m| to_jdn(Chinese, year, m, 1, true).is_none()),
"{year} should have no leap month"
);
}
}
#[test]
fn dangi_diverges_from_chinese_where_the_meridian_matters() {
let mut differ = 0;
for year in 1900..=2050 {
let c = to_jdn(Chinese, year, 1, 1, false);
let k = to_jdn(Dangi, year, 1, 1, false);
assert!(c.is_some() && k.is_some(), "new year {year}");
if c != k {
differ += 1;
}
}
assert!(
(1..=12).contains(&differ),
"{differ} of 151 dangi new years differ from Chinese"
);
assert!(to_jdn(Dangi, 2050, 12, 29, false).is_some());
}
#[test]
fn extreme_years_are_defined_not_panicking() {
for cal in [Chinese, Dangi] {
for year in [-250_000, -5738, -4098, -2173, -180, 1, 1651, 250_000] {
let jdn = to_jdn(cal, year, 1, 1, false);
assert!(jdn.is_some(), "{year}-01-01 should convert");
let d = from_jdn(cal, jdn.unwrap()).expect("converts back");
assert!((1..=12).contains(&d.month) && (1..=30).contains(&d.day));
}
}
for year in [1, 1000, 1651, 1899, 1900, 2000, 2100, 2400] {
let jdn = to_jdn(Chinese, year, 1, 1, false).expect("converts");
let d = from_jdn(Chinese, jdn).expect("converts back");
assert_eq!((d.year, d.month, d.day), (year, 1, 1), "round-trip {year}");
}
}
#[test]
fn solar_longitude_hits_the_solstices_and_equinoxes() {
let march = solar_longitude(rd_from_gregorian(2024, 3, 20) as f64 + 3.0 / 24.0);
assert!(
!(0.5..=359.5).contains(&march),
"March equinox longitude {march}"
);
let december = solar_longitude(rd_from_gregorian(2024, 12, 21) as f64 + 10.0 / 24.0);
assert!(
(december - 270.0).abs() < 0.5,
"December solstice longitude {december}"
);
}
#[test]
fn new_moons_match_published_times() {
for (y, m, d, hour) in [
(2024, 1, 11, 11.0),
(2024, 7, 5, 22.6),
(2000, 1, 6, 18.2),
(1900, 1, 1, 13.9),
] {
let target = rd_from_gregorian(y, m, d) as f64 + hour / 24.0;
let moon = nth_new_moon(new_moon_index_before(target - 1.0) + 1);
assert!(
(moon - target).abs() < 1.0 / 24.0,
"new moon {y}-{m}-{d}: got {} expected {target}",
moon
);
}
}
#[test]
fn ephemeris_correction_is_continuous_and_plausible() {
let dt = |y| ephemeris_correction(rd_from_gregorian(y, 1, 1) as f64) * 86400.0;
assert!((dt(2000) - 63.8).abs() < 2.0, "ΔT(2000) = {}", dt(2000));
assert!((dt(1900) - (-2.8)).abs() < 3.0, "ΔT(1900) = {}", dt(1900));
assert!((dt(2020) - 69.0).abs() < 3.0, "ΔT(2020) = {}", dt(2020));
for boundary in [500, 1600, 1700, 1800, 1900, 1987, 2006, 2051, 2151] {
let before = dt(boundary - 1);
let after = dt(boundary + 1);
let scale = before.abs().max(after.abs()).max(10.0);
assert!(
(after - before).abs() < 0.15 * scale,
"ΔT jumps {} s across {boundary} (from {before} to {after})",
after - before
);
}
}