use lunar_rust::lunar::LunarRefHelper;
use lunar_rust::{lunar, solar};
use crate::astro::builder::{branch_of, parse_solar_date, stem_of, validate_time_index};
use crate::astro::palace::get_palace_names;
use crate::data::constants::TIGER_RULE;
use crate::data::stars::StarKey;
use crate::data::types::*;
use crate::error::IztroError;
use crate::i18n::{translate_horoscope_name, translate_star};
use crate::models::astrolabe::{Astrolabe, PalaceRef};
use crate::models::horoscope::{
AgeItem, HoroscopeData, HoroscopeItem, HoroscopeRef, YearlyDecStar, YearlyItem,
};
use crate::models::star::Star;
use crate::models::surpalaces::SurroundedPalaces;
use crate::star::decorative::get_yearly12;
use crate::star::location::{
get_chang_qu_index_by_stem, get_kui_yue_index, get_lu_yang_tuo_ma_index, get_luan_xi_index,
get_nianjie_index,
};
use crate::utils::{earthly_branch_to_palace_index, fix_index};
const CHILDHOOD_PALACES: [Palace; 6] = [
Palace::Soul,
Palace::Wealth,
Palace::Health,
Palace::Spouse,
Palace::Spirit,
Palace::Career,
];
pub fn get_horoscope_stars(
stem: HeavenlyStem,
branch: EarthlyBranch,
scope: Scope,
lang: Language,
) -> [Vec<Star>; 12] {
let kui_yue = get_kui_yue_index(stem);
let chang_qu = get_chang_qu_index_by_stem(stem);
let lu_yang_tuo_ma = get_lu_yang_tuo_ma_index(stem, branch);
let luan_xi = get_luan_xi_index(branch);
let star_keys: (
StarKey,
StarKey,
StarKey,
StarKey,
StarKey,
StarKey,
StarKey,
StarKey,
StarKey,
StarKey,
) = match scope {
Scope::Decadal => (
StarKey::Yunkui,
StarKey::Yunyue,
StarKey::Yunchang,
StarKey::Yunqu,
StarKey::Yunlu,
StarKey::Yunyang,
StarKey::Yuntuo,
StarKey::Yunma,
StarKey::Yunluan,
StarKey::Yunxi,
),
Scope::Yearly => (
StarKey::Liukui,
StarKey::Liuyue,
StarKey::Liuchang,
StarKey::Liuqu,
StarKey::Liulu,
StarKey::Liuyang,
StarKey::Liutuo,
StarKey::Liuma,
StarKey::Liuluan,
StarKey::Liuxi,
),
Scope::Monthly => (
StarKey::Yuekui,
StarKey::Yueyue,
StarKey::Yuechang,
StarKey::Yuequ,
StarKey::Yuelu,
StarKey::Yueyang,
StarKey::Yuetuo,
StarKey::Yuema,
StarKey::Yueluan,
StarKey::Yuexi,
),
Scope::Daily => (
StarKey::Rikui,
StarKey::Riyue,
StarKey::Richang,
StarKey::Riqu,
StarKey::Rilu,
StarKey::Riyang,
StarKey::Rituo,
StarKey::Rima,
StarKey::Riluan,
StarKey::Rixi,
),
Scope::Hourly => (
StarKey::Shikui,
StarKey::Shiyue,
StarKey::Shichang,
StarKey::Shiqu,
StarKey::Shilu,
StarKey::Shiyang,
StarKey::Shituo,
StarKey::Shima,
StarKey::Shiluan,
StarKey::Shixi,
),
Scope::Origin => (
StarKey::TiankuiMin,
StarKey::TianyueMin,
StarKey::WenchangMin,
StarKey::WenquMin,
StarKey::LucunMin,
StarKey::QingyangMin,
StarKey::TuoluoMin,
StarKey::TianmaMin,
StarKey::Hongluan,
StarKey::Tianxi,
),
};
let mut stars: [Vec<Star>; 12] = std::array::from_fn(|_| Vec::new());
if scope == Scope::Yearly {
let nianjie_idx = get_nianjie_index(branch);
stars[nianjie_idx].push(Star {
key: StarKey::Nianjie,
name: translate_star(StarKey::Nianjie, lang).to_string(),
star_type: StarType::Helper,
scope: Scope::Yearly,
brightness: None,
mutagen: None,
});
}
let placements = [
(kui_yue.kui, star_keys.0, StarType::Soft),
(kui_yue.yue, star_keys.1, StarType::Soft),
(chang_qu.chang, star_keys.2, StarType::Soft),
(chang_qu.qu, star_keys.3, StarType::Soft),
(lu_yang_tuo_ma.lu, star_keys.4, StarType::Lucun),
(lu_yang_tuo_ma.yang, star_keys.5, StarType::Tough),
(lu_yang_tuo_ma.tuo, star_keys.6, StarType::Tough),
(lu_yang_tuo_ma.ma, star_keys.7, StarType::Tianma),
(luan_xi.hongluan, star_keys.8, StarType::Flower),
(luan_xi.tianxi, star_keys.9, StarType::Flower),
];
for (idx, key, star_type) in placements {
stars[idx].push(Star {
key,
name: translate_star(key, lang).to_string(),
star_type,
scope,
brightness: None,
mutagen: None,
});
}
stars
}
fn time_index_to_hour(time_index: u8) -> i64 {
match time_index {
0 => 0,
12 => 23,
i => (i as i64) * 2 - 1,
}
}
pub fn get_horoscope(
astrolabe: &Astrolabe,
solar_date: &str,
time_index: u8,
language: Language,
) -> Result<HoroscopeData, IztroError> {
validate_time_index(time_index)?;
let (birth_year, birth_month, birth_day) = parse_solar_date(&astrolabe.solar_date)?;
let birth_hour = time_index_to_hour(astrolabe.time_index);
let birth_solar = solar::from_ymdhms(birth_year, birth_month, birth_day, birth_hour, 0, 0);
let birth_lunar = lunar::from_solar(&birth_solar);
let birthday_lunar_year = birth_lunar.get_year();
let birthday_lunar_month = birth_lunar.get_month().unsigned_abs() as i32;
let birthday_lunar_day = birth_lunar.get_day() as u32;
let birthday_is_leap = birth_lunar.get_month() < 0;
let (target_year, target_month, target_day) = parse_solar_date(solar_date)?;
let target_hour = time_index_to_hour(time_index);
let target_solar = solar::from_ymdhms(target_year, target_month, target_day, target_hour, 0, 0);
let target_lunar = lunar::from_solar(&target_solar);
let target_lunar_year = target_lunar.get_year();
let target_lunar_month_raw = target_lunar.get_month();
let target_is_leap = target_lunar_month_raw < 0;
let target_lunar_month = target_lunar_month_raw.unsigned_abs() as i32;
let target_lunar_day = target_lunar.get_day() as u32;
let mut nominal_age = (target_lunar_year - birthday_lunar_year) as i64;
match astrolabe.config.age_divide {
AgeDivide::Normal => nominal_age += 1,
AgeDivide::Birthday => {
let passed_birthday = (target_lunar_year == birthday_lunar_year
&& target_lunar_month == birthday_lunar_month
&& target_lunar_day > birthday_lunar_day)
|| target_lunar_month > birthday_lunar_month;
if passed_birthday {
nominal_age += 1;
}
}
}
let nominal_age = nominal_age.max(1) as u32;
let (target_year_gan_str, target_year_zhi_str) = match astrolabe.config.horoscope_divide {
HoroscopeDivide::Normal => (target_lunar.get_year_gan(), target_lunar.get_year_zhi()),
HoroscopeDivide::Exact => (
target_lunar.get_year_gan_by_li_chun(),
target_lunar.get_year_zhi_by_li_chun(),
),
};
let target_day_gan_str = target_lunar.get_day_gan_exact();
let target_day_zhi_str = target_lunar.get_day_zhi_exact();
let target_time_gan_str = target_lunar.get_time_gan();
let target_time_zhi_str = target_lunar.get_time_zhi();
let target_year_stem = stem_of(&target_year_gan_str, "target year")?;
let target_year_branch = branch_of(&target_year_zhi_str, "target year")?;
let (target_month_stem, target_month_branch) = match astrolabe.config.horoscope_divide {
HoroscopeDivide::Normal => {
let target_month_fix: i32 = if target_is_leap && target_lunar_day > 15 {
1
} else {
0
};
(
HeavenlyStem::from_index(fix_index(
TIGER_RULE[target_year_stem.index()].index() as i32 + target_lunar_month - 1
+ target_month_fix,
10,
)),
EarthlyBranch::from_index(fix_index(
2 + target_lunar_month - 1 + target_month_fix,
12,
)),
)
}
HoroscopeDivide::Exact => {
let gan = target_lunar.get_month_gan_exact();
let zhi = target_lunar.get_month_zhi_exact();
(
stem_of(&gan, "target month")?,
branch_of(&zhi, "target month")?,
)
}
};
let target_day_stem = stem_of(&target_day_gan_str, "target day")?;
let target_day_branch = branch_of(&target_day_zhi_str, "target day")?;
let target_time_stem = stem_of(&target_time_gan_str, "target time")?;
let target_time_branch = branch_of(&target_time_zhi_str, "target time")?;
let (decadal_palace_idx, is_childhood) = find_decadal_palace(astrolabe, nominal_age);
let (decadal_stem, decadal_branch) = if is_childhood {
let p = &astrolabe.palaces[decadal_palace_idx];
(p.heavenly_stem, p.earthly_branch)
} else {
let d = &astrolabe.palaces[decadal_palace_idx].decadal;
(d.heavenly_stem, d.earthly_branch)
};
let decadal_mutagen = astrolabe.config.mutagens_of(decadal_stem).to_vec();
let decadal_palace_names = get_palace_names(decadal_palace_idx).to_vec();
let decadal_stars = get_horoscope_stars(decadal_stem, decadal_branch, Scope::Decadal, language);
let decadal = HoroscopeItem {
index: decadal_palace_idx,
name: translate_horoscope_name(
if is_childhood {
HoroscopeName::Childhood
} else {
HoroscopeName::Decadal
},
language,
)
.to_string(),
heavenly_stem: decadal_stem,
earthly_branch: decadal_branch,
palace_names: decadal_palace_names,
mutagen: decadal_mutagen,
stars: Some(decadal_stars.to_vec()),
};
let age_palace_idx = find_age_palace(astrolabe, nominal_age);
let age_stem = astrolabe.palaces[age_palace_idx].heavenly_stem;
let age_branch = astrolabe.palaces[age_palace_idx].earthly_branch;
let age_mutagen = astrolabe.config.mutagens_of(age_stem).to_vec();
let age_palace_names = get_palace_names(age_palace_idx).to_vec();
let age = AgeItem {
base: HoroscopeItem {
index: age_palace_idx,
name: translate_horoscope_name(HoroscopeName::Age, language).to_string(),
heavenly_stem: age_stem,
earthly_branch: age_branch,
palace_names: age_palace_names,
mutagen: age_mutagen,
stars: None,
},
nominal_age,
};
let yearly_index = earthly_branch_to_palace_index(target_year_branch);
let yearly_mutagen = astrolabe.config.mutagens_of(target_year_stem).to_vec();
let yearly_palace_names = get_palace_names(yearly_index).to_vec();
let yearly_stars = get_horoscope_stars(
target_year_stem,
target_year_branch,
Scope::Yearly,
language,
);
let (yearly_suiqian12, yearly_jiangqian12) =
get_yearly12(target_year_branch, astrolabe.config.algorithm);
let yearly = YearlyItem {
base: HoroscopeItem {
index: yearly_index,
name: translate_horoscope_name(HoroscopeName::Yearly, language).to_string(),
heavenly_stem: target_year_stem,
earthly_branch: target_year_branch,
palace_names: yearly_palace_names,
mutagen: yearly_mutagen,
stars: Some(yearly_stars.to_vec()),
},
yearly_dec_star: YearlyDecStar {
jiangqian12: yearly_jiangqian12.to_vec(),
suiqian12: yearly_suiqian12.to_vec(),
},
};
let leap_addition = if birthday_is_leap && birthday_lunar_day > 15 {
1
} else {
0
};
let date_leap_addition = if target_is_leap && target_lunar_day > 15 {
1
} else {
0
};
let birth_time_branch_raw_index = fix_index(astrolabe.time_index as i32, 12) as i32;
let monthly_index = fix_index(
yearly_index as i32 - (birthday_lunar_month + leap_addition)
+ birth_time_branch_raw_index
+ (target_lunar_month + date_leap_addition),
12,
);
let monthly_mutagen = astrolabe.config.mutagens_of(target_month_stem).to_vec();
let monthly_palace_names = get_palace_names(monthly_index).to_vec();
let monthly_stars = get_horoscope_stars(
target_month_stem,
target_month_branch,
Scope::Monthly,
language,
);
let monthly = HoroscopeItem {
index: monthly_index,
name: translate_horoscope_name(HoroscopeName::Monthly, language).to_string(),
heavenly_stem: target_month_stem,
earthly_branch: target_month_branch,
palace_names: monthly_palace_names,
mutagen: monthly_mutagen,
stars: Some(monthly_stars.to_vec()),
};
let daily_index = fix_index(monthly_index as i32 + target_lunar_day as i32 - 1, 12);
let daily_mutagen = astrolabe.config.mutagens_of(target_day_stem).to_vec();
let daily_palace_names = get_palace_names(daily_index).to_vec();
let daily_stars =
get_horoscope_stars(target_day_stem, target_day_branch, Scope::Daily, language);
let daily = HoroscopeItem {
index: daily_index,
name: translate_horoscope_name(HoroscopeName::Daily, language).to_string(),
heavenly_stem: target_day_stem,
earthly_branch: target_day_branch,
palace_names: daily_palace_names,
mutagen: daily_mutagen,
stars: Some(daily_stars.to_vec()),
};
let target_hour_branch_index = fix_index(time_index as i32, 12) as i32;
let hourly_index = fix_index(daily_index as i32 + target_hour_branch_index, 12);
let hourly_mutagen = astrolabe.config.mutagens_of(target_time_stem).to_vec();
let hourly_palace_names = get_palace_names(hourly_index).to_vec();
let hourly_stars = get_horoscope_stars(
target_time_stem,
target_time_branch,
Scope::Hourly,
language,
);
let hourly = HoroscopeItem {
index: hourly_index,
name: translate_horoscope_name(HoroscopeName::Hourly, language).to_string(),
heavenly_stem: target_time_stem,
earthly_branch: target_time_branch,
palace_names: hourly_palace_names,
mutagen: hourly_mutagen,
stars: Some(hourly_stars.to_vec()),
};
let lunar_date_str = format!(
"{}年{}月{}",
target_lunar.get_year_in_chinese(),
target_lunar.get_month_in_chinese(),
target_lunar.get_day_in_chinese(),
);
Ok(HoroscopeData {
solar_date: solar_date.to_string(),
lunar_date: lunar_date_str,
decadal,
age,
yearly,
monthly,
daily,
hourly,
})
}
fn find_decadal_palace(astrolabe: &Astrolabe, nominal_age: u32) -> (usize, bool) {
for p in &astrolabe.palaces {
let (start, end) = p.decadal.range;
if nominal_age >= start && nominal_age <= end {
return (p.index, false);
}
}
let childhood_idx = ((nominal_age as usize).saturating_sub(1)) % CHILDHOOD_PALACES.len();
let target_palace = CHILDHOOD_PALACES[childhood_idx];
let idx = astrolabe
.palaces
.iter()
.find(|p| p.name == target_palace)
.map(|p| p.index)
.unwrap_or(0);
(idx, true)
}
fn find_age_palace(astrolabe: &Astrolabe, nominal_age: u32) -> usize {
for p in &astrolabe.palaces {
if p.ages.contains(&nominal_age) {
return p.index;
}
}
0
}
impl HoroscopeItem {
fn palace_index_by_name(&self, name: Palace) -> Option<usize> {
self.palace_names.iter().position(|p| *p == name)
}
}
impl HoroscopeData {
pub fn age_palace<'a>(&self, astrolabe: &'a Astrolabe) -> PalaceRef<'a> {
astrolabe.palace_at(self.age.index)
}
pub fn palace<'a>(
&self,
name: Palace,
scope: Scope,
astrolabe: &'a Astrolabe,
) -> Option<PalaceRef<'a>> {
if scope == Scope::Origin {
astrolabe.palace(name)
} else {
let scope_item = self.scope_item(scope)?;
let idx = scope_item.palace_index_by_name(name)?;
Some(astrolabe.palace_at(idx))
}
}
pub fn surround_palaces<'a>(
&self,
name: Palace,
scope: Scope,
astrolabe: &'a Astrolabe,
) -> Option<SurroundedPalaces<'a>> {
let palace = self.palace(name, scope, astrolabe)?;
astrolabe.surrounded_palaces(palace.index)
}
pub fn has_horoscope_mutagen(
&self,
name: Palace,
scope: Scope,
mutagen: Mutagen,
astrolabe: &Astrolabe,
) -> bool {
if scope == Scope::Origin {
return false;
}
let scope_item = match self.scope_item(scope) {
Some(i) => i,
None => return false,
};
let palace = match self.palace(name, scope, astrolabe) {
Some(p) => p,
None => return false,
};
let mutagen_index = match mutagen {
Mutagen::Lu => 0,
Mutagen::Quan => 1,
Mutagen::Ke => 2,
Mutagen::Ji => 3,
};
let star_key = scope_item.mutagen[mutagen_index];
palace
.major_stars
.iter()
.chain(palace.minor_stars.iter())
.any(|s| s.key == star_key)
}
pub fn has_horoscope_stars(
&self,
name: Palace,
scope: Scope,
stars: &[StarKey],
astrolabe: &Astrolabe,
) -> bool {
let palace_idx = match self.palace(name, scope, astrolabe) {
Some(p) => p.index,
None => return false,
};
let all_keys = self.collect_horoscope_star_keys(palace_idx);
stars.iter().all(|s| all_keys.contains(s))
}
pub fn has_one_of_horoscope_stars(
&self,
name: Palace,
scope: Scope,
stars: &[StarKey],
astrolabe: &Astrolabe,
) -> bool {
let palace_idx = match self.palace(name, scope, astrolabe) {
Some(p) => p.index,
None => return false,
};
let all_keys = self.collect_horoscope_star_keys(palace_idx);
stars.iter().any(|s| all_keys.contains(s))
}
pub fn not_have_horoscope_stars(
&self,
name: Palace,
scope: Scope,
stars: &[StarKey],
astrolabe: &Astrolabe,
) -> bool {
let palace_idx = match self.palace(name, scope, astrolabe) {
Some(p) => p.index,
None => return false,
};
let all_keys = self.collect_horoscope_star_keys(palace_idx);
stars.iter().all(|s| !all_keys.contains(s))
}
fn collect_horoscope_star_keys(&self, palace_idx: usize) -> Vec<StarKey> {
let mut keys = Vec::new();
if let Some(stars) = self.decadal.stars.as_ref().and_then(|s| s.get(palace_idx)) {
keys.extend(stars.iter().map(|s| s.key));
}
if let Some(stars) = self
.yearly
.base
.stars
.as_ref()
.and_then(|s| s.get(palace_idx))
{
keys.extend(stars.iter().map(|s| s.key));
}
keys
}
fn scope_item(&self, scope: Scope) -> Option<&HoroscopeItem> {
match scope {
Scope::Decadal => Some(&self.decadal),
Scope::Yearly => Some(&self.yearly.base),
Scope::Monthly => Some(&self.monthly),
Scope::Daily => Some(&self.daily),
Scope::Hourly => Some(&self.hourly),
_ => None,
}
}
}
impl Astrolabe {
pub fn horoscope(
&self,
target_date: &str,
target_time_index: u8,
) -> Result<HoroscopeRef<'_>, IztroError> {
Ok(HoroscopeRef {
data: get_horoscope(self, target_date, target_time_index, self.language)?,
astrolabe: self,
})
}
}
impl Astrolabe {
pub fn horoscope_now(&self) -> Result<HoroscopeRef<'_>, IztroError> {
let now = chrono::Local::now();
let date = now.format("%Y-%-m-%-d").to_string();
let time_index =
crate::utils::time_to_index(now.format("%H").to_string().parse().unwrap_or(0));
self.horoscope(&date, time_index)
}
}
impl<'a> HoroscopeRef<'a> {
pub fn age_palace(&self) -> PalaceRef<'a> {
self.data.age_palace(self.astrolabe)
}
pub fn palace(&self, name: Palace, scope: Scope) -> Option<PalaceRef<'a>> {
self.data.palace(name, scope, self.astrolabe)
}
pub fn surround_palaces(&self, name: Palace, scope: Scope) -> Option<SurroundedPalaces<'a>> {
self.data.surround_palaces(name, scope, self.astrolabe)
}
pub fn has_horoscope_mutagen(&self, name: Palace, scope: Scope, mutagen: Mutagen) -> bool {
self.data
.has_horoscope_mutagen(name, scope, mutagen, self.astrolabe)
}
pub fn has_horoscope_stars(&self, name: Palace, scope: Scope, stars: &[StarKey]) -> bool {
self.data
.has_horoscope_stars(name, scope, stars, self.astrolabe)
}
pub fn has_one_of_horoscope_stars(
&self,
name: Palace,
scope: Scope,
stars: &[StarKey],
) -> bool {
self.data
.has_one_of_horoscope_stars(name, scope, stars, self.astrolabe)
}
pub fn not_have_horoscope_stars(&self, name: Palace, scope: Scope, stars: &[StarKey]) -> bool {
self.data
.not_have_horoscope_stars(name, scope, stars, self.astrolabe)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::builder::by_solar;
fn make_astrolabe() -> Astrolabe {
by_solar(
"2000-8-16",
2,
Gender::Female,
true,
Language::ZhCN,
Config::default(),
)
.unwrap()
}
#[test]
fn test_horoscope_holds_astrolabe() {
let astrolabe = make_astrolabe();
let h = astrolabe.horoscope("2023-10-15", 4).unwrap();
assert_eq!(h.astrolabe().solar_date, astrolabe.solar_date);
assert_eq!(h.solar_date, "2023-10-15");
let by_data = h
.data()
.palace(Palace::Soul, Scope::Decadal, &astrolabe)
.unwrap();
assert_eq!(
h.palace(Palace::Soul, Scope::Decadal).unwrap().index,
by_data.index
);
assert_eq!(h.age_palace().index, h.data().age_palace(&astrolabe).index);
assert_eq!(
h.surround_palaces(Palace::Soul, Scope::Yearly)
.unwrap()
.target
.index,
by_data.astrolabe().palace_at(h.yearly.index).index
);
for m in [Mutagen::Lu, Mutagen::Quan, Mutagen::Ke, Mutagen::Ji] {
assert_eq!(
h.has_horoscope_mutagen(Palace::Soul, Scope::Decadal, m),
h.data()
.has_horoscope_mutagen(Palace::Soul, Scope::Decadal, m, &astrolabe)
);
}
let stars = [StarKey::Yunkui, StarKey::Yunyue];
assert_eq!(
h.has_horoscope_stars(Palace::Soul, Scope::Decadal, &stars),
h.data()
.has_horoscope_stars(Palace::Soul, Scope::Decadal, &stars, &astrolabe)
);
assert_eq!(
h.has_one_of_horoscope_stars(Palace::Soul, Scope::Decadal, &stars),
!h.not_have_horoscope_stars(Palace::Soul, Scope::Decadal, &stars)
|| h.palace(Palace::Soul, Scope::Decadal).is_none()
);
}
#[test]
fn test_get_horoscope_basic() {
let astrolabe = make_astrolabe();
let horoscope = get_horoscope(&astrolabe, "2023-10-15", 4, Language::ZhCN).unwrap();
assert_eq!(horoscope.solar_date, "2023-10-15");
assert!(!horoscope.lunar_date.is_empty());
assert_eq!(horoscope.decadal.palace_names.len(), 12);
assert!(horoscope.decadal.stars.is_some());
let dec_stars = horoscope.decadal.stars.as_ref().unwrap();
assert_eq!(dec_stars.len(), 12);
assert!(horoscope.yearly.stars.is_some());
let yr_stars = horoscope.yearly.stars.as_ref().unwrap();
assert_eq!(yr_stars.len(), 12);
assert!(horoscope.monthly.stars.is_some());
assert!(horoscope.daily.stars.is_some());
assert!(horoscope.hourly.stars.is_some());
assert!(horoscope.age.nominal_age > 0);
assert_eq!(horoscope.decadal.mutagen.len(), 4);
assert_eq!(horoscope.yearly.mutagen.len(), 4);
assert_eq!(horoscope.monthly.mutagen.len(), 4);
assert_eq!(horoscope.daily.mutagen.len(), 4);
assert_eq!(horoscope.hourly.mutagen.len(), 4);
}
#[test]
fn test_get_horoscope_nominal_age() {
let astrolabe = make_astrolabe();
let horoscope = get_horoscope(&astrolabe, "2023-10-15", 4, Language::ZhCN).unwrap();
assert!(horoscope.age.nominal_age >= 23 && horoscope.age.nominal_age <= 25);
}
#[test]
fn test_get_horoscope_stars_decadal() {
let stars = get_horoscope_stars(
HeavenlyStem::Jia,
EarthlyBranch::Yin,
Scope::Decadal,
Language::ZhCN,
);
let total: usize = stars.iter().map(|v| v.len()).sum();
assert_eq!(total, 10);
for palace_stars in &stars {
for s in palace_stars {
assert_eq!(s.scope, Scope::Decadal);
}
}
}
#[test]
fn test_get_horoscope_stars_yearly_has_nianjie() {
let stars = get_horoscope_stars(
HeavenlyStem::Jia,
EarthlyBranch::Yin,
Scope::Yearly,
Language::ZhCN,
);
let total: usize = stars.iter().map(|v| v.len()).sum();
assert_eq!(total, 11);
let has_nianjie = stars
.iter()
.flat_map(|v| v.iter())
.any(|s| s.key == StarKey::Nianjie);
assert!(has_nianjie);
}
#[test]
fn test_horoscope_palace_query() {
let astrolabe = make_astrolabe();
let horoscope = get_horoscope(&astrolabe, "2023-10-15", 4, Language::ZhCN).unwrap();
let soul = horoscope.palace(Palace::Soul, Scope::Origin, &astrolabe);
assert!(soul.is_some());
assert_eq!(soul.unwrap().name, Palace::Soul);
let dec_soul = horoscope.palace(Palace::Soul, Scope::Decadal, &astrolabe);
assert!(dec_soul.is_some());
}
#[test]
fn test_horoscope_age_palace() {
let astrolabe = make_astrolabe();
let horoscope = get_horoscope(&astrolabe, "2023-10-15", 4, Language::ZhCN).unwrap();
let age_palace = horoscope.age_palace(&astrolabe);
assert!(age_palace.index < 12);
}
#[test]
fn test_horoscope_surround_palaces() {
let astrolabe = make_astrolabe();
let horoscope = get_horoscope(&astrolabe, "2023-10-15", 4, Language::ZhCN).unwrap();
let sp = horoscope.surround_palaces(Palace::Soul, Scope::Origin, &astrolabe);
assert!(sp.is_some());
let sp = sp.unwrap();
assert_eq!(sp.target.name, Palace::Soul);
}
#[test]
fn test_childhood_decadal() {
let astrolabe = make_astrolabe();
let horoscope = get_horoscope(&astrolabe, "2000-12-1", 4, Language::ZhCN).unwrap();
assert!(horoscope.age.nominal_age <= 2);
assert!(horoscope.decadal.index < 12);
}
#[test]
fn test_has_horoscope_mutagen() {
let astrolabe = make_astrolabe();
let horoscope = get_horoscope(&astrolabe, "2023-10-15", 4, Language::ZhCN).unwrap();
let mutagen_star = horoscope.decadal.mutagen[0]; let mut found = false;
for palace_name in &[
Palace::Soul,
Palace::Parents,
Palace::Spirit,
Palace::Property,
Palace::Career,
Palace::Friends,
Palace::Surface,
Palace::Health,
Palace::Wealth,
Palace::Children,
Palace::Spouse,
Palace::Siblings,
] {
if horoscope.has_horoscope_mutagen(
*palace_name,
Scope::Decadal,
Mutagen::Lu,
&astrolabe,
) {
found = true;
break;
}
}
assert!(
found,
"Lu mutagen star {:?} should be in some palace",
mutagen_star
);
}
#[test]
fn test_has_horoscope_stars_and_not() {
let astrolabe = make_astrolabe();
let horoscope = get_horoscope(&astrolabe, "2023-10-15", 4, Language::ZhCN).unwrap();
let dec_stars = horoscope.decadal.stars.as_ref().unwrap();
let mut test_palace_idx = None;
let mut test_star_key = None;
for (i, stars) in dec_stars.iter().enumerate() {
if !stars.is_empty() {
test_palace_idx = Some(i);
test_star_key = Some(stars[0].key);
break;
}
}
if let (Some(idx), Some(key)) = (test_palace_idx, test_star_key) {
let palace_name = astrolabe.palaces[idx].name;
assert!(horoscope.has_horoscope_stars(palace_name, Scope::Origin, &[key], &astrolabe));
assert!(!horoscope.not_have_horoscope_stars(
palace_name,
Scope::Origin,
&[key],
&astrolabe
));
}
}
}