use crate::core::error::ChartError;
use crate::core::model::bureau::five_element_bureau_from_life_palace;
use crate::core::model::calendar::BirthContext;
use crate::core::model::chart::{Chart, PALACE_COUNT, PALACE_NAMES, Palace, PalaceName};
use crate::core::model::ganzhi::{EARTHLY_BRANCHES, EarthlyBranch, HEAVENLY_STEMS, StemBranch};
use crate::core::model::profile::MethodProfile;
use crate::core::placement::natal::input::NatalChartInput;
use crate::core::placement::natal::life_body::{
LunarBirthContext, calculate_life_body_palace_indices,
};
use crate::core::placement::natal::palace_stems::palace_stem_for_branch;
pub fn build_empty_chart(
birth_context: BirthContext,
birth_year: StemBranch,
method_profile: MethodProfile,
) -> Result<Chart, ChartError> {
let palaces = PALACE_NAMES
.iter()
.copied()
.enumerate()
.map(|(index, name)| {
Palace::new(
name,
EARTHLY_BRANCHES[index],
HEAVENLY_STEMS[index % HEAVENLY_STEMS.len()],
Vec::new(),
)
})
.collect();
Chart::try_new(
birth_context,
birth_year,
method_profile,
palaces,
None,
None,
)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum NatalChartAnchor {
CalculatedLifePalace,
ExplicitLifePalace(EarthlyBranch),
}
pub fn build_minimal_natal_chart(input: NatalChartInput) -> Result<Chart, ChartError> {
build_minimal_natal_chart_with_anchor(input, NatalChartAnchor::CalculatedLifePalace)
}
pub fn build_minimal_natal_chart_with_anchor(
input: NatalChartInput,
anchor: NatalChartAnchor,
) -> Result<Chart, ChartError> {
let empty_chart = build_empty_chart(
input.birth_context().clone(),
StemBranch::try_new(input.birth_year_stem(), input.birth_year_branch()).map_err(|err| {
match err {
crate::core::model::ganzhi::StemBranchError::InvalidStemBranchPair {
stem,
branch,
} => ChartError::InvalidStemBranchPair { stem, branch },
}
})?,
input.method_profile().clone(),
)?;
let indices = calculate_life_body_palace_indices(LunarBirthContext::new(
input.lunar_month(),
input.birth_context().birth_time(),
))?;
let life_branch = match anchor {
NatalChartAnchor::CalculatedLifePalace => indices.life_palace_branch(),
NatalChartAnchor::ExplicitLifePalace(branch) => branch,
};
let year_stem = input.birth_year_stem();
let palaces = empty_chart
.palaces()
.iter()
.map(|palace| {
let branch = palace.branch();
Palace::new(
palace_name_relative_to_life_branch(branch, life_branch),
branch,
palace_stem_for_branch(year_stem, branch),
palace.stars().to_vec(),
)
})
.collect();
let life_pair = StemBranch::try_new(
palace_stem_for_branch(year_stem, life_branch),
life_branch,
)
.map_err(|err| match err {
crate::core::model::ganzhi::StemBranchError::InvalidStemBranchPair { stem, branch } => {
ChartError::InvalidStemBranchPair { stem, branch }
}
})?;
let five_element_bureau = five_element_bureau_from_life_palace(life_pair);
Chart::try_new(
input.birth_context().clone(),
empty_chart.birth_year(),
input.method_profile().clone(),
palaces,
Some(indices.body_palace_branch()),
Some(five_element_bureau),
)
}
fn palace_name_relative_to_life_branch(
branch: EarthlyBranch,
life_branch: EarthlyBranch,
) -> PalaceName {
let offset = (life_branch.index() + PALACE_COUNT - branch.index()) % PALACE_COUNT;
PalaceName::from_index(offset)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::model::calendar::{CalendarDate, Gender};
use crate::core::model::ganzhi::HeavenlyStem;
use crate::core::placement::natal::life_body::LunarMonth;
fn fixture_input() -> NatalChartInput {
let birth_context = BirthContext::new(
CalendarDate::solar(1990, 5, 17),
EarthlyBranch::Chen,
Gender::Female,
);
NatalChartInput::new(
birth_context,
MethodProfile::placeholder("anchor_unit_test"),
LunarMonth::new(4).expect("valid lunar month"),
HeavenlyStem::Geng,
EarthlyBranch::Wu,
)
}
fn life_branch(chart: &Chart) -> EarthlyBranch {
chart
.life_palace()
.expect("chart should have a Life Palace")
.branch()
}
#[test]
fn calculated_anchor_matches_default_builder() {
let calculated = build_minimal_natal_chart(fixture_input()).expect("calculated chart");
let via_anchor = build_minimal_natal_chart_with_anchor(
fixture_input(),
NatalChartAnchor::CalculatedLifePalace,
)
.expect("anchor chart");
assert_eq!(calculated, via_anchor);
}
#[test]
fn explicit_anchor_moves_life_palace_but_preserves_body_palace() {
let calculated = build_minimal_natal_chart(fixture_input()).expect("calculated chart");
let original_body = calculated
.body_palace_branch()
.expect("calculated chart should have a Body Palace branch");
let target = if life_branch(&calculated) == EarthlyBranch::Zi {
EarthlyBranch::Wu
} else {
EarthlyBranch::Zi
};
let reanchored = build_minimal_natal_chart_with_anchor(
fixture_input(),
NatalChartAnchor::ExplicitLifePalace(target),
)
.expect("re-anchored chart");
assert_eq!(life_branch(&reanchored), target);
assert_eq!(reanchored.body_palace_branch(), Some(original_body));
assert_ne!(life_branch(&reanchored), life_branch(&calculated));
}
}