use crate::data::stars::StarKey;
use crate::data::types::{Mutagen, Palace, Scope};
use crate::models::star::Star;
use crate::pattern::view::{KONG_WANG, KONG_YAO};
use crate::pattern::{ChartView, PatternHit, PatternKey, Rule, StarAt};
pub static RULES: &[Rule] = &[
Rule {
key: PatternKey::LuShuaiMaKun,
detect: lu_shuai_ma_kun,
},
Rule {
key: PatternKey::SanQiJiaHui,
detect: san_qi_jia_hui,
},
Rule {
key: PatternKey::LuMaJiaoChi,
detect: lu_ma_jiao_chi,
},
Rule {
key: PatternKey::LuHeYuanYang,
detect: lu_he_yuan_yang,
},
Rule {
key: PatternKey::MingLuAnLu,
detect: ming_lu_an_lu,
},
Rule {
key: PatternKey::LuMaPeiYin,
detect: lu_ma_pei_yin,
},
Rule {
key: PatternKey::LiangChongHuaGai,
detect: liang_chong_hua_gai,
},
Rule {
key: PatternKey::FengYunJiHui,
detect: feng_yun_ji_hui,
},
];
const SHA_JI_MA: [StarKey; 4] = [
StarKey::QingyangMin,
StarKey::TuoluoMin,
StarKey::Tianxing,
StarKey::Yinsha,
];
const HAO_YAO: [StarKey; 3] = [StarKey::Dahao, StarKey::Xiaohao, StarKey::DijieMin];
pub fn lu_shuai_ma_kun(v: &ChartView) -> Vec<PatternHit> {
if v.scope() == Scope::Origin {
return vec![];
}
let l = v.soul();
let Some((p, lucun, decay)) = v.surround(l).into_iter().find_map(|p| {
let lucun = v.find(p, StarKey::LucunMin)?;
Some((p, lucun, decayed(v, p)?))
}) else {
return vec![];
};
let Some((q, ma, trap)) = v.surround(l).into_iter().find_map(|q| {
let ma = v.find(q, StarKey::TianmaMin)?;
Some((q, ma, trapped(v, q)?))
}) else {
return vec![];
};
let mut hit = v.hit(PatternKey::LuShuaiMaKun, l, vec![(p, lucun)]);
hit.stars.push(decay);
hit.stars.push(v.star_at(q, ma));
hit.stars.push(v.star_at(q, trap));
if let Some((qp, qisha)) = v.find_in_surround(l, StarKey::QishaMaj) {
hit.variant = Some("qisha");
hit.stars.push(v.star_at(qp, qisha));
}
vec![hit]
}
fn decayed(v: &ChartView, i: usize) -> Option<StarAt> {
if let Some(s) = KONG_YAO.iter().chain(&HAO_YAO).find_map(|k| v.find(i, *k)) {
return Some(v.star_at(i, s));
}
let boshi = v.boshi12(i);
matches!(boshi, StarKey::Dahao | StarKey::Xiaohao).then(|| StarAt {
star: boshi,
palace: i,
brightness: None,
mutagen: None,
})
}
fn trapped<'a>(v: &ChartView<'a>, i: usize) -> Option<&'a Star> {
SHA_JI_MA
.iter()
.find_map(|k| v.find(i, *k))
.or_else(|| v.mutagen_star(i, Mutagen::Ji))
}
pub fn san_qi_jia_hui(v: &ChartView) -> Vec<PatternHit> {
let s = v.soul();
let (Some(lu), Some(quan), Some(ke)) = (
v.find_mutagen_in_surround(s, Mutagen::Lu),
v.find_mutagen_in_surround(s, Mutagen::Quan),
v.find_mutagen_in_surround(s, Mutagen::Ke),
) else {
return vec![];
};
let mut hit = v.hit(PatternKey::SanQiJiaHui, s, vec![lu, quan, ke]);
if ke.0 == s && lu.0 == v.index_of(Palace::Wealth) && quan.0 == v.index_of(Palace::Career) {
hit.variant = Some("ke_soul_lu_wealth_quan_career");
}
vec![hit]
}
pub fn lu_ma_jiao_chi(v: &ChartView) -> Vec<PatternHit> {
let mut hits: Vec<PatternHit> = (0..12)
.filter_map(|i| {
let lucun = v.find(i, StarKey::LucunMin)?;
let ma = v.find(i, StarKey::TianmaMin)?;
Some(v.hit(PatternKey::LuMaJiaoChi, i, vec![(i, lucun), (i, ma)]))
})
.collect();
let s = v.soul();
if let (Some(lucun), Some(ma)) = (
v.find_in_surround(s, StarKey::LucunMin),
v.find_in_surround(s, StarKey::TianmaMin),
) {
let mut hit = v.hit(PatternKey::LuMaJiaoChi, s, vec![lucun, ma]);
hit.variant = Some("surround");
hits.push(hit);
}
hits
}
pub fn lu_he_yuan_yang(v: &ChartView) -> Vec<PatternHit> {
let s = v.soul();
let o = v.opposite(s);
let (cun, hua) = (v.find(s, StarKey::LucunMin), v.mutagen_star(s, Mutagen::Lu));
if let (Some(cun), Some(hua)) = (cun, hua) {
return vec![v.hit(PatternKey::LuHeYuanYang, s, vec![(s, cun), (s, hua)])];
}
let pair = match (cun, hua) {
(Some(cun), _) => v
.mutagen_star(o, Mutagen::Lu)
.map(|hua| vec![(s, cun), (o, hua)]),
(_, Some(hua)) => v
.find(o, StarKey::LucunMin)
.map(|cun| vec![(s, hua), (o, cun)]),
_ => None,
};
pair.map(|stars| {
let mut hit = v.hit(PatternKey::LuHeYuanYang, s, stars);
hit.variant = Some("opposite");
vec![hit]
})
.unwrap_or_default()
}
pub fn ming_lu_an_lu(v: &ChartView) -> Vec<PatternHit> {
let s = v.soul();
let h = v.hidden(s);
let mut hits = vec![];
if let (Some(cun), Some(hua)) = (v.find(s, StarKey::LucunMin), v.mutagen_star(h, Mutagen::Lu)) {
hits.push(v.hit(PatternKey::MingLuAnLu, s, vec![(s, cun), (h, hua)]));
} else if let (Some(hua), Some(cun)) =
(v.mutagen_star(s, Mutagen::Lu), v.find(h, StarKey::LucunMin))
{
hits.push(v.hit(PatternKey::MingLuAnLu, s, vec![(s, hua), (h, cun)]));
}
hits
}
pub fn lu_ma_pei_yin(v: &ChartView) -> Vec<PatternHit> {
(0..12)
.filter_map(|i| {
let cun = v.find(i, StarKey::LucunMin)?;
let ma = v.find(i, StarKey::TianmaMin)?;
let xiang = v.find(i, StarKey::TianxiangMaj)?;
let mut hit = v.hit(
PatternKey::LuMaPeiYin,
i,
vec![(i, cun), (i, ma), (i, xiang)],
);
hit.broken = v.has_any(i, &KONG_WANG);
Some(hit)
})
.collect()
}
pub fn liang_chong_hua_gai(v: &ChartView) -> Vec<PatternHit> {
let s = v.soul();
let (Some(cun), Some(hua)) = (v.find(s, StarKey::LucunMin), v.mutagen_star(s, Mutagen::Lu))
else {
return vec![];
};
let mut hits = Vec::new();
if let Some(kong) = [StarKey::DikongMin, StarKey::DijieMin]
.iter()
.find_map(|k| v.find(s, *k))
{
hits.push(v.hit(
PatternKey::LiangChongHuaGai,
s,
vec![(s, cun), (s, hua), (s, kong)],
));
}
if let Some(kong) = KONG_YAO
.iter()
.filter(|k| **k != StarKey::DikongMin)
.find_map(|k| v.find(s, *k))
{
let mut hit = v.hit(
PatternKey::LiangChongHuaGai,
s,
vec![(s, cun), (s, hua), (s, kong)],
);
hit.variant = Some("kong_yao");
hits.push(hit);
}
hits
}
pub fn feng_yun_ji_hui(v: &ChartView) -> Vec<PatternHit> {
if v.scope() != Scope::Decadal {
return vec![];
}
let (Some(h), Some((dp, ds))) = (v.horoscope(), meets_lu_ma(v, v.soul())) else {
return vec![];
};
let decadal_tight = meets_in_palace(v, v.soul());
let mut hits = vec![];
if let Some((ap, age)) = meets_lu_ma(v, h.age.index) {
let mut hit = v.hit(
PatternKey::FengYunJiHui,
v.soul(),
vec![(dp, ds), (ap, age)],
);
if decadal_tight && meets_in_palace(v, h.age.index) {
hit.variant = Some("same_palace");
}
hits.push(hit);
}
let yearly = ChartView::at(v.astrolabe(), h, Scope::Yearly, v.config());
if let Some((yp, ys)) = meets_lu_ma(&yearly, yearly.soul()) {
let mut hit = v.hit(PatternKey::FengYunJiHui, v.soul(), vec![(dp, ds)]);
hit.stars.push(yearly.star_at(yp, ys));
hit.variant = Some(
if decadal_tight && meets_in_palace(&yearly, yearly.soul()) {
"yearly_same_palace"
} else {
"yearly"
},
);
hits.push(hit);
}
hits
}
fn meets_lu_ma<'a>(v: &ChartView<'a>, i: usize) -> Option<(usize, &'a Star)> {
v.find_in_surround(i, StarKey::LucunMin)
.or_else(|| v.find_in_surround(i, StarKey::TianmaMin))
.or_else(|| v.find_mutagen_in_surround(i, Mutagen::Lu))
}
fn meets_in_palace(v: &ChartView, i: usize) -> bool {
v.has_any(i, &[StarKey::LucunMin, StarKey::TianmaMin]) || v.has_mutagen(i, Mutagen::Lu)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::types::EarthlyBranch;
use crate::models::astrolabe::Astrolabe;
use crate::models::horoscope::HoroscopeRef;
use crate::pattern::PatternConfig;
use crate::pattern::testutil::find_chart;
fn natal(a: &Astrolabe) -> ChartView<'_> {
ChartView::natal(a, &PatternConfig::default())
}
fn horo(a: &Astrolabe) -> HoroscopeRef<'_> {
a.horoscope("2026-8-19", 3).unwrap()
}
fn decadal<'a>(h: &'a HoroscopeRef<'a>) -> ChartView<'a> {
ChartView::at(
h.astrolabe(),
h.data(),
Scope::Decadal,
&PatternConfig::default(),
)
}
#[test]
fn san_qi_jia_hui_needs_all_three_mutagens() {
let a = find_chart(|a| {
let v = natal(a);
[Mutagen::Lu, Mutagen::Quan, Mutagen::Ke]
.iter()
.all(|m| v.find_mutagen_in_surround(v.soul(), *m).is_some())
});
let v = natal(&a);
let hits = san_qi_jia_hui(&v);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].palace, v.soul());
assert_eq!(hits[0].stars.len(), 3);
let ms: Vec<_> = hits[0].stars.iter().map(|s| s.mutagen).collect();
assert_eq!(
ms,
vec![Some(Mutagen::Lu), Some(Mutagen::Quan), Some(Mutagen::Ke)]
);
let b = find_chart(|a| {
let v = natal(a);
v.find_mutagen_in_surround(v.soul(), Mutagen::Lu).is_some()
&& v.find_mutagen_in_surround(v.soul(), Mutagen::Quan)
.is_some()
&& v.find_mutagen_in_surround(v.soul(), Mutagen::Ke).is_none()
});
assert!(san_qi_jia_hui(&natal(&b)).is_empty());
}
#[test]
fn san_qi_jia_hui_marks_ke_soul_lu_wealth_quan_career() {
let a = find_chart(|a| {
let v = natal(a);
let s = v.soul();
v.has_mutagen(s, Mutagen::Ke)
&& v.has_mutagen(v.index_of(Palace::Wealth), Mutagen::Lu)
&& v.has_mutagen(v.index_of(Palace::Career), Mutagen::Quan)
});
let hits = san_qi_jia_hui(&natal(&a));
assert_eq!(hits[0].variant, Some("ke_soul_lu_wealth_quan_career"));
}
#[test]
fn lu_ma_jiao_chi_same_palace_is_main_form() {
let a = find_chart(|a| {
let v = natal(a);
(0..12).any(|i| v.has(i, StarKey::LucunMin) && v.has(i, StarKey::TianmaMin))
});
let v = natal(&a);
let hits: Vec<_> = lu_ma_jiao_chi(&v)
.into_iter()
.filter(|h| h.variant.is_none())
.collect();
assert_eq!(hits.len(), 1);
let hit = &hits[0];
assert!(matches!(
v.branch(hit.palace),
EarthlyBranch::Yin | EarthlyBranch::Shen | EarthlyBranch::Si | EarthlyBranch::Hai
));
assert!(hit.stars.iter().all(|s| s.palace == hit.palace));
let keys: Vec<_> = hit.stars.iter().map(|s| s.star).collect();
assert_eq!(keys, vec![StarKey::LucunMin, StarKey::TianmaMin]);
let b = find_chart(|a| {
let v = natal(a);
let (Some((lu, _)), Some((ma, _))) =
(v.locate(StarKey::LucunMin), v.locate(StarKey::TianmaMin))
else {
return false;
};
let surround = v.surround(v.soul());
lu != ma && !(surround.contains(&lu) && surround.contains(&ma))
});
assert!(lu_ma_jiao_chi(&natal(&b)).is_empty());
}
#[test]
fn lu_ma_jiao_chi_surround_variant() {
let a = find_chart(|a| {
let v = natal(a);
let (Some((lu, _)), Some((ma, _))) =
(v.locate(StarKey::LucunMin), v.locate(StarKey::TianmaMin))
else {
return false;
};
let surround = v.surround(v.soul());
lu != ma && surround.contains(&lu) && surround.contains(&ma)
});
let v = natal(&a);
let hits = lu_ma_jiao_chi(&v);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].variant, Some("surround"));
assert_eq!(hits[0].palace, v.soul());
let surround = v.surround(v.soul());
assert!(hits[0].stars.iter().all(|s| surround.contains(&s.palace)));
}
#[test]
fn lu_he_yuan_yang_same_palace() {
let a = find_chart(|a| {
let v = natal(a);
v.has(v.soul(), StarKey::LucunMin) && v.has_mutagen(v.soul(), Mutagen::Lu)
});
let v = natal(&a);
let hits = lu_he_yuan_yang(&v);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].variant, None);
assert_eq!(hits[0].palace, v.soul());
assert!(hits[0].stars.iter().all(|s| s.palace == v.soul()));
}
#[test]
fn lu_he_yuan_yang_opposite_palace() {
let a = find_chart(|a| {
let v = natal(a);
let s = v.soul();
v.has(s, StarKey::LucunMin)
&& !v.has_mutagen(s, Mutagen::Lu)
&& v.has_mutagen(v.opposite(s), Mutagen::Lu)
});
let v = natal(&a);
let hits = lu_he_yuan_yang(&v);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].variant, Some("opposite"));
assert_eq!(hits[0].stars[1].palace, v.opposite(v.soul()));
let b = find_chart(|a| {
let v = natal(a);
let s = v.soul();
v.has(s, StarKey::LucunMin)
&& !v.has_mutagen(s, Mutagen::Lu)
&& !v.has_mutagen(v.opposite(s), Mutagen::Lu)
});
assert!(lu_he_yuan_yang(&natal(&b)).is_empty());
}
#[test]
fn ming_lu_an_lu_pairs_soul_with_hidden_palace() {
let a = find_chart(|a| {
let v = natal(a);
v.has(v.soul(), StarKey::LucunMin) && v.has_mutagen(v.hidden(v.soul()), Mutagen::Lu)
});
let v = natal(&a);
let hits = ming_lu_an_lu(&v);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].palace, v.soul());
assert_eq!(hits[0].stars[1].palace, v.hidden(v.soul()));
let b = find_chart(|a| {
let v = natal(a);
let s = v.soul();
v.has(s, StarKey::LucunMin)
&& !v.has_mutagen(v.hidden(s), Mutagen::Lu)
&& !v.has_mutagen(s, Mutagen::Lu)
});
assert!(ming_lu_an_lu(&natal(&b)).is_empty());
}
#[test]
fn lu_ma_pei_yin_hits_any_palace() {
let a = find_chart(|a| {
let v = natal(a);
let s = v.soul();
v.has(s, StarKey::LucunMin)
&& v.has(s, StarKey::TianmaMin)
&& v.has(s, StarKey::TianxiangMaj)
});
let v = natal(&a);
let hits = lu_ma_pei_yin(&v);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].palace, v.soul());
assert_eq!(hits[0].stars.len(), 3);
assert_eq!(hits[0].broken, v.has_any(v.soul(), &KONG_WANG));
let b = find_chart(|a| {
let v = natal(a);
(0..12).any(|i| {
i != v.soul()
&& v.has(i, StarKey::LucunMin)
&& v.has(i, StarKey::TianmaMin)
&& v.has(i, StarKey::TianxiangMaj)
})
});
let v = natal(&b);
let hits = lu_ma_pei_yin(&v);
assert_eq!(hits.len(), 1);
assert_ne!(hits[0].palace, v.soul());
assert!(hits[0].stars.iter().all(|s| s.palace == hits[0].palace));
assert!(matches!(
v.branch(hits[0].palace),
EarthlyBranch::Yin | EarthlyBranch::Shen | EarthlyBranch::Si | EarthlyBranch::Hai
));
let c = find_chart(|a| {
let v = natal(a);
(0..12).any(|i| {
v.has(i, StarKey::LucunMin)
&& v.has(i, StarKey::TianmaMin)
&& !v.has(i, StarKey::TianxiangMaj)
})
});
assert!(lu_ma_pei_yin(&natal(&c)).is_empty());
}
#[test]
fn liang_chong_hua_gai_requires_double_lu_and_kong_jie() {
let a = find_chart(|a| {
let v = natal(a);
let s = v.soul();
v.has(s, StarKey::LucunMin)
&& v.has_mutagen(s, Mutagen::Lu)
&& v.has_any(s, &[StarKey::DikongMin, StarKey::DijieMin])
});
let v = natal(&a);
let hit = liang_chong_hua_gai(&v)
.into_iter()
.find(|h| h.variant.is_none())
.expect("hit");
assert_eq!(hit.palace, v.soul());
assert!(matches!(
hit.stars[2].star,
StarKey::DikongMin | StarKey::DijieMin
));
let b = find_chart(|a| {
let v = natal(a);
let s = v.soul();
v.has(s, StarKey::LucunMin)
&& v.has_mutagen(s, Mutagen::Lu)
&& !v.has_any(s, &[StarKey::DikongMin, StarKey::DijieMin])
&& !v.has_any(s, &[StarKey::Tiankong, StarKey::Jiekong, StarKey::Xunkong])
});
assert!(liang_chong_hua_gai(&natal(&b)).is_empty());
}
#[test]
fn liang_chong_hua_gai_kong_yao_variant() {
let a = find_chart(|a| {
let v = natal(a);
let s = v.soul();
v.has(s, StarKey::LucunMin)
&& v.has_mutagen(s, Mutagen::Lu)
&& !v.has_any(s, &[StarKey::DikongMin, StarKey::DijieMin])
&& v.has_any(s, &[StarKey::Tiankong, StarKey::Jiekong, StarKey::Xunkong])
});
let v = natal(&a);
let hits = liang_chong_hua_gai(&v);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].variant, Some("kong_yao"));
assert!(matches!(
hits[0].stars[2].star,
StarKey::Tiankong | StarKey::Jiekong | StarKey::Xunkong
));
}
#[test]
fn lu_shuai_ma_kun_needs_both_lu_decayed_and_ma_trapped() {
let a = find_chart(|a| {
let Ok(h) = a.horoscope("2026-8-19", 3) else {
return false;
};
!lu_shuai_ma_kun(&decadal(&h)).is_empty()
});
let h = horo(&a);
let v = decadal(&h);
let hits = lu_shuai_ma_kun(&v);
assert_eq!(hits.len(), 1);
let hit = &hits[0];
assert_eq!(hit.scope, Scope::Decadal);
assert_eq!(hit.palace, v.soul());
let qisha = v.find_in_surround(v.soul(), StarKey::QishaMaj).is_some();
assert_eq!(hit.variant, qisha.then_some("qisha"));
assert_eq!(hit.stars.len(), if qisha { 5 } else { 4 });
assert!(matches!(
hit.stars[0].star,
StarKey::LucunMin | StarKey::Yunlu | StarKey::Liulu
));
assert!(matches!(
hit.stars[2].star,
StarKey::TianmaMin | StarKey::Yunma | StarKey::Liuma
));
assert_eq!(hit.stars[0].palace, hit.stars[1].palace);
assert_eq!(hit.stars[2].palace, hit.stars[3].palace);
let surround = v.surround(v.soul());
assert!(hit.stars.iter().all(|s| surround.contains(&s.palace)));
assert!(
!a.patterns()
.iter()
.any(|x| x.key == PatternKey::LuShuaiMaKun)
);
assert!(
h.patterns(Scope::Decadal)
.iter()
.any(|x| x.key == PatternKey::LuShuaiMaKun)
);
let b = find_chart(|a| {
let Ok(h) = a.horoscope("2026-8-19", 3) else {
return false;
};
let v = decadal(&h);
let l = v.soul();
v.surround(l)
.into_iter()
.any(|p| v.has(p, StarKey::LucunMin) && decayed(&v, p).is_some())
&& v.surround(l)
.into_iter()
.all(|q| !v.has(q, StarKey::TianmaMin) || trapped(&v, q).is_none())
});
let hb = horo(&b);
assert!(lu_shuai_ma_kun(&decadal(&hb)).is_empty());
}
#[test]
fn lu_shuai_ma_kun_marks_qisha_variant() {
let a = find_chart(|a| {
let Ok(h) = a.horoscope("2026-8-19", 3) else {
return false;
};
let v = decadal(&h);
v.find_in_surround(v.soul(), StarKey::QishaMaj).is_some()
&& !lu_shuai_ma_kun(&v).is_empty()
});
let h = horo(&a);
let v = decadal(&h);
let hits = lu_shuai_ma_kun(&v);
assert_eq!(hits[0].variant, Some("qisha"));
assert_eq!(hits[0].stars.len(), 5);
assert_eq!(hits[0].stars[4].star, StarKey::QishaMaj);
}
fn is_age_combo(h: &PatternHit) -> bool {
!h.variant.is_some_and(|s| s.starts_with("yearly"))
}
#[test]
fn feng_yun_ji_hui_pairs_decadal_with_age_or_yearly() {
let a = find_chart(|a| {
let Ok(h) = a.horoscope("2026-8-19", 3) else {
return false;
};
let v = decadal(&h);
meets_lu_ma(&v, v.soul()).is_some() && meets_lu_ma(&v, h.age.index).is_some()
});
let h = horo(&a);
let v = decadal(&h);
let hits = feng_yun_ji_hui(&v);
let hit = hits
.iter()
.find(|x| is_age_combo(x))
.expect("decadal + age");
assert_eq!(hit.scope, Scope::Decadal);
assert_eq!(hit.palace, v.soul());
assert_eq!(hit.stars.len(), 2);
assert!(v.surround(v.soul()).contains(&hit.stars[0].palace));
assert!(v.surround(h.age.index).contains(&hit.stars[1].palace));
assert!(
!h.patterns(Scope::Yearly)
.iter()
.any(|x| x.key == PatternKey::FengYunJiHui)
);
assert!(
!a.patterns()
.iter()
.any(|x| x.key == PatternKey::FengYunJiHui)
);
}
#[test]
fn feng_yun_ji_hui_yearly_variant() {
let a = find_chart(|a| {
let Ok(h) = a.horoscope("2026-8-19", 3) else {
return false;
};
let v = decadal(&h);
let y = ChartView::at(a, h.data(), Scope::Yearly, &PatternConfig::default());
meets_lu_ma(&v, v.soul()).is_some() && meets_lu_ma(&y, y.soul()).is_some()
});
let h = horo(&a);
let hits = feng_yun_ji_hui(&decadal(&h));
let hit = hits
.iter()
.find(|x| !is_age_combo(x))
.expect("decadal + yearly");
assert_eq!(hit.stars.len(), 2);
let y = ChartView::at(&a, h.data(), Scope::Yearly, &PatternConfig::default());
assert!(y.surround(y.soul()).contains(&hit.stars[1].palace));
}
#[test]
fn feng_yun_ji_hui_same_palace_variant() {
let a = find_chart(|a| {
let Ok(h) = a.horoscope("2026-8-19", 3) else {
return false;
};
let v = decadal(&h);
meets_in_palace(&v, v.soul()) && meets_in_palace(&v, h.age.index)
});
let h = horo(&a);
let v = decadal(&h);
let hits = feng_yun_ji_hui(&v);
let hit = hits
.iter()
.find(|x| is_age_combo(x))
.expect("decadal + age");
assert_eq!(hit.variant, Some("same_palace"));
let b = find_chart(|a| {
let Ok(h) = a.horoscope("2026-8-19", 3) else {
return false;
};
let v = decadal(&h);
meets_lu_ma(&v, v.soul()).is_some()
&& meets_lu_ma(&v, h.age.index).is_some()
&& !(meets_in_palace(&v, v.soul()) && meets_in_palace(&v, h.age.index))
});
let hb = horo(&b);
let hits = feng_yun_ji_hui(&decadal(&hb));
assert_eq!(
hits.iter().find(|x| is_age_combo(x)).expect("hit").variant,
None
);
}
#[test]
fn feng_yun_ji_hui_absent_when_decadal_misses_lu_ma() {
let a = find_chart(|a| {
let Ok(h) = a.horoscope("2026-8-19", 3) else {
return false;
};
meets_lu_ma(&decadal(&h), h.decadal.index).is_none()
});
let h = horo(&a);
assert!(feng_yun_ji_hui(&decadal(&h)).is_empty());
}
}