use crate::core::calculation::YearBoundary;
use crate::core::calendar::{LunarConversion, solar_to_lunar_with_year_boundary};
use crate::core::error::ChartError;
use crate::core::facade::by_lunar::{LunarChartRequest, by_lunar};
use crate::core::model::calendar::{BirthTime, Gender, SolarDay, SolarMonth};
use crate::core::model::chart::{Chart, HoroscopeLunarDate, HoroscopeSolarDate, NatalDateFacts};
use crate::core::model::ganzhi::EarthlyBranch;
use crate::core::model::profile::{ChartPlane, MethodProfile};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SolarChartRequest {
solar_year: i32,
solar_month: SolarMonth,
solar_day: SolarDay,
birth_time: BirthTime,
gender: Gender,
fix_leap: bool,
year_boundary: YearBoundary,
method_profile: MethodProfile,
chart_plane: ChartPlane,
}
impl SolarChartRequest {
pub fn builder() -> SolarChartRequestBuilder {
SolarChartRequestBuilder::default()
}
pub const fn solar_year(&self) -> i32 {
self.solar_year
}
pub const fn solar_month(&self) -> SolarMonth {
self.solar_month
}
pub const fn solar_day(&self) -> SolarDay {
self.solar_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 fix_leap(&self) -> bool {
self.fix_leap
}
pub const fn year_boundary(&self) -> YearBoundary {
self.year_boundary
}
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 SolarChartRequestBuilder {
solar_year: Option<i32>,
solar_month: Option<SolarMonth>,
solar_day: Option<SolarDay>,
birth_time: Option<BirthTime>,
gender: Option<Gender>,
fix_leap: Option<bool>,
year_boundary: Option<YearBoundary>,
method_profile: Option<MethodProfile>,
chart_plane: Option<ChartPlane>,
}
impl SolarChartRequestBuilder {
pub fn solar_year(mut self, value: i32) -> Self {
self.solar_year = Some(value);
self
}
pub fn solar_month(mut self, value: SolarMonth) -> Self {
self.solar_month = Some(value);
self
}
pub fn solar_day(mut self, value: SolarDay) -> Self {
self.solar_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 fix_leap(mut self, value: bool) -> Self {
self.fix_leap = Some(value);
self
}
pub const fn year_boundary(mut self, value: YearBoundary) -> Self {
self.year_boundary = 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<SolarChartRequest, ChartError> {
Ok(SolarChartRequest {
solar_year: self.solar_year.ok_or(ChartError::MissingRequiredInput {
field: "solar_year",
})?,
solar_month: self.solar_month.ok_or(ChartError::MissingRequiredInput {
field: "solar_month",
})?,
solar_day: self
.solar_day
.ok_or(ChartError::MissingRequiredInput { field: "solar_day" })?,
birth_time: self.birth_time.ok_or(ChartError::MissingRequiredInput {
field: "birth_time",
})?,
gender: self
.gender
.ok_or(ChartError::MissingRequiredInput { field: "gender" })?,
fix_leap: self.fix_leap.unwrap_or(true),
year_boundary: self.year_boundary.unwrap_or_default(),
method_profile: self
.method_profile
.ok_or(ChartError::MissingRequiredInput {
field: "method_profile",
})?,
chart_plane: self.chart_plane.unwrap_or_default(),
})
}
}
pub fn by_solar(request: SolarChartRequest) -> Result<Chart, ChartError> {
let conversion = solar_to_lunar_with_year_boundary(
request.solar_year(),
request.solar_month(),
request.solar_day(),
request.birth_time_variant().iztro_time_index(),
request.year_boundary(),
)?;
by_solar_with_conversion(request, conversion)
}
pub(crate) fn by_solar_with_conversion(
request: SolarChartRequest,
conversion: LunarConversion,
) -> Result<Chart, ChartError> {
let lunar_request = LunarChartRequest::builder()
.lunar_year(conversion.lunar_year())
.lunar_month(conversion.lunar_month())
.lunar_day(conversion.lunar_day())
.birth_time_variant(request.birth_time_variant())
.gender(request.gender())
.birth_year_stem(conversion.birth_year_stem())
.birth_year_branch(conversion.birth_year_branch())
.is_leap_month(conversion.is_leap_month())
.fix_leap(request.fix_leap())
.method_profile(request.method_profile().clone())
.chart_plane(request.chart_plane())
.build()?;
let chart = by_lunar(lunar_request)?;
let natal_dates = NatalDateFacts::new(
HoroscopeSolarDate::new(
request.solar_year(),
request.solar_month().value(),
request.solar_day().value(),
),
HoroscopeLunarDate::new(
conversion.lunar_year(),
conversion.lunar_month().value(),
conversion.lunar_day().value(),
conversion.is_leap_month(),
),
);
Ok(Chart::try_new_with_four_pillars_and_profile(
chart.birth_context().clone(),
chart.birth_year(),
Some(conversion.four_pillars()),
chart.chart_profile().clone(),
chart.palaces().to_vec(),
chart.body_palace_branch(),
chart.five_element_bureau(),
)?
.with_natal_date_facts(natal_dates))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::model::profile::ChartAlgorithmKind;
fn base_builder(profile: MethodProfile) -> SolarChartRequestBuilder {
SolarChartRequest::builder()
.solar_year(1990)
.solar_month(SolarMonth::new(6).expect("valid solar month"))
.solar_day(SolarDay::new(15).expect("valid solar day"))
.birth_time(EarthlyBranch::Chen)
.gender(Gender::Female)
.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",
)
}
#[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 chart_plane_propagates_into_lunar_request_path() {
let earth = by_solar(
base_builder(zhongzhou_profile())
.chart_plane(ChartPlane::Earth)
.build()
.expect("earth request should build"),
)
.expect("zhongzhou earth solar request should build");
let heaven = by_solar(
base_builder(zhongzhou_profile())
.chart_plane(ChartPlane::Heaven)
.build()
.expect("heaven request should build"),
)
.expect("zhongzhou heaven solar request should build");
assert_ne!(earth, heaven);
}
#[test]
fn zhongzhou_earth_solar_request_anchors_to_heaven_body_palace() {
use crate::core::model::chart::PalaceName;
let solar_chart = |plane: ChartPlane| {
by_solar(
base_builder(zhongzhou_profile())
.chart_plane(plane)
.build()
.expect("request should build"),
)
.expect("zhongzhou solar chart should build")
};
let heaven = solar_chart(ChartPlane::Heaven);
let earth = solar_chart(ChartPlane::Earth);
let human = solar_chart(ChartPlane::Human);
let life_branch = |chart: &Chart| {
chart
.life_palace()
.expect("chart should have a Life Palace")
.branch()
};
let fortune_branch = |chart: &Chart| {
chart
.palace_by_name(PalaceName::Spirit)
.expect("chart should have a Fortune Palace")
.branch()
};
assert_eq!(
life_branch(&earth),
heaven
.body_palace_branch()
.expect("heaven chart should have a Body Palace branch"),
);
assert_eq!(life_branch(&human), fortune_branch(&heaven));
}
#[test]
fn solar_request_chart_carries_requested_plane() {
let solar_chart = |plane: ChartPlane| {
by_solar(
base_builder(zhongzhou_profile())
.chart_plane(plane)
.build()
.expect("request should build"),
)
.expect("zhongzhou solar chart should build")
};
for plane in [ChartPlane::Heaven, ChartPlane::Earth, ChartPlane::Human] {
let chart = solar_chart(plane);
assert_eq!(chart.chart_plane(), plane);
assert_eq!(
chart.method_profile().algorithm_kind(),
ChartAlgorithmKind::Zhongzhou,
);
}
}
#[test]
fn quanshu_earth_solar_request_is_unsupported() {
let request = base_builder(quanshu_profile())
.chart_plane(ChartPlane::Earth)
.build()
.expect("request should build");
assert_eq!(
by_solar(request),
Err(ChartError::UnsupportedChartPlane {
algorithm: ChartAlgorithmKind::QuanShu,
plane: ChartPlane::Earth,
}),
);
}
}