use crate::core::model::ganzhi::{EarthlyBranch, FourPillars, HeavenlyStem, StemBranch};
use crate::core::{
error::ChartError,
model::{
bureau::FiveElementBureau,
calendar::BirthContext,
chart::ChartDiagnosticSnapshot,
chart::horoscope::{HoroscopeLunarDate, HoroscopeSolarDate},
chart::palace::PalaceName,
chart::snapshot::ChartStackSnapshot,
profile::{ChartAlgorithmKind, ChartPlane, ChartProfile, MethodProfile},
star::mutagen::{Mutagen, Scope},
star::{
Brightness, KnownStarFamily, StarCategory, StarKind, StarName, try_known_star_metadata,
},
},
};
use serde::{Deserialize, Deserializer, Serialize};
pub const PALACE_COUNT: usize = 12;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Chart {
birth_context: BirthContext,
birth_year: StemBranch,
#[serde(default)]
four_pillars: Option<FourPillars>,
#[serde(flatten)]
chart_profile: ChartProfile,
palaces: Vec<Palace>,
body_palace_branch: Option<EarthlyBranch>,
five_element_bureau: Option<FiveElementBureau>,
#[serde(default, skip_serializing_if = "Option::is_none")]
natal_date_facts: Option<NatalDateFacts>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct NatalDateFacts {
solar: HoroscopeSolarDate,
lunar: HoroscopeLunarDate,
}
impl NatalDateFacts {
pub const fn new(solar: HoroscopeSolarDate, lunar: HoroscopeLunarDate) -> Self {
Self { solar, lunar }
}
pub const fn solar(&self) -> HoroscopeSolarDate {
self.solar
}
pub const fn lunar(&self) -> HoroscopeLunarDate {
self.lunar
}
}
impl Chart {
pub fn try_new(
birth_context: BirthContext,
birth_year: StemBranch,
method_profile: MethodProfile,
palaces: Vec<Palace>,
body_palace_branch: Option<EarthlyBranch>,
five_element_bureau: Option<FiveElementBureau>,
) -> Result<Self, ChartError> {
Self::try_new_with_four_pillars(
birth_context,
birth_year,
None,
method_profile,
palaces,
body_palace_branch,
five_element_bureau,
)
}
pub fn try_new_with_four_pillars(
birth_context: BirthContext,
birth_year: StemBranch,
four_pillars: Option<FourPillars>,
method_profile: MethodProfile,
palaces: Vec<Palace>,
body_palace_branch: Option<EarthlyBranch>,
five_element_bureau: Option<FiveElementBureau>,
) -> Result<Self, ChartError> {
Self::try_new_with_four_pillars_and_profile(
birth_context,
birth_year,
four_pillars,
ChartProfile::new(method_profile, ChartPlane::Heaven),
palaces,
body_palace_branch,
five_element_bureau,
)
}
pub fn try_new_with_profile(
birth_context: BirthContext,
birth_year: StemBranch,
chart_profile: ChartProfile,
palaces: Vec<Palace>,
body_palace_branch: Option<EarthlyBranch>,
five_element_bureau: Option<FiveElementBureau>,
) -> Result<Self, ChartError> {
Self::try_new_with_four_pillars_and_profile(
birth_context,
birth_year,
None,
chart_profile,
palaces,
body_palace_branch,
five_element_bureau,
)
}
pub fn try_new_with_four_pillars_and_profile(
birth_context: BirthContext,
birth_year: StemBranch,
four_pillars: Option<FourPillars>,
chart_profile: ChartProfile,
palaces: Vec<Palace>,
body_palace_branch: Option<EarthlyBranch>,
five_element_bureau: Option<FiveElementBureau>,
) -> Result<Self, ChartError> {
if palaces.len() != PALACE_COUNT {
return Err(ChartError::InvalidPalaceCount {
expected: PALACE_COUNT,
actual: palaces.len(),
});
}
if let Some(pillars) = four_pillars {
if pillars.yearly != birth_year {
return Err(ChartError::FourPillarsBirthYearMismatch {
birth_year,
four_pillars_year: pillars.yearly,
});
}
}
Ok(Self {
birth_context,
birth_year,
four_pillars,
chart_profile,
palaces,
body_palace_branch,
five_element_bureau,
natal_date_facts: None,
})
}
pub fn with_natal_date_facts(mut self, facts: NatalDateFacts) -> Self {
self.natal_date_facts = Some(facts);
self
}
pub(crate) fn with_chart_profile(mut self, chart_profile: ChartProfile) -> Self {
self.chart_profile = chart_profile;
self
}
pub const fn natal_date_facts(&self) -> Option<&NatalDateFacts> {
self.natal_date_facts.as_ref()
}
pub const fn birth_context(&self) -> &BirthContext {
&self.birth_context
}
pub const fn birth_year(&self) -> StemBranch {
self.birth_year
}
pub const fn four_pillars(&self) -> Option<&FourPillars> {
self.four_pillars.as_ref()
}
pub const fn chart_profile(&self) -> &ChartProfile {
&self.chart_profile
}
pub const fn method_profile(&self) -> &MethodProfile {
self.chart_profile.method_profile()
}
pub const fn chart_plane(&self) -> ChartPlane {
self.chart_profile.chart_plane()
}
pub const fn algorithm_kind(&self) -> ChartAlgorithmKind {
self.chart_profile.algorithm_kind()
}
pub fn palaces(&self) -> &[Palace] {
&self.palaces
}
pub fn stack_snapshot(&self) -> ChartStackSnapshot {
ChartStackSnapshot::from_natal_chart(self)
}
pub fn diagnostic_snapshot(&self) -> ChartDiagnosticSnapshot {
ChartDiagnosticSnapshot::from_chart(self)
}
pub const fn body_palace_branch(&self) -> Option<EarthlyBranch> {
self.body_palace_branch
}
pub fn is_body_palace_branch(&self, branch: EarthlyBranch) -> bool {
self.body_palace_branch == Some(branch)
}
pub fn body_palace(&self) -> Option<&Palace> {
let body_branch = self.body_palace_branch?;
self.palace_by_branch(body_branch)
}
pub const fn five_element_bureau(&self) -> Option<FiveElementBureau> {
self.five_element_bureau
}
pub fn life_palace(&self) -> Option<&Palace> {
self.palace_by_name(PalaceName::Life)
}
pub fn palace_by_name(&self, name: PalaceName) -> Option<&Palace> {
self.palaces.iter().find(|palace| palace.name() == name)
}
pub fn palace_by_branch(&self, branch: EarthlyBranch) -> Option<&Palace> {
self.palaces.iter().find(|palace| palace.branch() == branch)
}
pub fn branch_of_palace(&self, name: PalaceName) -> Option<EarthlyBranch> {
self.palace_by_name(name).map(Palace::branch)
}
pub fn palace_name_at_branch(&self, branch: EarthlyBranch) -> Option<PalaceName> {
self.palace_by_branch(branch).map(Palace::name)
}
pub fn required_palace_by_name(&self, name: PalaceName) -> Result<&Palace, ChartError> {
self.palace_by_name(name)
.ok_or(ChartError::RequiredPalaceNameMissing { palace_name: name })
}
pub fn required_palace_by_branch(&self, branch: EarthlyBranch) -> Result<&Palace, ChartError> {
self.palace_by_branch(branch)
.ok_or(ChartError::RequiredPalaceBranchMissing { branch })
}
pub fn major_stars(&self) -> Vec<MajorStarPlacementRef<'_>> {
self.stars_by_category(StarCategory::Major)
}
pub fn major_star(&self, name: StarName) -> Option<MajorStarPlacementRef<'_>> {
self.star(name)
.filter(|fact| fact.placement().category() == StarCategory::Major)
}
pub fn palace_by_major_star(&self, name: StarName) -> Option<&Palace> {
self.major_star(name).map(|fact| fact.palace())
}
pub fn major_stars_in_palace(&self, name: PalaceName) -> Vec<MajorStarPlacementRef<'_>> {
self.stars_in_palace(name)
.into_iter()
.filter(|fact| fact.placement().category() == StarCategory::Major)
.collect()
}
pub fn major_stars_in_branch(&self, branch: EarthlyBranch) -> Vec<MajorStarPlacementRef<'_>> {
self.stars_in_branch(branch)
.into_iter()
.filter(|fact| fact.placement().category() == StarCategory::Major)
.collect()
}
pub fn stars(&self) -> Vec<StarPlacementRef<'_>> {
self.palaces.iter().flat_map(stars_in).collect()
}
pub fn star(&self, name: StarName) -> Option<StarPlacementRef<'_>> {
self.palaces.iter().find_map(|palace| {
palace
.stars()
.iter()
.find(|star| star.name() == name)
.map(|placement| StarPlacementRef::new(palace, placement))
})
}
pub fn palace_containing_star(&self, name: StarName) -> Option<&Palace> {
self.star(name).map(|fact| fact.palace())
}
pub fn stars_in_palace(&self, name: PalaceName) -> Vec<StarPlacementRef<'_>> {
self.palaces
.iter()
.filter(|palace| palace.name() == name)
.flat_map(stars_in)
.collect()
}
pub fn stars_in_branch(&self, branch: EarthlyBranch) -> Vec<StarPlacementRef<'_>> {
self.palaces
.iter()
.filter(|palace| palace.branch() == branch)
.flat_map(stars_in)
.collect()
}
pub fn stars_by_category(&self, category: StarCategory) -> Vec<StarPlacementRef<'_>> {
self.stars()
.into_iter()
.filter(|fact| fact.placement().category() == category)
.collect()
}
pub fn stars_by_kind(&self, kind: StarKind) -> Vec<StarPlacementRef<'_>> {
self.stars()
.into_iter()
.filter(|fact| fact.placement().kind() == kind)
.collect()
}
pub fn decorative_stars(&self) -> Vec<DecorativeStarPlacementRef<'_>> {
self.palaces.iter().flat_map(decorative_stars_in).collect()
}
pub fn decorative_star(&self, name: StarName) -> Option<DecorativeStarPlacementRef<'_>> {
self.palaces.iter().find_map(|palace| {
palace
.decorative_stars()
.iter()
.find(|star| star.name() == name)
.map(|placement| DecorativeStarPlacementRef::new(palace, placement))
})
}
}
fn stars_in(palace: &Palace) -> impl Iterator<Item = StarPlacementRef<'_>> {
palace
.stars()
.iter()
.map(|placement| StarPlacementRef::new(palace, placement))
}
fn decorative_stars_in(palace: &Palace) -> impl Iterator<Item = DecorativeStarPlacementRef<'_>> {
palace
.decorative_stars()
.iter()
.map(|placement| DecorativeStarPlacementRef::new(palace, placement))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StarPlacementRef<'a> {
palace: &'a Palace,
placement: &'a StarPlacement,
}
impl<'a> StarPlacementRef<'a> {
pub const fn new(palace: &'a Palace, placement: &'a StarPlacement) -> Self {
Self { palace, placement }
}
pub const fn palace(&self) -> &'a Palace {
self.palace
}
pub const fn placement(&self) -> &'a StarPlacement {
self.placement
}
}
pub type MajorStarPlacementRef<'a> = StarPlacementRef<'a>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DecorativeStarPlacementRef<'a> {
palace: &'a Palace,
placement: &'a DecorativeStarPlacement,
}
impl<'a> DecorativeStarPlacementRef<'a> {
pub const fn new(palace: &'a Palace, placement: &'a DecorativeStarPlacement) -> Self {
Self { palace, placement }
}
pub const fn palace(&self) -> &'a Palace {
self.palace
}
pub const fn placement(&self) -> &'a DecorativeStarPlacement {
self.placement
}
pub const fn branch(&self) -> EarthlyBranch {
self.palace.branch()
}
pub const fn name(&self) -> StarName {
self.placement.name()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Palace {
name: PalaceName,
branch: EarthlyBranch,
stem: HeavenlyStem,
stars: Vec<StarPlacement>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
decorative_stars: Vec<DecorativeStarPlacement>,
}
impl Palace {
pub fn new(
name: PalaceName,
branch: EarthlyBranch,
stem: HeavenlyStem,
stars: Vec<StarPlacement>,
) -> Self {
Self {
name,
branch,
stem,
stars,
decorative_stars: Vec::new(),
}
}
pub fn with_decorative_stars(mut self, decorative_stars: Vec<DecorativeStarPlacement>) -> Self {
self.decorative_stars = decorative_stars;
self
}
pub const fn name(&self) -> PalaceName {
self.name
}
pub const fn branch(&self) -> EarthlyBranch {
self.branch
}
pub const fn stem(&self) -> HeavenlyStem {
self.stem
}
pub fn stars(&self) -> &[StarPlacement] {
&self.stars
}
pub fn decorative_stars(&self) -> &[DecorativeStarPlacement] {
&self.decorative_stars
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct StarPlacement {
name: StarName,
kind: StarKind,
brightness: Brightness,
mutagen: Option<Mutagen>,
scope: Scope,
}
impl StarPlacement {
pub const fn new(
name: StarName,
kind: StarKind,
brightness: Brightness,
mutagen: Option<Mutagen>,
scope: Scope,
) -> Self {
Self {
name,
kind,
brightness,
mutagen,
scope,
}
}
pub const fn name(&self) -> StarName {
self.name
}
pub const fn kind(&self) -> StarKind {
self.kind
}
pub const fn category(&self) -> StarCategory {
self.kind.category()
}
pub const fn brightness(&self) -> Brightness {
self.brightness
}
pub const fn mutagen(&self) -> Option<Mutagen> {
self.mutagen
}
pub const fn scope(&self) -> Scope {
self.scope
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecorativeStarFamily {
Changsheng12,
Boshi12,
Suiqian12,
Jiangqian12,
}
impl DecorativeStarFamily {
pub const fn known_family(self) -> KnownStarFamily {
match self {
Self::Changsheng12 => KnownStarFamily::Changsheng12,
Self::Boshi12 => KnownStarFamily::Boshi12,
Self::Suiqian12 => KnownStarFamily::Suiqian12,
Self::Jiangqian12 => KnownStarFamily::Jiangqian12,
}
}
pub const fn from_known_family(family: KnownStarFamily) -> Option<Self> {
match family {
KnownStarFamily::Changsheng12 => Some(Self::Changsheng12),
KnownStarFamily::Boshi12 => Some(Self::Boshi12),
KnownStarFamily::Suiqian12 => Some(Self::Suiqian12),
KnownStarFamily::Jiangqian12 => Some(Self::Jiangqian12),
_ => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DecorativeStarPlacement {
name: StarName,
family: DecorativeStarFamily,
scope: Scope,
}
impl DecorativeStarPlacement {
pub fn try_new(
name: StarName,
family: DecorativeStarFamily,
scope: Scope,
) -> Result<Self, ChartError> {
let metadata = try_known_star_metadata(name)
.ok_or(ChartError::InvalidDecorativeStarPlacement { star: name })?;
if metadata.family() != family.known_family() || metadata.kind().is_some() {
return Err(ChartError::InvalidDecorativeStarPlacement { star: name });
}
Ok(Self {
name,
family,
scope,
})
}
pub const fn name(&self) -> StarName {
self.name
}
pub const fn family(&self) -> DecorativeStarFamily {
self.family
}
pub const fn scope(&self) -> Scope {
self.scope
}
}
impl<'de> Deserialize<'de> for DecorativeStarPlacement {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct DecorativeStarPlacementData {
name: StarName,
family: DecorativeStarFamily,
scope: Scope,
}
let data = DecorativeStarPlacementData::deserialize(deserializer)?;
Self::try_new(data.name, data.family, data.scope).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::model::ganzhi::{EARTHLY_BRANCHES, HEAVENLY_STEMS};
use crate::core::model::{
calendar::{CalendarDate, Gender},
chart::PALACE_NAMES,
profile::ChartAlgorithmKind,
};
use serde_json::Value;
fn method_profile() -> MethodProfile {
MethodProfile::new("zhongzhou_test", ChartAlgorithmKind::Zhongzhou, "test")
}
fn sample_chart(chart_profile: ChartProfile) -> Chart {
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_with_profile(
BirthContext::new(
CalendarDate::solar(1990, 5, 17),
EarthlyBranch::Chen,
Gender::Female,
),
StemBranch::try_new(HeavenlyStem::Geng, EarthlyBranch::Wu)
.expect("valid sexagenary pair"),
chart_profile,
palaces,
None,
None,
)
.expect("sample chart should build")
}
#[test]
fn serialization_is_flattened_with_top_level_keys() {
let chart = sample_chart(ChartProfile::new(method_profile(), ChartPlane::Heaven));
let value: Value = serde_json::to_value(&chart).expect("chart should serialize");
let object = value
.as_object()
.expect("chart should serialize to an object");
assert!(
object.contains_key("method_profile"),
"expected top-level method_profile key",
);
assert!(
object.contains_key("chart_plane"),
"expected top-level chart_plane key",
);
assert!(
!object.contains_key("chart_profile"),
"chart_profile must not appear as a nested key",
);
assert_eq!(object["chart_plane"], Value::String("heaven".to_owned()));
}
#[test]
fn old_json_without_chart_plane_deserializes_as_heaven() {
let chart = sample_chart(ChartProfile::new(method_profile(), ChartPlane::Heaven));
let mut value: Value = serde_json::to_value(&chart).expect("chart should serialize");
value
.as_object_mut()
.expect("chart object")
.remove("chart_plane");
let decoded: Chart = serde_json::from_value(value).expect("legacy JSON should deserialize");
assert_eq!(decoded.chart_plane(), ChartPlane::Heaven);
assert_eq!(decoded.method_profile(), &method_profile());
}
#[test]
fn json_with_earth_chart_plane_deserializes_as_earth() {
let chart = sample_chart(ChartProfile::new(method_profile(), ChartPlane::Heaven));
let mut value: Value = serde_json::to_value(&chart).expect("chart should serialize");
value
.as_object_mut()
.expect("chart object")
.insert("chart_plane".to_owned(), Value::String("earth".to_owned()));
let decoded: Chart = serde_json::from_value(value).expect("JSON should deserialize");
assert_eq!(decoded.chart_plane(), ChartPlane::Earth);
}
#[test]
fn round_trip_preserves_chart_profile() {
let original = sample_chart(ChartProfile::new(method_profile(), ChartPlane::Human));
let encoded = serde_json::to_string(&original).expect("chart should serialize");
let decoded: Chart = serde_json::from_str(&encoded).expect("chart should deserialize");
assert_eq!(decoded.chart_profile(), original.chart_profile());
assert_eq!(decoded.chart_plane(), ChartPlane::Human);
assert_eq!(decoded.method_profile(), original.method_profile());
assert_eq!(decoded, original);
}
}