use crate::core::calendar::resolve_lunar_date;
use crate::core::error::{ChartError, validate_chart_algorithm_plane};
use crate::core::model::calendar::{BirthContext, BirthTime, CalendarDate, Gender};
use crate::core::model::chart::Chart;
use crate::core::model::ganzhi::{EarthlyBranch, HeavenlyStem, StemBranch};
use crate::core::model::profile::{ChartPlane, ChartProfile, MethodProfile};
use crate::core::placement::natal::input::{NatalChartInput, NatalChartWithSupportedStarsInput};
use crate::core::placement::natal::life_body::{LunarDay, LunarMonth};
use crate::core::placement::natal::minimal::build_minimal_natal_chart;
use crate::core::placement::natal::plane::resolve_natal_chart_anchor;
use crate::core::placement::natal::strategy::DeterministicNatalStarPlacementStrategy;
use crate::core::placement::natal::supported::build_natal_chart_with_supported_stars_using_anchor_and_strategy;
#[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,
chart_plane: ChartPlane,
}
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
}
pub const fn chart_plane(&self) -> ChartPlane {
self.chart_plane
}
}
#[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>,
chart_plane: Option<ChartPlane>,
}
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 const fn chart_plane(mut self, chart_plane: ChartPlane) -> Self {
self.chart_plane = Some(chart_plane);
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",
})?,
chart_plane: self.chart_plane.unwrap_or_default(),
})
}
}
pub fn by_lunar(request: LunarChartRequest) -> Result<Chart, ChartError> {
let algorithm = request.method_profile().algorithm_kind();
let plane = request.chart_plane();
validate_chart_algorithm_plane(algorithm, plane)?;
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 {
crate::core::model::ganzhi::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(),
);
let supported_input = 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(),
);
let anchor = resolve_natal_chart_anchor(algorithm, plane, || {
build_minimal_natal_chart(NatalChartInput::new(
supported_input.birth_context().clone(),
supported_input.method_profile().clone(),
supported_input.lunar_month(),
supported_input.birth_year_stem(),
supported_input.birth_year_branch(),
))
})?;
let chart_profile = ChartProfile::new(request.method_profile().clone(), plane);
let chart = build_natal_chart_with_supported_stars_using_anchor_and_strategy(
supported_input,
anchor,
&DeterministicNatalStarPlacementStrategy::default(),
)?;
Ok(chart.with_chart_profile(chart_profile))
}
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
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::model::chart::PalaceName;
use crate::core::model::profile::ChartAlgorithmKind;
fn base_builder(profile: MethodProfile) -> LunarChartRequestBuilder {
LunarChartRequest::builder()
.lunar_year(1990)
.lunar_month(LunarMonth::new(5).expect("valid lunar month"))
.lunar_day(LunarDay::new(17).expect("valid lunar day"))
.birth_time(EarthlyBranch::Chen)
.gender(Gender::Female)
.birth_year_stem(HeavenlyStem::Geng)
.birth_year_branch(EarthlyBranch::Wu)
.method_profile(profile)
}
fn quanshu_profile() -> MethodProfile {
MethodProfile::new("quanshu_test", ChartAlgorithmKind::QuanShu, "quanshu test")
}
fn zhongzhou_profile() -> MethodProfile {
MethodProfile::new(
"zhongzhou_test",
ChartAlgorithmKind::Zhongzhou,
"zhongzhou test",
)
}
fn placeholder_profile() -> MethodProfile {
MethodProfile::placeholder("placeholder_test")
}
fn zhongzhou_chart(plane: ChartPlane) -> Chart {
by_lunar(
base_builder(zhongzhou_profile())
.chart_plane(plane)
.build()
.expect("request should build"),
)
.expect("zhongzhou chart should build")
}
fn life_palace_branch(chart: &Chart) -> EarthlyBranch {
chart
.life_palace()
.expect("chart should have a Life Palace")
.branch()
}
fn fortune_palace_branch(chart: &Chart) -> EarthlyBranch {
chart
.palace_by_name(PalaceName::Spirit)
.expect("chart should have a Fortune Palace")
.branch()
}
#[test]
fn chart_plane_defaults_to_heaven() {
let request = base_builder(quanshu_profile())
.build()
.expect("request should build");
assert_eq!(request.chart_plane(), ChartPlane::Heaven);
}
#[test]
fn explicit_chart_plane_is_preserved() {
let request = base_builder(zhongzhou_profile())
.chart_plane(ChartPlane::Earth)
.build()
.expect("request should build");
assert_eq!(request.chart_plane(), ChartPlane::Earth);
}
#[test]
fn default_request_matches_explicit_heaven_request() {
let default_chart = by_lunar(
base_builder(quanshu_profile())
.build()
.expect("default request should build"),
)
.expect("default by_lunar should build");
let heaven_chart = by_lunar(
base_builder(quanshu_profile())
.chart_plane(ChartPlane::Heaven)
.build()
.expect("heaven request should build"),
)
.expect("heaven by_lunar should build");
assert_eq!(default_chart, heaven_chart);
}
#[test]
fn quanshu_earth_is_unsupported() {
let request = base_builder(quanshu_profile())
.chart_plane(ChartPlane::Earth)
.build()
.expect("request should build");
assert_eq!(
by_lunar(request),
Err(ChartError::UnsupportedChartPlane {
algorithm: ChartAlgorithmKind::QuanShu,
plane: ChartPlane::Earth,
}),
);
}
#[test]
fn quanshu_human_is_unsupported() {
let request = base_builder(quanshu_profile())
.chart_plane(ChartPlane::Human)
.build()
.expect("request should build");
assert_eq!(
by_lunar(request),
Err(ChartError::UnsupportedChartPlane {
algorithm: ChartAlgorithmKind::QuanShu,
plane: ChartPlane::Human,
}),
);
}
#[test]
fn zhongzhou_heaven_still_succeeds() {
let request = base_builder(zhongzhou_profile())
.chart_plane(ChartPlane::Heaven)
.build()
.expect("request should build");
assert!(by_lunar(request).is_ok());
}
#[test]
fn zhongzhou_earth_succeeds() {
let request = base_builder(zhongzhou_profile())
.chart_plane(ChartPlane::Earth)
.build()
.expect("request should build");
assert!(by_lunar(request).is_ok());
}
#[test]
fn zhongzhou_human_succeeds() {
let request = base_builder(zhongzhou_profile())
.chart_plane(ChartPlane::Human)
.build()
.expect("request should build");
assert!(by_lunar(request).is_ok());
}
#[test]
fn zhongzhou_default_matches_explicit_heaven() {
let default_chart = by_lunar(
base_builder(zhongzhou_profile())
.build()
.expect("default request should build"),
)
.expect("default by_lunar should build");
assert_eq!(default_chart, zhongzhou_chart(ChartPlane::Heaven));
}
#[test]
fn placeholder_earth_is_unsupported() {
let request = base_builder(placeholder_profile())
.chart_plane(ChartPlane::Earth)
.build()
.expect("request should build");
assert_eq!(
by_lunar(request),
Err(ChartError::UnsupportedChartPlane {
algorithm: ChartAlgorithmKind::Placeholder,
plane: ChartPlane::Earth,
}),
);
}
#[test]
fn placeholder_human_is_unsupported() {
let request = base_builder(placeholder_profile())
.chart_plane(ChartPlane::Human)
.build()
.expect("request should build");
assert_eq!(
by_lunar(request),
Err(ChartError::UnsupportedChartPlane {
algorithm: ChartAlgorithmKind::Placeholder,
plane: ChartPlane::Human,
}),
);
}
#[test]
fn zhongzhou_earth_life_palace_anchors_to_heaven_body_palace() {
let heaven = zhongzhou_chart(ChartPlane::Heaven);
let earth = zhongzhou_chart(ChartPlane::Earth);
assert_eq!(
life_palace_branch(&earth),
heaven
.body_palace_branch()
.expect("heaven chart should have a Body Palace branch"),
);
}
#[test]
fn zhongzhou_human_life_palace_anchors_to_heaven_fortune_palace() {
let heaven = zhongzhou_chart(ChartPlane::Heaven);
let human = zhongzhou_chart(ChartPlane::Human);
assert_eq!(life_palace_branch(&human), fortune_palace_branch(&heaven));
}
#[test]
fn reanchored_bureau_follows_reanchored_life_palace() {
use crate::core::model::bureau::five_element_bureau_from_life_palace;
for plane in [ChartPlane::Earth, ChartPlane::Human] {
let chart = zhongzhou_chart(plane);
let life = chart
.life_palace()
.expect("chart should have a Life Palace");
let expected = five_element_bureau_from_life_palace(
StemBranch::try_new(life.stem(), life.branch())
.expect("life palace stem-branch should be valid"),
);
assert_eq!(chart.five_element_bureau(), Some(expected));
}
}
#[test]
fn reanchored_planes_differ_from_heaven() {
let heaven = zhongzhou_chart(ChartPlane::Heaven);
assert_ne!(zhongzhou_chart(ChartPlane::Earth), heaven);
assert_ne!(zhongzhou_chart(ChartPlane::Human), heaven);
}
#[test]
fn quanshu_heaven_chart_carries_heaven_plane() {
let chart = by_lunar(
base_builder(quanshu_profile())
.chart_plane(ChartPlane::Heaven)
.build()
.expect("request should build"),
)
.expect("chart should build");
assert_eq!(chart.chart_plane(), ChartPlane::Heaven);
assert_eq!(
chart.method_profile().algorithm_kind(),
ChartAlgorithmKind::QuanShu,
);
}
#[test]
fn default_request_chart_carries_heaven_plane() {
let chart = by_lunar(
base_builder(quanshu_profile())
.build()
.expect("request should build"),
)
.expect("chart should build");
assert_eq!(chart.chart_plane(), ChartPlane::Heaven);
}
#[test]
fn zhongzhou_charts_carry_requested_plane() {
for plane in [ChartPlane::Heaven, ChartPlane::Earth, ChartPlane::Human] {
let chart = zhongzhou_chart(plane);
assert_eq!(chart.chart_plane(), plane);
assert_eq!(
chart.method_profile().algorithm_kind(),
ChartAlgorithmKind::Zhongzhou,
);
}
}
#[test]
fn reanchored_planes_still_place_stars() {
for plane in [ChartPlane::Earth, ChartPlane::Human] {
let chart = zhongzhou_chart(plane);
assert!(
chart
.palaces()
.iter()
.any(|palace| !palace.stars().is_empty()),
"{plane:?} chart should have placed stars",
);
}
}
}