use crate::calendar::{civil_from_days, days_from_civil};
use crate::error::LunarError;
use crate::generated::solar_terms::{JIE_BOUNDARIES, JIE_END_YEAR, JIE_START_YEAR};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct TermMoment {
pub(crate) ordinal: u16,
pub(crate) second_of_day: u32,
}
pub(crate) const SECONDS_PER_DAY: i64 = 86_400;
pub(crate) const MIN_YEAR: i32 = JIE_START_YEAR;
pub(crate) const MAX_YEAR: i32 = JIE_END_YEAR;
pub(crate) const LI_CHUN: usize = 1;
pub(crate) const MONTH_BRANCH_OF_JIE: [usize; 12] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0];
pub(crate) const MONTH_BRANCH_BEFORE_FIRST_JIE: usize = 0;
fn year_terms(year: i32) -> Result<&'static [TermMoment; 12], LunarError> {
if !(JIE_START_YEAR..=JIE_END_YEAR).contains(&year) {
return Err(LunarError::SolarTermOutOfRange { year });
}
Ok(&JIE_BOUNDARIES[(year - JIE_START_YEAR) as usize])
}
fn term_instant(year: i32, moment: TermMoment) -> i64 {
let day_number = days_from_civil(year, 1, 1) + (moment.ordinal as i32 - 1);
day_number as i64 * SECONDS_PER_DAY + moment.second_of_day as i64
}
pub(crate) fn day_instant(year: i32, month: u8, day: u8, second_of_day: i64) -> i64 {
days_from_civil(year, month, day) as i64 * SECONDS_PER_DAY + second_of_day
}
pub(crate) fn jie_instants(year: i32) -> Result<[i64; 12], LunarError> {
let terms = year_terms(year)?;
let mut out = [0i64; 12];
for (slot, &moment) in out.iter_mut().zip(terms.iter()) {
*slot = term_instant(year, moment);
}
Ok(out)
}
pub(crate) fn li_chun_date(year: i32) -> Result<(u8, u8), LunarError> {
let moment = year_terms(year)?[LI_CHUN];
let date = civil_from_days(days_from_civil(year, 1, 1) + (moment.ordinal as i32 - 1));
Ok((date.month, date.day))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn li_chun_dates_match_reference() {
assert_eq!(li_chun_date(2000).unwrap(), (2, 4));
assert_eq!(li_chun_date(2024).unwrap(), (2, 4));
}
#[test]
fn jie_instants_are_strictly_increasing() {
let instants = jie_instants(2000).unwrap();
for pair in instants.windows(2) {
assert!(pair[0] < pair[1]);
}
}
#[test]
fn li_chun_2000_instant_matches_reference() {
let instants = jie_instants(2000).unwrap();
let expected = day_instant(2000, 2, 4, 20 * 3600 + 40 * 60 + 24);
assert_eq!(instants[LI_CHUN], expected);
}
#[test]
fn out_of_range_years_error() {
assert_eq!(
jie_instants(1849).unwrap_err(),
LunarError::SolarTermOutOfRange { year: 1849 }
);
assert_eq!(
li_chun_date(2151).unwrap_err(),
LunarError::SolarTermOutOfRange { year: 2151 }
);
}
}