use crate::core::calculation::{HoroscopeCalculationDiagnosticSnapshot, NominalAgeBoundary};
use crate::core::error::ChartError;
use crate::core::model::calendar::{BirthTime, SolarDay, SolarMonth};
use crate::core::model::chart::{
Chart, HoroscopeChart, HoroscopeLunarDate, HoroscopeSolarDate, HoroscopeTargetContext,
TemporalLayer, build_age_period, build_daily_period, build_decadal_frame, build_hourly_period,
build_monthly_period, build_yearly_period, nominal_age_for_target,
select_decadal_period_by_age, target_lunar_date,
};
use crate::core::placement::overlay::age::build_age_horoscope_layer;
use crate::core::placement::overlay::daily_horoscope::build_daily_horoscope_layer;
use crate::core::placement::overlay::decadal_horoscope::build_decadal_horoscope_layer;
use crate::core::placement::overlay::hourly_horoscope::build_hourly_horoscope_layer;
use crate::core::placement::overlay::monthly_horoscope::build_monthly_horoscope_layer;
use crate::core::placement::overlay::yearly_horoscope::build_yearly_horoscope_layer;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct HoroscopeStackInput {
target_solar_year: i32,
target_solar_month: SolarMonth,
target_solar_day: SolarDay,
target_time: BirthTime,
nominal_age_boundary: NominalAgeBoundary,
}
impl HoroscopeStackInput {
pub const fn new(
target_solar_year: i32,
target_solar_month: SolarMonth,
target_solar_day: SolarDay,
target_time: BirthTime,
) -> Self {
Self {
target_solar_year,
target_solar_month,
target_solar_day,
target_time,
nominal_age_boundary: NominalAgeBoundary::NaturalYear,
}
}
pub const fn with_nominal_age_boundary(
mut self,
nominal_age_boundary: NominalAgeBoundary,
) -> Self {
self.nominal_age_boundary = nominal_age_boundary;
self
}
pub const fn nominal_age_boundary(&self) -> NominalAgeBoundary {
self.nominal_age_boundary
}
pub const fn target_solar_year(&self) -> i32 {
self.target_solar_year
}
pub const fn target_solar_month(&self) -> SolarMonth {
self.target_solar_month
}
pub const fn target_solar_day(&self) -> SolarDay {
self.target_solar_day
}
pub const fn target_time(&self) -> BirthTime {
self.target_time
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HoroscopeGenerationReport {
pub horoscope: HoroscopeChart,
pub calculation: HoroscopeCalculationDiagnosticSnapshot,
}
pub fn build_full_horoscope_chart(
natal: Chart,
input: HoroscopeStackInput,
) -> Result<HoroscopeChart, ChartError> {
Ok(build_full_horoscope_chart_report(natal, input)?.horoscope)
}
pub fn build_full_horoscope_chart_report(
natal: Chart,
input: HoroscopeStackInput,
) -> Result<HoroscopeGenerationReport, ChartError> {
let target_lunar = target_lunar_date(
input.target_solar_year,
input.target_solar_month,
input.target_solar_day,
)?;
let nominal_age = nominal_age_for_target(
&natal,
target_lunar.year,
target_lunar.month,
target_lunar.day,
input.nominal_age_boundary,
)?;
let decadal_frame = build_decadal_frame(&natal)?;
let decadal_period = select_decadal_period_by_age(&decadal_frame, nominal_age)?;
let decadal_layer = build_decadal_horoscope_layer(&natal, decadal_period)?;
let age_period = build_age_period(&natal, nominal_age)?;
let age_layer = build_age_horoscope_layer(&natal, &age_period)?;
let yearly_period = build_yearly_period(target_lunar.year)?;
let yearly_layer = build_yearly_horoscope_layer(&natal, &yearly_period)?;
let monthly_period = build_monthly_period(
&natal,
input.target_solar_year,
input.target_solar_month,
input.target_solar_day,
input.target_time,
)?;
let monthly_layer = build_monthly_horoscope_layer(&natal, &monthly_period)?;
let daily_period = build_daily_period(
&natal,
input.target_solar_year,
input.target_solar_month,
input.target_solar_day,
input.target_time,
)?;
let daily_layer = build_daily_horoscope_layer(&natal, &daily_period)?;
let hourly_period = build_hourly_period(
&natal,
input.target_solar_year,
input.target_solar_month,
input.target_solar_day,
input.target_time,
)?;
let hourly_layer = build_hourly_horoscope_layer(&natal, &hourly_period)?;
let layers: Vec<TemporalLayer> = vec![
decadal_layer,
age_layer,
yearly_layer,
monthly_layer,
daily_layer,
hourly_layer,
];
let target_context = HoroscopeTargetContext::new(
HoroscopeSolarDate::new(
input.target_solar_year,
input.target_solar_month.value(),
input.target_solar_day.value(),
),
HoroscopeLunarDate::new(
target_lunar.year,
target_lunar.month,
target_lunar.day,
target_lunar.is_leap_month,
),
input.target_time.iztro_time_index(),
);
let target_solar_date = HoroscopeSolarDate::new(
input.target_solar_year,
input.target_solar_month.value(),
input.target_solar_day.value(),
);
let target_lunar_date = HoroscopeLunarDate::new(
target_lunar.year,
target_lunar.month,
target_lunar.day,
target_lunar.is_leap_month,
);
let calculation = HoroscopeCalculationDiagnosticSnapshot {
target_solar_date,
target_lunar_date,
nominal_age_boundary: input.nominal_age_boundary,
resolved_nominal_age: nominal_age,
};
let horoscope = HoroscopeChart::with_layers_and_target_context(natal, layers, target_context);
Ok(HoroscopeGenerationReport {
horoscope,
calculation,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::facade::by_solar::{SolarChartRequest, by_solar};
use crate::core::model::calendar::Gender;
use crate::core::model::chart::TemporalContext;
use crate::core::model::profile::{ChartAlgorithmKind, MethodProfile};
use crate::core::model::star::mutagen::Scope;
fn natal_born_1985_02_15() -> Chart {
by_solar(
SolarChartRequest::builder()
.solar_year(1985)
.solar_month(SolarMonth::new(2).expect("valid month"))
.solar_day(SolarDay::new(15).expect("valid day"))
.birth_time(crate::core::model::ganzhi::EarthlyBranch::Chen)
.gender(Gender::Female)
.method_profile(MethodProfile::new(
"quanshu_test",
ChartAlgorithmKind::QuanShu,
"quanshu test",
))
.build()
.expect("request should build"),
)
.expect("natal chart should build")
}
fn stack_input(boundary: NominalAgeBoundary) -> HoroscopeStackInput {
HoroscopeStackInput::new(
2000,
SolarMonth::new(6).expect("valid month"),
SolarDay::new(1).expect("valid day"),
BirthTime::from_iztro_time_index(2).expect("valid time index"),
)
.with_nominal_age_boundary(boundary)
}
fn age_layer_nominal_age(chart: &HoroscopeChart) -> u8 {
chart
.layers_in_scope(Scope::Age)
.find_map(|layer| match layer.context() {
TemporalContext::Age { nominal_age, .. } => Some(*nominal_age),
_ => None,
})
.expect("horoscope should expose a nominal-age layer")
}
#[test]
fn nominal_age_boundary_threads_into_full_horoscope() {
let natural = build_full_horoscope_chart(
natal_born_1985_02_15(),
stack_input(NominalAgeBoundary::NaturalYear),
)
.expect("natural-year horoscope should build");
let birthday = build_full_horoscope_chart(
natal_born_1985_02_15(),
stack_input(NominalAgeBoundary::Birthday),
)
.expect("birthday horoscope should build");
assert_eq!(
age_layer_nominal_age(&natural),
age_layer_nominal_age(&birthday) + 1,
"the birthday boundary should be one nominal year behind before the birthday",
);
}
#[test]
fn default_nominal_age_boundary_is_natural_year() {
let default_chart = build_full_horoscope_chart(
natal_born_1985_02_15(),
HoroscopeStackInput::new(
2000,
SolarMonth::new(6).expect("valid month"),
SolarDay::new(1).expect("valid day"),
BirthTime::from_iztro_time_index(2).expect("valid time index"),
),
)
.expect("default horoscope should build");
let natural = build_full_horoscope_chart(
natal_born_1985_02_15(),
stack_input(NominalAgeBoundary::NaturalYear),
)
.expect("natural horoscope should build");
assert_eq!(
age_layer_nominal_age(&default_chart),
age_layer_nominal_age(&natural),
);
}
}