use crate::core::calendar::resolve_lunar_date;
use crate::core::error::ChartError;
use crate::core::model::calendar::{BirthContext, BirthTime, CalendarDate, Gender};
use crate::core::model::chart::Chart;
use crate::core::model::profile::MethodProfile;
use crate::core::placement::natal::input::NatalChartWithSupportedStarsInput;
use crate::core::placement::natal::life_body::{LunarDay, LunarMonth};
use crate::core::placement::natal::supported::build_natal_chart_with_supported_stars;
use lunar_lite::{EarthlyBranch, HeavenlyStem, StemBranch};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LunarChartRequest {
lunar_year: i32,
lunar_month: LunarMonth,
lunar_day: LunarDay,
birth_time: BirthTime,
gender: Gender,
birth_year_stem: HeavenlyStem,
birth_year_branch: EarthlyBranch,
is_leap_month: bool,
fix_leap: bool,
method_profile: MethodProfile,
}
impl LunarChartRequest {
pub fn builder() -> LunarChartRequestBuilder {
LunarChartRequestBuilder::default()
}
pub const fn lunar_year(&self) -> i32 {
self.lunar_year
}
pub const fn lunar_month(&self) -> LunarMonth {
self.lunar_month
}
pub const fn lunar_day(&self) -> LunarDay {
self.lunar_day
}
pub const fn birth_time(&self) -> EarthlyBranch {
self.birth_time.branch()
}
pub const fn birth_time_variant(&self) -> BirthTime {
self.birth_time
}
pub const fn gender(&self) -> Gender {
self.gender
}
pub const fn birth_year_stem(&self) -> HeavenlyStem {
self.birth_year_stem
}
pub const fn birth_year_branch(&self) -> EarthlyBranch {
self.birth_year_branch
}
pub const fn is_leap_month(&self) -> bool {
self.is_leap_month
}
pub const fn fix_leap(&self) -> bool {
self.fix_leap
}
pub const fn method_profile(&self) -> &MethodProfile {
&self.method_profile
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LunarChartRequestBuilder {
lunar_year: Option<i32>,
lunar_month: Option<LunarMonth>,
lunar_day: Option<LunarDay>,
birth_time: Option<BirthTime>,
gender: Option<Gender>,
birth_year_stem: Option<HeavenlyStem>,
birth_year_branch: Option<EarthlyBranch>,
is_leap_month: Option<bool>,
fix_leap: Option<bool>,
method_profile: Option<MethodProfile>,
}
impl LunarChartRequestBuilder {
pub fn lunar_year(mut self, value: i32) -> Self {
self.lunar_year = Some(value);
self
}
pub fn lunar_month(mut self, value: LunarMonth) -> Self {
self.lunar_month = Some(value);
self
}
pub fn lunar_day(mut self, value: LunarDay) -> Self {
self.lunar_day = Some(value);
self
}
pub fn birth_time(mut self, value: EarthlyBranch) -> Self {
self.birth_time = Some(BirthTime::from_branch(value));
self
}
pub fn birth_time_variant(mut self, value: BirthTime) -> Self {
self.birth_time = Some(value);
self
}
pub fn iztro_time_index(mut self, value: u8) -> Result<Self, ChartError> {
self.birth_time = Some(BirthTime::from_iztro_time_index(value)?);
Ok(self)
}
pub fn gender(mut self, value: Gender) -> Self {
self.gender = Some(value);
self
}
pub fn birth_year_stem(mut self, value: HeavenlyStem) -> Self {
self.birth_year_stem = Some(value);
self
}
pub fn birth_year_branch(mut self, value: EarthlyBranch) -> Self {
self.birth_year_branch = Some(value);
self
}
pub fn is_leap_month(mut self, value: bool) -> Self {
self.is_leap_month = Some(value);
self
}
pub fn fix_leap(mut self, value: bool) -> Self {
self.fix_leap = Some(value);
self
}
pub fn method_profile(mut self, value: MethodProfile) -> Self {
self.method_profile = Some(value);
self
}
pub fn build(self) -> Result<LunarChartRequest, ChartError> {
Ok(LunarChartRequest {
lunar_year: self.lunar_year.ok_or(ChartError::MissingRequiredInput {
field: "lunar_year",
})?,
lunar_month: self.lunar_month.ok_or(ChartError::MissingRequiredInput {
field: "lunar_month",
})?,
lunar_day: self
.lunar_day
.ok_or(ChartError::MissingRequiredInput { field: "lunar_day" })?,
birth_time: self.birth_time.ok_or(ChartError::MissingRequiredInput {
field: "birth_time",
})?,
gender: self
.gender
.ok_or(ChartError::MissingRequiredInput { field: "gender" })?,
birth_year_stem: self
.birth_year_stem
.ok_or(ChartError::MissingRequiredInput {
field: "birth_year_stem",
})?,
birth_year_branch: self
.birth_year_branch
.ok_or(ChartError::MissingRequiredInput {
field: "birth_year_branch",
})?,
is_leap_month: self.is_leap_month.unwrap_or(false),
fix_leap: self.fix_leap.unwrap_or(true),
method_profile: self
.method_profile
.ok_or(ChartError::MissingRequiredInput {
field: "method_profile",
})?,
})
}
}
pub fn by_lunar(request: LunarChartRequest) -> Result<Chart, ChartError> {
let resolved = resolve_lunar_date(
request.lunar_year(),
request.lunar_month(),
request.lunar_day(),
request.is_leap_month(),
)?;
let birth_time = request.birth_time_variant();
let effective_lunar_month = effective_lunar_month(
resolved.lunar_month(),
resolved.lunar_day(),
resolved.is_leap_month(),
request.fix_leap(),
birth_time,
)?;
let major_lunar_day = major_lunar_day(resolved.lunar_day(), resolved.month_days(), birth_time)?;
let daily_star_offset = daily_star_offset(resolved.lunar_day(), birth_time);
let birth_year = StemBranch::try_new(request.birth_year_stem(), request.birth_year_branch())
.map_err(|err| match err {
lunar_lite::StemBranchError::InvalidStemBranchPair { stem, branch } => {
ChartError::InvalidStemBranchPair { stem, branch }
}
})?;
let birth_context = BirthContext::new_with_birth_time_variant(
CalendarDate::lunar(
resolved.lunar_year(),
resolved.lunar_month().value(),
resolved.lunar_day().value(),
),
birth_time,
request.gender(),
);
build_natal_chart_with_supported_stars(
NatalChartWithSupportedStarsInput::new_with_daily_star_offset(
birth_context,
request.method_profile().clone(),
effective_lunar_month,
major_lunar_day,
daily_star_offset,
birth_year.stem(),
birth_year.branch(),
),
)
}
fn effective_lunar_month(
lunar_month: LunarMonth,
lunar_day: LunarDay,
is_leap_month: bool,
fix_leap: bool,
birth_time: BirthTime,
) -> Result<LunarMonth, ChartError> {
let needs_advance =
is_leap_month && fix_leap && lunar_day.value() > 15 && !birth_time.is_late_zi();
if !needs_advance {
return Ok(lunar_month);
}
LunarMonth::new(lunar_month.value() + 1).map_err(|_| {
ChartError::UnsupportedLeapMonthCombination {
lunar_month: lunar_month.value(),
lunar_day: lunar_day.value(),
}
})
}
fn major_lunar_day(
lunar_day: LunarDay,
month_days: u8,
birth_time: BirthTime,
) -> Result<LunarDay, ChartError> {
if !birth_time.is_late_zi() {
return Ok(lunar_day);
}
let next_day = if lunar_day.value() >= month_days {
1
} else {
lunar_day.value() + 1
};
LunarDay::new(next_day)
}
fn daily_star_offset(lunar_day: LunarDay, birth_time: BirthTime) -> u8 {
if birth_time.is_late_zi() {
lunar_day.value()
} else {
lunar_day.value() - 1
}
}