use crate::core::model::ganzhi::{EarthlyBranch, FourPillars, HeavenlyStem, StemBranch};
use crate::core::{
error::ChartError,
labels::zh_cn,
model::{
bureau::FiveElementBureau,
calendar::Gender,
chart::{
Chart, DecorativeStarFamily, DecorativeStarPlacement, HoroscopeChart,
HoroscopeLunarDate, HoroscopePalaceProjection, HoroscopeProjectionMutagenActivation,
HoroscopeRuntime, HoroscopeSolarDate, HoroscopeSupportedFieldsSnapshot,
HoroscopeSurroundPalaces, Palace, PalaceName, StarPlacement, TemporalContext,
},
star::{
Brightness, StarCategory, StarKind, StarName,
mutagen::{Mutagen, Scope},
},
},
};
use serde::{Deserialize, Serialize};
const FACADE_PROJECTION_SCOPES: [Scope; 6] = [
Scope::Natal,
Scope::Decadal,
Scope::Yearly,
Scope::Monthly,
Scope::Daily,
Scope::Hourly,
];
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct HoroscopeFacadeSnapshot {
#[serde(flatten)]
supported_fields: HoroscopeSupportedFieldsSnapshot,
context: HoroscopeFacadeContext,
astrolabe: NatalFacadeSnapshot,
age_palace: HoroscopePalaceProjectionSnapshot,
palace_projections: Vec<HoroscopePalaceProjectionSnapshot>,
surround_palaces: Vec<HoroscopeSurroundPalacesSnapshot>,
}
impl HoroscopeFacadeSnapshot {
pub fn from_horoscope_chart(chart: &HoroscopeChart) -> Result<Self, ChartError> {
let supported_fields = HoroscopeSupportedFieldsSnapshot::from_horoscope_chart(chart)?;
let context = HoroscopeFacadeContext::from_horoscope_chart(chart)?;
let astrolabe = NatalFacadeSnapshot::from_chart(chart.natal());
let runtime = HoroscopeRuntime::new(chart)?;
let age_palace = HoroscopePalaceProjectionSnapshot::from_projection(&runtime.age_palace()?);
let mut palace_projections = Vec::with_capacity(FACADE_PROJECTION_SCOPES.len());
let mut surround_palaces = Vec::with_capacity(FACADE_PROJECTION_SCOPES.len());
for scope in FACADE_PROJECTION_SCOPES {
palace_projections.push(HoroscopePalaceProjectionSnapshot::from_projection(
&runtime.palace(scope, PalaceName::Life)?,
));
surround_palaces.push(HoroscopeSurroundPalacesSnapshot::from_surround(
&runtime.surround_palaces(scope, PalaceName::Life)?,
));
}
Ok(Self {
supported_fields,
context,
astrolabe,
age_palace,
palace_projections,
surround_palaces,
})
}
pub const fn supported_fields(&self) -> &HoroscopeSupportedFieldsSnapshot {
&self.supported_fields
}
pub const fn context(&self) -> &HoroscopeFacadeContext {
&self.context
}
pub const fn astrolabe(&self) -> &NatalFacadeSnapshot {
&self.astrolabe
}
pub const fn age_palace(&self) -> &HoroscopePalaceProjectionSnapshot {
&self.age_palace
}
pub fn palace_projections(&self) -> &[HoroscopePalaceProjectionSnapshot] {
&self.palace_projections
}
pub fn surround_palaces(&self) -> &[HoroscopeSurroundPalacesSnapshot] {
&self.surround_palaces
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct NatalFacadeSnapshot {
gender: Gender,
birth_year_stem: HeavenlyStem,
birth_year_stem_zh: String,
birth_year_branch: EarthlyBranch,
birth_year_branch_zh: String,
five_element_bureau: Option<FiveElementBureau>,
life_palace_branch: Option<EarthlyBranch>,
life_palace_branch_zh: Option<String>,
body_palace_branch: Option<EarthlyBranch>,
body_palace_branch_zh: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
four_pillars: Option<NatalFacadeFourPillarsSnapshot>,
palaces: Vec<NatalFacadePalaceSnapshot>,
}
impl NatalFacadeSnapshot {
pub fn from_chart(chart: &Chart) -> Self {
let life_palace_branch = chart.life_palace().map(Palace::branch);
let body_palace_branch = chart.body_palace_branch();
Self {
gender: chart.birth_context().gender(),
birth_year_stem: chart.birth_year().stem(),
birth_year_stem_zh: zh_cn::heavenly_stem_zh(chart.birth_year().stem()).to_owned(),
birth_year_branch: chart.birth_year().branch(),
birth_year_branch_zh: zh_cn::earthly_branch_zh(chart.birth_year().branch()).to_owned(),
five_element_bureau: chart.five_element_bureau(),
life_palace_branch,
life_palace_branch_zh: life_palace_branch
.map(|branch| zh_cn::earthly_branch_zh(branch).to_owned()),
body_palace_branch,
body_palace_branch_zh: body_palace_branch
.map(|branch| zh_cn::earthly_branch_zh(branch).to_owned()),
four_pillars: chart
.four_pillars()
.map(NatalFacadeFourPillarsSnapshot::from_four_pillars),
palaces: chart
.palaces()
.iter()
.map(|palace| NatalFacadePalaceSnapshot::from_palace(chart, palace))
.collect(),
}
}
pub const fn gender(&self) -> Gender {
self.gender
}
pub const fn birth_year_stem(&self) -> HeavenlyStem {
self.birth_year_stem
}
pub fn birth_year_stem_zh(&self) -> &str {
&self.birth_year_stem_zh
}
pub const fn birth_year_branch(&self) -> EarthlyBranch {
self.birth_year_branch
}
pub fn birth_year_branch_zh(&self) -> &str {
&self.birth_year_branch_zh
}
pub const fn five_element_bureau(&self) -> Option<FiveElementBureau> {
self.five_element_bureau
}
pub const fn life_palace_branch(&self) -> Option<EarthlyBranch> {
self.life_palace_branch
}
pub fn life_palace_branch_zh(&self) -> Option<&str> {
self.life_palace_branch_zh.as_deref()
}
pub const fn body_palace_branch(&self) -> Option<EarthlyBranch> {
self.body_palace_branch
}
pub fn body_palace_branch_zh(&self) -> Option<&str> {
self.body_palace_branch_zh.as_deref()
}
pub const fn four_pillars(&self) -> Option<&NatalFacadeFourPillarsSnapshot> {
self.four_pillars.as_ref()
}
pub fn palaces(&self) -> &[NatalFacadePalaceSnapshot] {
&self.palaces
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct NatalFacadeFourPillarsSnapshot {
yearly: StemBranch,
yearly_zh: String,
monthly: StemBranch,
monthly_zh: String,
daily: StemBranch,
daily_zh: String,
hourly: StemBranch,
hourly_zh: String,
}
impl NatalFacadeFourPillarsSnapshot {
fn from_four_pillars(pillars: &FourPillars) -> Self {
Self {
yearly: pillars.yearly,
yearly_zh: zh_cn::stem_branch_zh(pillars.yearly),
monthly: pillars.monthly,
monthly_zh: zh_cn::stem_branch_zh(pillars.monthly),
daily: pillars.daily,
daily_zh: zh_cn::stem_branch_zh(pillars.daily),
hourly: pillars.hourly,
hourly_zh: zh_cn::stem_branch_zh(pillars.hourly),
}
}
pub const fn yearly(&self) -> StemBranch {
self.yearly
}
pub fn yearly_zh(&self) -> &str {
&self.yearly_zh
}
pub const fn monthly(&self) -> StemBranch {
self.monthly
}
pub fn monthly_zh(&self) -> &str {
&self.monthly_zh
}
pub const fn daily(&self) -> StemBranch {
self.daily
}
pub fn daily_zh(&self) -> &str {
&self.daily_zh
}
pub const fn hourly(&self) -> StemBranch {
self.hourly
}
pub fn hourly_zh(&self) -> &str {
&self.hourly_zh
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct NatalFacadePalaceSnapshot {
branch: EarthlyBranch,
branch_zh: String,
name: PalaceName,
name_zh: String,
stem: HeavenlyStem,
stem_zh: String,
roles: Vec<NatalFacadePalaceRole>,
typed_stars: Vec<NatalFacadeTypedStarSnapshot>,
decorative_stars: Vec<NatalFacadeDecorativeStarSnapshot>,
}
impl NatalFacadePalaceSnapshot {
fn from_palace(chart: &Chart, palace: &Palace) -> Self {
let mut roles = vec![NatalFacadePalaceRole::NatalPalace(palace.name())];
if chart.is_body_palace_branch(palace.branch()) {
roles.push(NatalFacadePalaceRole::NatalBodyPalace);
}
let mut typed_stars: Vec<NatalFacadeTypedStarSnapshot> = palace
.stars()
.iter()
.map(NatalFacadeTypedStarSnapshot::from_star_placement)
.collect();
order_facade_typed_stars(&mut typed_stars);
let mut decorative_stars: Vec<NatalFacadeDecorativeStarSnapshot> = palace
.decorative_stars()
.iter()
.map(NatalFacadeDecorativeStarSnapshot::from_decorative_star_placement)
.collect();
order_facade_decorative_stars(&mut decorative_stars);
Self {
branch: palace.branch(),
branch_zh: zh_cn::earthly_branch_zh(palace.branch()).to_owned(),
name: palace.name(),
name_zh: zh_cn::palace_name_zh(palace.name()).to_owned(),
stem: palace.stem(),
stem_zh: zh_cn::heavenly_stem_zh(palace.stem()).to_owned(),
roles,
typed_stars,
decorative_stars,
}
}
pub const fn branch(&self) -> EarthlyBranch {
self.branch
}
pub fn branch_zh(&self) -> &str {
&self.branch_zh
}
pub const fn name(&self) -> PalaceName {
self.name
}
pub fn name_zh(&self) -> &str {
&self.name_zh
}
pub const fn stem(&self) -> HeavenlyStem {
self.stem
}
pub fn stem_zh(&self) -> &str {
&self.stem_zh
}
pub fn roles(&self) -> &[NatalFacadePalaceRole] {
&self.roles
}
pub fn typed_stars(&self) -> &[NatalFacadeTypedStarSnapshot] {
&self.typed_stars
}
pub fn decorative_stars(&self) -> &[NatalFacadeDecorativeStarSnapshot] {
&self.decorative_stars
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(tag = "kind", content = "palace_name", rename_all = "snake_case")]
pub enum NatalFacadePalaceRole {
NatalPalace(PalaceName),
NatalBodyPalace,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct NatalFacadeTypedStarSnapshot {
name: StarName,
name_zh: String,
kind: StarKind,
kind_zh: String,
category: StarCategory,
brightness: Brightness,
brightness_zh: String,
mutagen: Option<Mutagen>,
mutagen_zh: Option<String>,
scope: Scope,
}
impl NatalFacadeTypedStarSnapshot {
fn from_star_placement(placement: &StarPlacement) -> Self {
Self {
name: placement.name(),
name_zh: zh_cn::star_name_zh(placement.name()).to_owned(),
kind: placement.kind(),
kind_zh: zh_cn::star_kind_zh(placement.kind()).to_owned(),
category: placement.category(),
brightness: placement.brightness(),
brightness_zh: zh_cn::brightness_zh(placement.brightness()).to_owned(),
mutagen: placement.mutagen(),
mutagen_zh: placement
.mutagen()
.map(|mutagen| zh_cn::mutagen_zh(mutagen).to_owned()),
scope: placement.scope(),
}
}
pub const fn name(&self) -> StarName {
self.name
}
pub fn name_zh(&self) -> &str {
&self.name_zh
}
pub const fn kind(&self) -> StarKind {
self.kind
}
pub fn kind_zh(&self) -> &str {
&self.kind_zh
}
pub const fn category(&self) -> StarCategory {
self.category
}
pub const fn brightness(&self) -> Brightness {
self.brightness
}
pub fn brightness_zh(&self) -> &str {
&self.brightness_zh
}
pub const fn mutagen(&self) -> Option<Mutagen> {
self.mutagen
}
pub fn mutagen_zh(&self) -> Option<&str> {
self.mutagen_zh.as_deref()
}
pub const fn scope(&self) -> Scope {
self.scope
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct NatalFacadeDecorativeStarSnapshot {
name: StarName,
name_zh: String,
family: DecorativeStarFamily,
family_zh: String,
scope: Scope,
}
impl NatalFacadeDecorativeStarSnapshot {
fn from_decorative_star_placement(placement: &DecorativeStarPlacement) -> Self {
Self {
name: placement.name(),
name_zh: zh_cn::star_name_zh(placement.name()).to_owned(),
family: placement.family(),
family_zh: zh_cn::decorative_star_family_zh(placement.family()).to_owned(),
scope: placement.scope(),
}
}
pub const fn name(&self) -> StarName {
self.name
}
pub fn name_zh(&self) -> &str {
&self.name_zh
}
pub const fn family(&self) -> DecorativeStarFamily {
self.family
}
pub fn family_zh(&self) -> &str {
&self.family_zh
}
pub const fn scope(&self) -> Scope {
self.scope
}
}
fn order_facade_typed_stars(stars: &mut [NatalFacadeTypedStarSnapshot]) {
stars.sort_by_key(|star| (star.kind, star.name, star.brightness, star.mutagen));
}
fn order_facade_decorative_stars(stars: &mut [NatalFacadeDecorativeStarSnapshot]) {
stars.sort_by_key(|star| (star.family, star.name));
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct HoroscopeFacadeContext {
solar_date: Option<HoroscopeSolarDate>,
lunar_date: HoroscopeLunarDate,
time_index: Option<u8>,
}
impl HoroscopeFacadeContext {
fn from_horoscope_chart(chart: &HoroscopeChart) -> Result<Self, ChartError> {
if let Some(context) = chart.target_context() {
return Ok(Self {
solar_date: Some(context.solar_date()),
lunar_date: context.lunar_date(),
time_index: Some(context.time_index()),
});
}
Ok(Self {
solar_date: None,
lunar_date: HoroscopeLunarDate::new(
lunar_year(chart)?,
lunar_month(chart)?,
lunar_day(chart)?,
false,
),
time_index: None,
})
}
pub const fn solar_date(&self) -> Option<HoroscopeSolarDate> {
self.solar_date
}
pub const fn lunar_date(&self) -> HoroscopeLunarDate {
self.lunar_date
}
pub const fn time_index(&self) -> Option<u8> {
self.time_index
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct HoroscopePalaceProjectionSnapshot {
scope: Scope,
requested_palace_name: PalaceName,
branch: EarthlyBranch,
natal_palace_name: PalaceName,
temporal_palace_name: Option<PalaceName>,
natal_palace_stem: HeavenlyStem,
natal_typed_stars: Vec<StarName>,
natal_decorative_stars: Vec<StarName>,
temporal_stars: Vec<StarName>,
temporal_decorative_stars: Vec<StarName>,
temporal_mutagen_activations: Vec<HoroscopeProjectionMutagenActivationSnapshot>,
}
impl HoroscopePalaceProjectionSnapshot {
fn from_projection(projection: &HoroscopePalaceProjection) -> Self {
Self {
scope: projection.scope(),
requested_palace_name: projection.requested_palace_name(),
branch: projection.branch(),
natal_palace_name: projection.natal_palace_name(),
temporal_palace_name: projection.temporal_palace_name(),
natal_palace_stem: projection.natal_palace_stem(),
natal_typed_stars: projection.natal_typed_stars().to_vec(),
natal_decorative_stars: projection.natal_decorative_stars().to_vec(),
temporal_stars: projection.temporal_stars().to_vec(),
temporal_decorative_stars: projection.temporal_decorative_stars().to_vec(),
temporal_mutagen_activations: projection
.temporal_mutagen_activations()
.iter()
.map(HoroscopeProjectionMutagenActivationSnapshot::from_activation)
.collect(),
}
}
pub const fn scope(&self) -> Scope {
self.scope
}
pub const fn requested_palace_name(&self) -> PalaceName {
self.requested_palace_name
}
pub const fn branch(&self) -> EarthlyBranch {
self.branch
}
pub const fn natal_palace_name(&self) -> PalaceName {
self.natal_palace_name
}
pub const fn temporal_palace_name(&self) -> Option<PalaceName> {
self.temporal_palace_name
}
pub const fn natal_palace_stem(&self) -> HeavenlyStem {
self.natal_palace_stem
}
pub fn natal_typed_stars(&self) -> &[StarName] {
&self.natal_typed_stars
}
pub fn natal_decorative_stars(&self) -> &[StarName] {
&self.natal_decorative_stars
}
pub fn temporal_stars(&self) -> &[StarName] {
&self.temporal_stars
}
pub fn temporal_decorative_stars(&self) -> &[StarName] {
&self.temporal_decorative_stars
}
pub fn temporal_mutagen_activations(&self) -> &[HoroscopeProjectionMutagenActivationSnapshot] {
&self.temporal_mutagen_activations
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct HoroscopeProjectionMutagenActivationSnapshot {
target_star: StarName,
mutagen: Mutagen,
}
impl HoroscopeProjectionMutagenActivationSnapshot {
fn from_activation(activation: &HoroscopeProjectionMutagenActivation) -> Self {
Self {
target_star: activation.target_star(),
mutagen: activation.mutagen(),
}
}
pub const fn target_star(&self) -> StarName {
self.target_star
}
pub const fn mutagen(&self) -> Mutagen {
self.mutagen
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct HoroscopeSurroundPalacesSnapshot {
scope: Scope,
requested_palace_name: PalaceName,
target: HoroscopePalaceProjectionSnapshot,
opposite: HoroscopePalaceProjectionSnapshot,
wealth: HoroscopePalaceProjectionSnapshot,
career: HoroscopePalaceProjectionSnapshot,
}
impl HoroscopeSurroundPalacesSnapshot {
fn from_surround(surround: &HoroscopeSurroundPalaces) -> Self {
let target = HoroscopePalaceProjectionSnapshot::from_projection(surround.target());
Self {
scope: target.scope(),
requested_palace_name: target.requested_palace_name(),
target,
opposite: HoroscopePalaceProjectionSnapshot::from_projection(surround.opposite()),
wealth: HoroscopePalaceProjectionSnapshot::from_projection(surround.wealth()),
career: HoroscopePalaceProjectionSnapshot::from_projection(surround.career()),
}
}
pub const fn scope(&self) -> Scope {
self.scope
}
pub const fn requested_palace_name(&self) -> PalaceName {
self.requested_palace_name
}
pub const fn target(&self) -> &HoroscopePalaceProjectionSnapshot {
&self.target
}
pub const fn opposite(&self) -> &HoroscopePalaceProjectionSnapshot {
&self.opposite
}
pub const fn wealth(&self) -> &HoroscopePalaceProjectionSnapshot {
&self.wealth
}
pub const fn career(&self) -> &HoroscopePalaceProjectionSnapshot {
&self.career
}
}
fn single_layer_context(
chart: &HoroscopeChart,
scope: Scope,
) -> Result<&TemporalContext, ChartError> {
let mut layers = chart.layers_in_scope(scope);
let layer = layers
.next()
.ok_or(ChartError::MissingHoroscopeLayer { scope })?;
if layers.next().is_some() {
return Err(ChartError::DuplicateHoroscopeLayer { scope });
}
Ok(layer.context())
}
fn lunar_year(chart: &HoroscopeChart) -> Result<i32, ChartError> {
match single_layer_context(chart, Scope::Yearly)? {
TemporalContext::Yearly { lunar_year, .. } => Ok(*lunar_year),
context => Err(ChartError::TemporalScopeMismatch {
layer: Scope::Yearly,
context: context.scope(),
}),
}
}
fn lunar_month(chart: &HoroscopeChart) -> Result<u8, ChartError> {
match single_layer_context(chart, Scope::Monthly)? {
TemporalContext::Monthly { lunar_month, .. } => Ok(*lunar_month),
context => Err(ChartError::TemporalScopeMismatch {
layer: Scope::Monthly,
context: context.scope(),
}),
}
}
fn lunar_day(chart: &HoroscopeChart) -> Result<u8, ChartError> {
match single_layer_context(chart, Scope::Daily)? {
TemporalContext::Daily { lunar_day, .. } => Ok(*lunar_day),
context => Err(ChartError::TemporalScopeMismatch {
layer: Scope::Daily,
context: context.scope(),
}),
}
}