use crate::core::calculation::YearBoundary;
use crate::core::error::ChartError;
use crate::core::model::ganzhi::{EarthlyBranch, FourPillars, HeavenlyStem, StemBranch};
use super::tyme::{LunarDateInfo, ResolvedSolarDateTime, TymeCalendar};
pub(super) fn resolve_four_pillars(
calendar: &TymeCalendar,
time: ResolvedSolarDateTime,
lunar: LunarDateInfo,
year_boundary: YearBoundary,
) -> Result<FourPillars, ChartError> {
let yearly = resolve_year_pillar(calendar, time, lunar.year, year_boundary)?;
let monthly = month_pillar_normal(yearly.stem(), lunar.month, lunar.day, lunar.is_leap_month);
let (daily, hourly) = calendar.day_hour_pillars(time)?;
Ok(FourPillars {
yearly,
monthly,
daily,
hourly,
})
}
pub(super) fn resolve_year_pillar(
calendar: &TymeCalendar,
time: ResolvedSolarDateTime,
lunar_year: i32,
year_boundary: YearBoundary,
) -> Result<StemBranch, ChartError> {
match year_boundary {
YearBoundary::ChineseNewYearEve => Ok(StemBranch::from_lunar_year(lunar_year)),
YearBoundary::LiChun => {
let lichun = calendar.lichun_instant(time.year)?;
let pillar_year = if is_before_instant(time, lichun) {
time.year - 1
} else {
time.year
};
Ok(StemBranch::from_lunar_year(pillar_year))
}
}
}
fn is_before_instant(time: ResolvedSolarDateTime, lichun: ResolvedSolarDateTime) -> bool {
(time.month, time.day, time.hour, time.minute, time.second)
< (
lichun.month,
lichun.day,
lichun.hour,
lichun.minute,
lichun.second,
)
}
fn month_pillar_normal(
year_stem: HeavenlyStem,
lunar_month: u8,
lunar_day: u8,
is_leap_month: bool,
) -> StemBranch {
let yin_stem = year_stem.index() % 5 * 2 + 2;
let fix_leap = usize::from(is_leap_month && lunar_day > 15);
let offset = (lunar_month as usize - 1) + fix_leap;
let stem = (yin_stem + offset) % 10;
let branch = (2 + offset) % 12; StemBranch::try_new(
HeavenlyStem::from_index(stem),
EarthlyBranch::from_index(branch),
)
.expect("computed stem and branch share parity by construction")
}
#[cfg(test)]
mod tests {
use super::*;
fn time(year: i32, month: u8, day: u8, hour: u8) -> ResolvedSolarDateTime {
ResolvedSolarDateTime {
year,
month,
day,
hour,
minute: 30,
second: 0,
}
}
fn sb(stem: HeavenlyStem, branch: EarthlyBranch) -> StemBranch {
StemBranch::try_new(stem, branch).expect("valid stem-branch")
}
#[test]
fn lichun_before_exact_instant_uses_previous_year() {
let pillar = resolve_year_pillar(
&TymeCalendar,
time(2024, 2, 4, 8),
2023,
YearBoundary::LiChun,
)
.expect("year pillar resolves");
assert_eq!(pillar, sb(HeavenlyStem::Gui, EarthlyBranch::Mao));
}
#[test]
fn lichun_after_exact_instant_uses_new_year() {
let pillar = resolve_year_pillar(
&TymeCalendar,
time(2024, 2, 4, 20),
2023,
YearBoundary::LiChun,
)
.expect("year pillar resolves");
assert_eq!(pillar, sb(HeavenlyStem::Jia, EarthlyBranch::Chen));
}
#[test]
fn chinese_new_year_eve_uses_lunar_year() {
let pillar = resolve_year_pillar(
&TymeCalendar,
time(2024, 2, 4, 20),
2023,
YearBoundary::ChineseNewYearEve,
)
.expect("year pillar resolves");
assert_eq!(pillar, sb(HeavenlyStem::Gui, EarthlyBranch::Mao));
}
#[test]
fn month_pillar_normal_matches_wu_hu_dun() {
let pillar = month_pillar_normal(HeavenlyStem::Geng, 7, 17, false);
assert_eq!(pillar, sb(HeavenlyStem::Jia, EarthlyBranch::Shen));
}
}