use lunar_rust::lunar::{self, LunarRefHelper};
use lunar_rust::lunar_month::LunarMonthRefHelper;
use lunar_rust::lunar_year::{self, LunarYearRefHelper};
use lunar_rust::solar::{self, SolarRefHelper};
use crate::error::IztroError;
const DEFECT_YEAR: i64 = 1602;
const DEFECT_MONTH: i64 = -2;
const DEFECT_MONTH_TRUE_FIRST_JD: f64 = 2306261.0;
const DEFECT_MONTH_TABLE_FIRST_JD: f64 = 2306262.0;
const DEFECT_PREV_MONTH_DAY_COUNT: i64 = 30;
const DEFECT_MONTH_DAY_COUNT: i64 = 29;
fn defect_present() -> bool {
static PRESENT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*PRESENT.get_or_init(|| {
lunar_year::LunarYear::from_lunar_year(DEFECT_YEAR)
.get_month(DEFECT_MONTH)
.is_some_and(|m| m.get_first_julian_day() == DEFECT_MONTH_TABLE_FIRST_JD)
})
}
pub(crate) struct LunarYmd {
pub year: i64,
pub month: i64,
pub day: i64,
}
pub(crate) fn ymd_of(lunar_ref: &lunar::LunarRef) -> Result<LunarYmd, IztroError> {
let year = lunar_ref.get_year();
let month = lunar_ref.get_month();
let day = lunar_ref.get_day();
let (month, day) = match (year, month) {
(DEFECT_YEAR, 2) if day > DEFECT_PREV_MONTH_DAY_COUNT => {
(DEFECT_MONTH, day - DEFECT_PREV_MONTH_DAY_COUNT)
}
(DEFECT_YEAR, DEFECT_MONTH) if defect_present() => (DEFECT_MONTH, day + 1),
_ => (month, day),
};
if !(1..=12).contains(&month.abs()) || !(1..=30).contains(&day) {
return Err(IztroError::Internal(format!(
"lunar_rust returned an out-of-range lunar date: year {year} month {month} day {day}"
)));
}
Ok(LunarYmd { year, month, day })
}
pub(crate) fn month_day_count(year: i64, signed_month: i64) -> Option<i64> {
match (year, signed_month) {
(DEFECT_YEAR, 2) => Some(DEFECT_PREV_MONTH_DAY_COUNT),
(DEFECT_YEAR, DEFECT_MONTH) => Some(DEFECT_MONTH_DAY_COUNT),
_ => lunar_year::LunarYear::from_lunar_year(year)
.get_month(signed_month)
.map(|m| m.get_day_count()),
}
}
pub(crate) fn solar_date_of(year: i64, signed_month: i64, day: i64) -> String {
let solar_ref = if (year, signed_month) == (DEFECT_YEAR, DEFECT_MONTH) {
solar::from_julian_day(DEFECT_MONTH_TRUE_FIRST_JD + (day - 1) as f64)
} else {
lunar::from_ymd(year, signed_month, day).get_solar()
};
format!(
"{}-{}-{}",
solar_ref.get_year(),
solar_ref.get_month(),
solar_ref.get_day(),
)
}
pub(crate) fn month_in_chinese(month: u32, is_leap: bool) -> String {
const MONTH: [&str; 13] = [
"", "正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊",
];
format!(
"{}{}",
if is_leap { "闰" } else { "" },
MONTH[month as usize]
)
}
pub(crate) fn day_in_chinese(day: u32) -> &'static str {
const DAY: [&str; 31] = [
"", "初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十", "十一",
"十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十", "廿一", "廿二",
"廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十",
];
DAY[day as usize]
}
#[cfg(test)]
mod tests {
use super::*;
use lunar_rust::solar;
fn ymd(y: i64, m: i64, d: i64) -> (i64, i64, i64) {
let l = lunar::from_solar(&solar::from_ymdhms(y, m, d, 12, 0, 0));
let v = ymd_of(&l).unwrap();
(v.year, v.month, v.day)
}
#[test]
fn defect_window_remaps_to_true_lunar_dates() {
assert_eq!(ymd(1602, 3, 23), (1602, 2, 30)); assert_eq!(ymd(1602, 3, 24), (1602, -2, 1)); assert_eq!(ymd(1602, 3, 25), (1602, -2, 2)); assert_eq!(ymd(1602, 4, 21), (1602, -2, 29)); assert_eq!(ymd(1602, 4, 22), (1602, 3, 1)); }
#[test]
fn normal_dates_pass_through() {
assert_eq!(ymd(2000, 8, 16), (2000, 7, 17));
assert_eq!(ymd(1602, 2, 22), (1602, 2, 1)); }
#[test]
fn corrected_day_counts() {
assert_eq!(month_day_count(1602, 2), Some(30));
assert_eq!(month_day_count(1602, -2), Some(29));
assert_eq!(month_day_count(1602, 3), Some(29));
assert_eq!(month_day_count(2023, -2), Some(29)); assert_eq!(month_day_count(2000, -5), None); }
#[test]
fn defect_month_maps_back_to_true_solar_dates() {
assert_eq!(solar_date_of(1602, -2, 1), "1602-3-24");
assert_eq!(solar_date_of(1602, -2, 29), "1602-4-21");
assert_eq!(solar_date_of(1602, 2, 30), "1602-3-23"); assert_eq!(solar_date_of(1602, 3, 1), "1602-4-22");
assert_eq!(solar_date_of(2000, 7, 17), "2000-8-16");
}
#[test]
fn chinese_names_match_lunar_rust_tables() {
assert_eq!(month_in_chinese(1, false), "正");
assert_eq!(month_in_chinese(2, true), "闰二");
assert_eq!(month_in_chinese(12, false), "腊");
assert_eq!(day_in_chinese(1), "初一");
assert_eq!(day_in_chinese(30), "三十");
}
#[test]
#[ignore = "全域逐月约 90 秒(release)"]
fn month_table_day_counts_all_valid() {
use lunar_rust::lunar_year::LunarYearRefHelper;
for year in 1582..=9999i64 {
for month_ref in lunar_year::LunarYear::from_lunar_year(year).get_months() {
let corrected = month_day_count(month_ref.get_year(), month_ref.get_month())
.unwrap_or_else(|| {
panic!(
"month {} of lunar year {} vanished through the correction layer",
month_ref.get_month(),
month_ref.get_year()
)
});
assert!(
corrected == 29 || corrected == 30,
"lunar year {} month {}: corrected day count {corrected}",
month_ref.get_year(),
month_ref.get_month(),
);
}
}
}
}