use tyme4rs::tyme::lunar::{LunarDay, LunarMonth, LunarYear};
use tyme4rs::tyme::sixtycycle::SixtyCycle;
use tyme4rs::tyme::solar::{SolarDay, SolarTerm, SolarTime};
use crate::core::error::ChartError;
use crate::core::model::calendar::SolarDate;
use crate::core::model::ganzhi::StemBranch;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ResolvedSolarDateTime {
pub year: i32,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct LunarDateInfo {
pub year: i32,
pub month: u8,
pub day: u8,
pub is_leap_month: bool,
pub month_day_count: u8,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct LunarDateInput {
pub year: i32,
pub month: u8,
pub day: u8,
pub is_leap_month: bool,
}
pub(crate) struct TymeCalendar;
impl TymeCalendar {
pub(crate) fn lunar_from_solar(&self, date: SolarDate) -> Result<LunarDateInfo, ChartError> {
let (year, month, day) = (date.year(), date.month().value(), date.day().value());
SolarDay::validate(year as isize, month as usize, day as usize)
.map_err(|_| ChartError::InvalidSolarDate { year, month, day })?;
let lunar = SolarDay::from_ymd(year as isize, month as usize, day as usize).get_lunar_day();
let lunar_month: LunarMonth = lunar.get_lunar_month();
Ok(LunarDateInfo {
year: lunar_month.get_lunar_year().get_year() as i32,
month: lunar_month.get_month() as u8,
day: lunar.get_day() as u8,
is_leap_month: lunar_month.is_leap(),
month_day_count: lunar_month.get_day_count() as u8,
})
}
pub(crate) fn resolve_lunar(&self, input: LunarDateInput) -> Result<LunarDateInfo, ChartError> {
let LunarDateInput {
year,
month,
day,
is_leap_month,
} = input;
let unsupported = || ChartError::UnsupportedCalendarDate { year, month, day };
let leap_month = LunarYear::from_year(year as isize).get_leap_month();
let effective_leap = is_leap_month && leap_month == month as usize;
let signed_month = if effective_leap {
-(month as isize)
} else {
month as isize
};
LunarMonth::validate(year as isize, signed_month).map_err(|_| unsupported())?;
let lunar_month = LunarMonth::from_ym(year as isize, signed_month);
let month_day_count = lunar_month.get_day_count() as u8;
if !(1..=month_day_count).contains(&day) {
return Err(unsupported());
}
Ok(LunarDateInfo {
year,
month: lunar_month.get_month() as u8,
day,
is_leap_month: effective_leap,
month_day_count,
})
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn solar_from_lunar(&self, input: LunarDateInput) -> Result<SolarDate, ChartError> {
let resolved = self.resolve_lunar(input)?;
let signed_month = if resolved.is_leap_month {
-(resolved.month as isize)
} else {
resolved.month as isize
};
let conversion_failed = || ChartError::CalendarConversionFailed {
year: input.year,
month: input.month,
day: input.day,
};
LunarDay::validate(input.year as isize, signed_month, resolved.day as usize)
.map_err(|_| conversion_failed())?;
let solar = LunarDay::from_ymd(input.year as isize, signed_month, resolved.day as usize)
.get_solar_day();
SolarDate::new(
solar.get_year() as i32,
solar.get_month() as u8,
solar.get_day() as u8,
)
}
pub(crate) fn day_hour_pillars(
&self,
time: ResolvedSolarDateTime,
) -> Result<(StemBranch, StemBranch), ChartError> {
let solar = solar_time(time)?;
let sch = solar.get_sixty_cycle_hour();
Ok((
stem_branch_from_cycle(sch.get_day()),
stem_branch_from_cycle(sch.get_sixty_cycle()),
))
}
pub(crate) fn lichun_instant(
&self,
gregorian_year: i32,
) -> Result<ResolvedSolarDateTime, ChartError> {
let solar = SolarTerm::from_name(gregorian_year as isize, "立春")
.get_julian_day()
.get_solar_time();
Ok(ResolvedSolarDateTime {
year: solar.get_year() as i32,
month: solar.get_month() as u8,
day: solar.get_day() as u8,
hour: solar.get_hour() as u8,
minute: solar.get_minute() as u8,
second: solar.get_second() as u8,
})
}
}
fn solar_time(time: ResolvedSolarDateTime) -> Result<SolarTime, ChartError> {
SolarTime::validate(
time.year as isize,
time.month as usize,
time.day as usize,
time.hour as usize,
time.minute as usize,
time.second as usize,
)
.map_err(|_| ChartError::InvalidSolarDate {
year: time.year,
month: time.month,
day: time.day,
})?;
Ok(SolarTime::from_ymd_hms(
time.year as isize,
time.month as usize,
time.day as usize,
time.hour as usize,
time.minute as usize,
time.second as usize,
))
}
fn stem_branch_from_cycle(cycle: SixtyCycle) -> StemBranch {
StemBranch::from_cycle_index(cycle.get_index())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::model::ganzhi::{EarthlyBranch, HeavenlyStem};
fn solar(year: i32, month: u8, day: u8) -> SolarDate {
SolarDate::new(year, month, day).expect("valid solar date")
}
fn sb(stem: HeavenlyStem, branch: EarthlyBranch) -> StemBranch {
StemBranch::try_new(stem, branch).expect("valid stem-branch")
}
#[test]
fn solar_to_lunar_basic() {
let info = TymeCalendar
.lunar_from_solar(solar(2024, 2, 10))
.expect("conversion succeeds");
assert_eq!(info.year, 2024);
assert_eq!(info.month, 1);
assert_eq!(info.day, 1);
assert!(!info.is_leap_month);
}
#[test]
fn lunar_to_solar_basic() {
let resolved = TymeCalendar
.solar_from_lunar(LunarDateInput {
year: 2024,
month: 1,
day: 1,
is_leap_month: false,
})
.expect("conversion succeeds");
assert_eq!(resolved.year(), 2024);
assert_eq!(resolved.month().value(), 2);
assert_eq!(resolved.day().value(), 10);
}
#[test]
fn four_pillars_basic() {
let (daily, hourly) = TymeCalendar
.day_hour_pillars(ResolvedSolarDateTime {
year: 2000,
month: 8,
day: 16,
hour: 3,
minute: 30,
second: 0,
})
.expect("pillars resolve");
assert_eq!(daily, sb(HeavenlyStem::Bing, EarthlyBranch::Wu));
assert_eq!(hourly, sb(HeavenlyStem::Geng, EarthlyBranch::Yin));
}
#[test]
fn late_zi_rolls_day_pillar() {
let (early_day, _) = TymeCalendar
.day_hour_pillars(ResolvedSolarDateTime {
year: 2000,
month: 8,
day: 16,
hour: 0,
minute: 30,
second: 0,
})
.expect("pillars resolve");
let (late_day, _) = TymeCalendar
.day_hour_pillars(ResolvedSolarDateTime {
year: 2000,
month: 8,
day: 16,
hour: 23,
minute: 30,
second: 0,
})
.expect("pillars resolve");
assert_eq!(
late_day,
StemBranch::from_cycle_index(early_day.cycle_index() + 1)
);
}
#[test]
fn lichun_instant_is_datetime_level() {
let lichun = TymeCalendar.lichun_instant(2024).expect("lichun resolves");
assert_eq!(lichun.year, 2024);
assert_eq!(lichun.month, 2);
assert_eq!(lichun.day, 4);
assert!(lichun.hour > 0 || lichun.minute > 0);
}
}