use crate::duration::Duration;
use crate::naive::NaiveDate;
use crate::traits::Datelike;
use std::cell::RefCell;
use std::collections::HashMap;
const TROPICAL_YEAR_DAYS: f64 = 365.2422;
const SYNODIC_MONTH_DAYS: f64 = 29.530588861;
fn is_gregorian_leap_year(year: i16) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
type MonthEntry = (u32, bool, NaiveDate);
fn solar_longitude_deg(jde: f64) -> f64 {
let (ecl_point, _sun_earth_dist_au) = astro::sun::geocent_ecl_pos(jde);
let deg = ecl_point.long.to_degrees() % 360.0;
if deg < 0.0 {
deg + 360.0
} else {
deg
}
}
fn signed_longitude_distance_deg(a: f64, target: f64) -> f64 {
let d = (a - target) % 360.0;
if d <= -180.0 {
d + 360.0
} else if d > 180.0 {
d - 360.0
} else {
d
}
}
fn bisect_solar_longitude(seed_jde: f64, target_deg: f64, window_days: f64) -> f64 {
let f = |jde: f64| signed_longitude_distance_deg(solar_longitude_deg(jde), target_deg);
let step = window_days / 16.0;
let mut lo = seed_jde - window_days;
let mut f_lo = f(lo);
let mut hi = seed_jde + window_days;
let mut t = lo;
while t <= seed_jde + window_days {
let f_t = f(t);
if f_lo * f_t < 0.0 {
hi = t;
break;
}
lo = t;
f_lo = f_t;
t += step;
}
let mut a = lo;
let mut b = hi;
let mut f_a = f(a);
for _ in 0..60 {
let mid = (a + b) / 2.0;
let f_mid = f(mid);
if f_a * f_mid <= 0.0 {
b = mid;
} else {
a = mid;
f_a = f_mid;
}
}
(a + b) / 2.0
}
fn solar_term_jde(year: i32, seed_month: u32, seed_day: f64, target_deg: f64) -> Option<f64> {
let year_i16 = i16::try_from(year).ok()?;
let month_u8 = u8::try_from(seed_month).ok()?;
let seed_date = astro::time::Date {
year: year_i16,
month: month_u8,
decimal_day: seed_day,
cal_type: astro::time::CalType::Gregorian,
};
let seed_jd = astro::time::julian_day(&seed_date);
let delta_t = astro::time::delta_t(year, month_u8);
let seed_jde = astro::time::julian_ephemeris_day(seed_jd, delta_t);
Some(bisect_solar_longitude(seed_jde, target_deg, 8.0))
}
fn winter_solstice_jde(year: i32) -> Option<f64> {
solar_term_jde(year, 12, 21.0, 270.0)
}
fn astro_date_from_jde(jde: f64) -> Option<astro::time::Date> {
let (year, month, decimal_day) = astro::time::date_frm_julian_day(jde).ok()?;
Some(astro::time::Date { year, month, decimal_day, cal_type: astro::time::CalType::Gregorian })
}
fn nearest_new_moon_jde(seed_jde: f64) -> Option<f64> {
let seed_date = astro_date_from_jde(seed_jde)?;
let decimal_year = astro::time::decimal_year(&seed_date);
let k_target = (12.3685 * (decimal_year - 2000.0)).round();
let k_biased = if k_target >= 0.0 { k_target + 0.1 } else { k_target - 0.1 };
let synth_decimal_year = 2000.0 + k_biased / 12.3685;
let year = synth_decimal_year.floor();
let frac = synth_decimal_year - year;
let year_i16 = i16::try_from(year as i64).ok()?;
let days_in_year = if is_gregorian_leap_year(year_i16) { 366.0 } else { 365.0 };
let synth_date = astro::time::Date {
year: year_i16,
month: 1,
decimal_day: frac * days_in_year,
cal_type: astro::time::CalType::Gregorian,
};
Some(astro::lunar::time_of_phase(&synth_date, &astro::lunar::Phase::New))
}
fn new_moon_on_or_before(target_jde: f64) -> Option<f64> {
let mut guess = target_jde;
let mut nm_jde = nearest_new_moon_jde(guess)?;
let mut guard = 0;
while nm_jde > target_jde {
guess = nm_jde - SYNODIC_MONTH_DAYS;
nm_jde = nearest_new_moon_jde(guess)?;
guard += 1;
if guard > 30 {
return None;
}
}
let mut catch_up_guard = 0;
loop {
let next_jde = nearest_new_moon_jde(nm_jde + SYNODIC_MONTH_DAYS)?;
if next_jde <= target_jde {
nm_jde = next_jde;
} else {
break;
}
catch_up_guard += 1;
if catch_up_guard > 30 {
return None;
}
}
Some(nm_jde)
}
fn next_new_moon_after(jde: f64) -> Option<f64> {
let mut guess = jde + SYNODIC_MONTH_DAYS;
let mut nm_jde = nearest_new_moon_jde(guess)?;
let mut guard = 0;
while nm_jde <= jde {
guess = nm_jde + SYNODIC_MONTH_DAYS;
nm_jde = nearest_new_moon_jde(guess)?;
guard += 1;
if guard > 30 {
return None;
}
}
let mut catch_up_guard = 0;
loop {
let prev_jde = nearest_new_moon_jde(nm_jde - SYNODIC_MONTH_DAYS)?;
if prev_jde > jde {
nm_jde = prev_jde;
} else {
break;
}
catch_up_guard += 1;
if catch_up_guard > 30 {
return None;
}
}
Some(nm_jde)
}
fn jde_to_cst_date(jde: f64) -> Option<NaiveDate> {
let (y, m, _) = astro::time::date_frm_julian_day(jde).ok()?;
let delta_t = astro::time::delta_t(i32::from(y), m);
let ut_jde = jde - delta_t / 86_400.0;
let cst_jde = ut_jde + 8.0 / 24.0;
let (y2, m2, decimal_day) = astro::time::date_frm_julian_day(cst_jde).ok()?;
NaiveDate::from_ymd_opt(i32::from(y2), u32::from(m2), decimal_day.trunc() as u32)
}
fn new_moon_on_or_before_civil(target_jde: f64) -> Option<f64> {
let target_civil = jde_to_cst_date(target_jde)?;
let mut m = new_moon_on_or_before(target_jde)?;
let mut guard = 0;
loop {
let nxt = next_new_moon_after(m)?;
if jde_to_cst_date(nxt)? <= target_civil {
m = nxt;
} else {
break;
}
guard += 1;
if guard > 30 {
return None;
}
}
Some(m)
}
fn zhongqi_civil_dates_from(ws0_jde: f64, end_jde: f64) -> Option<Vec<NaiveDate>> {
let mut results = vec![jde_to_cst_date(ws0_jde)?];
let mut jde = ws0_jde;
let mut target = 270.0_f64;
let mut guard = 0;
while jde < end_jde + 3.0 {
target = (target + 30.0) % 360.0;
let seed = jde + TROPICAL_YEAR_DAYS / 12.0;
jde = bisect_solar_longitude(seed, target, 12.0);
results.push(jde_to_cst_date(jde)?);
guard += 1;
if guard > 20 {
break;
}
}
results.sort_unstable();
results.dedup();
Some(results)
}
thread_local! {
static SUI_MONTHS_CACHE: RefCell<HashMap<i32, Option<Vec<MonthEntry>>>> = RefCell::new(HashMap::new());
}
fn sui_months(year_for_solstice: i32) -> Option<Vec<MonthEntry>> {
if let Some(cached) = SUI_MONTHS_CACHE.with(|c| c.borrow().get(&year_for_solstice).cloned()) {
return cached;
}
let result = sui_months_uncached(year_for_solstice);
SUI_MONTHS_CACHE.with(|c| c.borrow_mut().insert(year_for_solstice, result.clone()));
result
}
fn sui_months_uncached(year_for_solstice: i32) -> Option<Vec<MonthEntry>> {
let ws0 = winter_solstice_jde(year_for_solstice)?;
let ws1 = winter_solstice_jde(year_for_solstice + 1)?;
let m_minus1 = new_moon_on_or_before_civil(ws0)?;
let m11 = new_moon_on_or_before_civil(ws1)?;
let mut moons = vec![m_minus1];
let mut guard = 0;
while *moons.last().unwrap() < m11 - 1.0 {
let nxt = next_new_moon_after(*moons.last().unwrap())?;
moons.push(nxt);
guard += 1;
if guard > 40 {
return None;
}
}
if (*moons.last().unwrap() - m11).abs() > 1.0 {
let last = moons.len() - 1;
moons[last] = m11;
}
let num_months = moons.len() - 1;
if num_months != 12 && num_months != 13 {
return None;
}
let civil_dates: Option<Vec<NaiveDate>> = moons.iter().map(|&m| jde_to_cst_date(m)).collect();
let civil_dates = civil_dates?;
let mut leap_index: Option<usize> = None;
if num_months == 13 {
let zq_dates = zhongqi_civil_dates_from(ws0, m11)?;
for (i, window) in civil_dates.windows(2).enumerate() {
let contains = zq_dates.iter().any(|&z| window[0] <= z && z < window[1]);
if !contains {
leap_index = Some(i);
break;
}
}
}
let mut result = Vec::with_capacity(num_months);
let mut current: u32 = 11;
let mut prev_number: Option<u32> = None;
for i in 0..num_months {
if leap_index == Some(i) {
result.push((prev_number?, true, civil_dates[i]));
} else {
result.push((current, false, civil_dates[i]));
prev_number = Some(current);
current = if current < 12 { current + 1 } else { 1 };
}
}
Some(result)
}
fn chinese_year_months(year: i32) -> Option<Vec<MonthEntry>> {
let sui_a = sui_months(year - 1)?;
let sui_b = sui_months(year)?;
let mut months: Vec<MonthEntry> = sui_a.into_iter().filter(|m| (1..=10).contains(&m.0)).collect();
for m in sui_b {
if m.0 == 11 || m.0 == 12 {
months.push(m);
} else {
break;
}
}
months.sort_by_key(|m| m.2);
Some(months)
}
pub(crate) fn chinese_new_year(year: i32) -> Option<NaiveDate> {
let months = chinese_year_months(year)?;
months.iter().find(|m| m.0 == 1 && !m.1).map(|m| m.2)
}
pub(crate) fn to_chinese_ymd(date: NaiveDate) -> Option<(i32, u32, bool, u32)> {
let mut year = date.year();
let mut guard = 0;
loop {
let ny = chinese_new_year(year)?;
if date < ny {
year -= 1;
} else {
let next_ny = chinese_new_year(year + 1)?;
if date >= next_ny {
year += 1;
} else {
break;
}
}
guard += 1;
if guard > 20 {
return None;
}
}
let months = chinese_year_months(year)?;
let (month, is_leap, start) = months.iter().rev().find(|m| m.2 <= date).copied()?;
let day = date.num_days_from_ce() - start.num_days_from_ce() + 1;
Some((year, month, is_leap, u32::try_from(day).ok()?))
}
pub(crate) fn from_chinese_ymd(year: i32, month: u32, is_leap: bool, day: u32) -> Option<NaiveDate> {
if day < 1 || day > 30 {
return None;
}
let months = chinese_year_months(year)?;
let idx = months.iter().position(|m| m.0 == month && m.1 == is_leap)?;
let start = months[idx].2;
let month_length: i32 = if idx + 1 < months.len() {
months[idx + 1].2.num_days_from_ce() - start.num_days_from_ce()
} else {
let next_ny = chinese_new_year(year + 1)?;
next_ny.num_days_from_ce() - start.num_days_from_ce()
};
if i32::try_from(day).ok()? > month_length {
return None;
}
start.checked_add_signed(Duration::days(i64::from(day) - 1))
}
pub(crate) fn qingming(year: i32) -> Option<NaiveDate> {
let jde = solar_term_jde(year, 4, 5.0, 15.0)?;
jde_to_cst_date(jde)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_moon_on_or_before_ws0_1948_regression() {
let ws0 = winter_solstice_jde(1948).expect("winter_solstice_jde(1948) returned None");
assert_eq!(jde_to_cst_date(ws0), NaiveDate::from_ymd_opt(1948, 12, 22));
let nm_jde = new_moon_on_or_before(ws0).expect("new_moon_on_or_before(ws0) returned None");
assert_eq!(jde_to_cst_date(nm_jde), NaiveDate::from_ymd_opt(1948, 12, 1));
}
#[test]
fn nearest_new_moon_jde_1948_wide_seed_scan() {
let ws0 = winter_solstice_jde(1948).expect("winter_solstice_jde(1948) returned None");
eprintln!("ws0 jde = {ws0} ({:?})", jde_to_cst_date(ws0));
let start = ws0 - 51.0; let mut seed = start;
while seed < ws0 + 15.0 {
let result = nearest_new_moon_jde(seed);
eprintln!(
"seed={seed} ({:?}) -> nearest_new_moon_jde = {result:?} ({:?})",
jde_to_cst_date(seed),
result.and_then(jde_to_cst_date)
);
seed += 2.0;
}
}
#[test]
fn sui_months_1948_step_by_step_regression() {
let ws0 = winter_solstice_jde(1948).expect("winter_solstice_jde(1948) returned None");
assert_eq!(jde_to_cst_date(ws0), NaiveDate::from_ymd_opt(1948, 12, 22), "ws0 civil date");
let ws1 = winter_solstice_jde(1949).expect("winter_solstice_jde(1949) returned None");
assert_eq!(jde_to_cst_date(ws1), NaiveDate::from_ymd_opt(1949, 12, 22), "ws1 civil date");
let m_minus1 = new_moon_on_or_before_civil(ws0).expect("new_moon_on_or_before_civil(ws0) returned None");
assert_eq!(jde_to_cst_date(m_minus1), NaiveDate::from_ymd_opt(1948, 12, 1), "m_minus1 civil date");
let m11 = new_moon_on_or_before_civil(ws1).expect("new_moon_on_or_before_civil(ws1) returned None");
assert_eq!(jde_to_cst_date(m11), NaiveDate::from_ymd_opt(1949, 12, 20), "m11 civil date");
let expected_moon_dates = [
(1948, 12, 1),
(1948, 12, 30),
(1949, 1, 29),
(1949, 2, 28),
(1949, 3, 29),
(1949, 4, 28),
(1949, 5, 28),
(1949, 6, 26),
(1949, 7, 26),
(1949, 8, 24),
(1949, 9, 22),
(1949, 10, 22),
(1949, 11, 20),
(1949, 12, 20),
];
let mut moons = vec![m_minus1];
let mut guard = 0;
while *moons.last().unwrap() < m11 - 1.0 {
let nxt = next_new_moon_after(*moons.last().unwrap())
.unwrap_or_else(|| panic!("next_new_moon_after failed at moons[{}]", moons.len() - 1));
moons.push(nxt);
guard += 1;
assert!(guard <= 40, "moons-building loop exceeded 40 iterations, moons so far: {}", moons.len());
}
assert_eq!(moons.len(), expected_moon_dates.len(), "wrong number of moons collected: {moons:?}");
for (i, (&jde, &(y, m, d))) in moons.iter().zip(expected_moon_dates.iter()).enumerate() {
assert_eq!(jde_to_cst_date(jde), NaiveDate::from_ymd_opt(y, m, d), "moons[{i}]");
}
let zq_dates = zhongqi_civil_dates_from(ws0, m11).expect("zhongqi_civil_dates_from returned None");
let expected_zq_dates = [
(1948, 12, 22),
(1949, 1, 20),
(1949, 2, 19),
(1949, 3, 21),
(1949, 4, 20),
(1949, 5, 21),
(1949, 6, 22),
(1949, 7, 23),
(1949, 8, 23),
(1949, 9, 23),
(1949, 10, 24),
(1949, 11, 22),
(1949, 12, 22),
(1950, 1, 20),
];
assert_eq!(zq_dates.len(), expected_zq_dates.len(), "wrong number of zhongqi collected: {zq_dates:?}");
for (i, (&d, &(y, m, day))) in zq_dates.iter().zip(expected_zq_dates.iter()).enumerate() {
assert_eq!(d, NaiveDate::from_ymd_opt(y, m, day).unwrap(), "zhongqi[{i}]");
}
let result = sui_months(1948);
assert!(result.is_some(), "sui_months(1948) returned None even though every intermediate value matched the Python reference");
}
#[test]
fn sui_months_1948_is_a_thirteen_month_leap_sui_with_leap_month_7() {
let months = sui_months(1948).expect("sui_months(1948) returned None");
assert_eq!(months.len(), 13, "expected 13 months, got {months:?}");
let leap: Vec<_> = months.iter().filter(|m| m.1).collect();
assert_eq!(leap.len(), 1, "expected exactly one leap month, got {leap:?}");
assert_eq!(leap[0].0, 7, "expected leap month 7, got {:?}", leap[0]);
assert_eq!(leap[0].2, NaiveDate::from_ymd_opt(1949, 8, 24).unwrap());
}
#[test]
fn chinese_new_year_1949_and_1950_match_reference_dates() {
assert_eq!(
chinese_new_year(1949),
NaiveDate::from_ymd_opt(1949, 1, 29),
"chinese_new_year(1949) mismatch or None"
);
assert_eq!(
chinese_new_year(1950),
NaiveDate::from_ymd_opt(1950, 2, 17),
"chinese_new_year(1950) mismatch or None"
);
}
#[test]
fn chinese_year_months_1949_has_thirteen_months_with_leap_7_and_month_11_on_dec_20() {
let months = chinese_year_months(1949).expect("chinese_year_months(1949) returned None");
assert_eq!(months.len(), 13, "expected 13 months, got {months:?}");
let leap: Vec<_> = months.iter().filter(|m| m.1).collect();
assert_eq!(leap.len(), 1, "expected exactly one leap month, got {leap:?}");
assert_eq!(leap[0].0, 7, "expected leap month 7, got {:?}", leap[0]);
assert_eq!(leap[0].2, NaiveDate::from_ymd_opt(1949, 8, 24).unwrap());
let m11 = months.iter().find(|m| m.0 == 11).expect("month 11 missing");
assert_eq!(m11.2, NaiveDate::from_ymd_opt(1949, 12, 20).unwrap());
let m12 = months.iter().find(|m| m.0 == 12).expect("month 12 missing");
assert_eq!(m12.2, NaiveDate::from_ymd_opt(1950, 1, 18).unwrap());
}
#[test]
fn to_chinese_ymd_1950_01_01_matches_reference() {
let d = NaiveDate::from_ymd_opt(1950, 1, 1).unwrap();
let result = to_chinese_ymd(d);
assert_eq!(result, Some((1949, 11, false, 13)), "to_chinese_ymd(1950-01-01) = {result:?}");
}
}