use crate::data::stars::StarKey;
use crate::data::types::{EarthlyBranch, Palace};
use crate::models::star::Star;
use crate::pattern::view::{KONG_WANG, SHA6, SPL};
use crate::pattern::{ChartView, PatternHit, PatternKey, Rule};
pub static RULES: &[Rule] = &[
Rule {
key: PatternKey::WuTanTongXing,
detect: wu_tan_tong_xing,
},
Rule {
key: PatternKey::LingChangTuoWu,
detect: ling_chang_tuo_wu,
},
Rule {
key: PatternKey::XingQiuJiaYin,
detect: xing_qiu_jia_yin,
},
Rule {
key: PatternKey::ShengBuFengShi,
detect: sheng_bu_feng_shi,
},
Rule {
key: PatternKey::XiongSuChaoYuan,
detect: xiong_su_chao_yuan,
},
Rule {
key: PatternKey::FuXiangChaoYuan,
detect: fu_xiang_chao_yuan,
},
Rule {
key: PatternKey::HuoTan,
detect: huo_tan,
},
Rule {
key: PatternKey::LingTan,
detect: ling_tan,
},
Rule {
key: PatternKey::ShiZhongYinYu,
detect: shi_zhong_yin_yu,
},
Rule {
key: PatternKey::LiangMaPiaoDang,
detect: liang_ma_piao_dang,
},
Rule {
key: PatternKey::YangLiangChangLu,
detect: yang_liang_chang_lu,
},
Rule {
key: PatternKey::ShaPoLang,
detect: sha_po_lang,
},
Rule {
key: PatternKey::QiShaChaoDou,
detect: qi_sha_chao_dou,
},
Rule {
key: PatternKey::YingXingRuMiao,
detect: ying_xing_ru_miao,
},
Rule {
key: PatternKey::ZhongShuiChaoDong,
detect: zhong_shui_chao_dong,
},
];
const XING: [StarKey; 2] = [StarKey::Tianxing, StarKey::QingyangMin];
fn all_in_surround<'a>(
v: &ChartView<'a>,
i: usize,
keys: &[StarKey],
) -> Option<Vec<(usize, &'a Star)>> {
keys.iter().map(|k| v.find_in_surround(i, *k)).collect()
}
fn present<'a>(v: &ChartView<'a>, i: usize, keys: &[StarKey]) -> Vec<(usize, &'a Star)> {
keys.iter()
.filter_map(|k| v.find(i, *k).map(|s| (i, s)))
.collect()
}
pub fn wu_tan_tong_xing(v: &ChartView) -> Vec<PatternHit> {
v.soul_and_body()
.into_iter()
.filter(|p| matches!(v.branch(*p), EarthlyBranch::Chou | EarthlyBranch::Wei))
.filter_map(|p| {
let (Some(wu), Some(tan)) =
(v.find(p, StarKey::WuquMaj), v.find(p, StarKey::TanlangMaj))
else {
return None;
};
Some(v.hit(PatternKey::WuTanTongXing, p, vec![(p, wu), (p, tan)]))
})
.collect()
}
pub fn ling_chang_tuo_wu(v: &ChartView) -> Vec<PatternHit> {
let s = v.soul();
let keys = [
StarKey::LingxingMin,
StarKey::WenchangMin,
StarKey::TuoluoMin,
StarKey::WuquMaj,
];
match all_in_surround(v, s, &keys) {
Some(stars) => vec![v.hit(PatternKey::LingChangTuoWu, s, stars)],
None => vec![],
}
}
pub fn xing_qiu_jia_yin(v: &ChartView) -> Vec<PatternHit> {
v.soul_and_body()
.into_iter()
.filter_map(|p| {
let (Some(lian), Some(xiang)) = (
v.find(p, StarKey::LianzhenMaj),
v.find(p, StarKey::TianxiangMaj),
) else {
return None;
};
let xing = present(v, p, &XING);
if xing.is_empty() {
return None;
}
let mut stars = vec![(p, lian), (p, xiang)];
stars.extend(xing);
Some(v.hit(PatternKey::XingQiuJiaYin, p, stars))
})
.collect()
}
pub fn sheng_bu_feng_shi(v: &ChartView) -> Vec<PatternHit> {
let s = v.soul();
let kong = present(v, s, &KONG_WANG);
if kong.is_empty() {
return vec![];
}
[
(StarKey::LianzhenMaj, None),
(StarKey::PojunMaj, Some("pojun")),
]
.into_iter()
.filter_map(|(key, variant)| {
let star = v.find(s, key)?;
let mut stars = vec![(s, star)];
stars.extend(kong.iter().copied());
let mut hit = v.hit(PatternKey::ShengBuFengShi, s, stars);
hit.variant = variant;
Some(hit)
})
.collect()
}
pub fn xiong_su_chao_yuan(v: &ChartView) -> Vec<PatternHit> {
let s = v.soul();
if !matches!(v.branch(s), EarthlyBranch::Yin | EarthlyBranch::Shen) {
return vec![];
}
let Some(lian) = v.find(s, StarKey::LianzhenMaj) else {
return vec![];
};
let mut hit = v.hit(PatternKey::XiongSuChaoYuan, s, vec![(s, lian)]);
hit.broken = v.surround_has_any(s, &SHA6);
vec![hit]
}
pub fn fu_xiang_chao_yuan(v: &ChartView) -> Vec<PatternHit> {
let (career, wealth) = (v.index_of(Palace::Career), v.index_of(Palace::Wealth));
let (Some(fu), Some(xiang)) = (
v.find(career, StarKey::TianfuMaj),
v.find(wealth, StarKey::TianxiangMaj),
) else {
return vec![];
};
let s = v.soul();
let mut hit = v.hit(
PatternKey::FuXiangChaoYuan,
s,
vec![(career, fu), (wealth, xiang)],
);
if v.is_empty(s) {
hit.variant = Some("soul_empty");
}
vec![hit]
}
pub fn huo_tan(v: &ChartView) -> Vec<PatternHit> {
tan_with_fire(v, PatternKey::HuoTan, StarKey::HuoxingMin)
}
pub fn ling_tan(v: &ChartView) -> Vec<PatternHit> {
tan_with_fire(v, PatternKey::LingTan, StarKey::LingxingMin)
}
fn tan_with_fire(v: &ChartView, key: PatternKey, fire: StarKey) -> Vec<PatternHit> {
let s = v.soul();
let Some(tan) = v.find(s, StarKey::TanlangMaj) else {
return vec![];
};
if let Some(f) = v.find(s, fire) {
return vec![v.hit(key, s, vec![(s, tan), (s, f)])];
}
let Some((pf, f)) = v.find_in_surround(s, fire) else {
return vec![];
};
let mut hit = v.hit(key, s, vec![(s, tan), (pf, f)]);
hit.variant = Some("surround");
vec![hit]
}
pub fn shi_zhong_yin_yu(v: &ChartView) -> Vec<PatternHit> {
v.soul_and_body()
.into_iter()
.filter(|p| matches!(v.branch(*p), EarthlyBranch::Zi | EarthlyBranch::Wu))
.filter_map(|p| {
let ju = v.find(p, StarKey::JumenMaj)?;
Some(v.hit(PatternKey::ShiZhongYinYu, p, vec![(p, ju)]))
})
.collect()
}
pub fn liang_ma_piao_dang(v: &ChartView) -> Vec<PatternHit> {
v.soul_and_body()
.into_iter()
.filter_map(|p| {
let (Some(liang), Some(ma)) = (
v.find(p, StarKey::TianliangMaj),
v.find(p, StarKey::TianmaMin),
) else {
return None;
};
Some(v.hit(PatternKey::LiangMaPiaoDang, p, vec![(p, liang), (p, ma)]))
})
.collect()
}
pub fn yang_liang_chang_lu(v: &ChartView) -> Vec<PatternHit> {
let s = v.soul();
let keys = [
StarKey::TaiyangMaj,
StarKey::TianliangMaj,
StarKey::WenchangMin,
StarKey::LucunMin,
];
match all_in_surround(v, s, &keys) {
Some(stars) => vec![v.hit(PatternKey::YangLiangChangLu, s, stars)],
None => vec![],
}
}
pub fn sha_po_lang(v: &ChartView) -> Vec<PatternHit> {
v.soul_and_body()
.into_iter()
.filter(|p| v.has_any(*p, &SPL))
.filter_map(|p| {
let stars = all_in_surround(v, p, &SPL)?;
Some(v.hit(PatternKey::ShaPoLang, p, stars))
})
.collect()
}
pub fn qi_sha_chao_dou(v: &ChartView) -> Vec<PatternHit> {
let s = v.soul();
let variant = match v.branch(s) {
EarthlyBranch::Zi | EarthlyBranch::Yin => "yang_dou",
EarthlyBranch::Wu | EarthlyBranch::Shen => "chao_dou",
_ => return vec![],
};
let Some(sha) = v.find(s, StarKey::QishaMaj) else {
return vec![];
};
let mut hit = v.hit(PatternKey::QiShaChaoDou, s, vec![(s, sha)]);
hit.variant = Some(variant);
vec![hit]
}
pub fn ying_xing_ru_miao(v: &ChartView) -> Vec<PatternHit> {
let s = v.soul();
if !matches!(v.branch(s), EarthlyBranch::Zi | EarthlyBranch::Wu) {
return vec![];
}
let Some(po) = v.find(s, StarKey::PojunMaj) else {
return vec![];
};
vec![v.hit(PatternKey::YingXingRuMiao, s, vec![(s, po)])]
}
pub fn zhong_shui_chao_dong(v: &ChartView) -> Vec<PatternHit> {
let s = v.soul();
if !matches!(v.branch(s), EarthlyBranch::Yin | EarthlyBranch::Mao) {
return vec![];
}
let (Some(po), Some(qu)) = (v.find(s, StarKey::PojunMaj), v.find(s, StarKey::WenquMin)) else {
return vec![];
};
vec![v.hit(PatternKey::ZhongShuiChaoDong, s, vec![(s, po), (s, qu)])]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::astrolabe::Astrolabe;
use crate::pattern::PatternConfig;
use crate::pattern::testutil::find_chart;
fn view(a: &Astrolabe) -> ChartView<'_> {
ChartView::natal(a, &PatternConfig::default())
}
fn hits(a: &Astrolabe, key: PatternKey) -> Vec<PatternHit> {
let v = view(a);
RULES
.iter()
.filter(|r| r.key == key)
.flat_map(|r| (r.detect)(&v))
.collect()
}
fn one(a: &Astrolabe, key: PatternKey) -> PatternHit {
let mut h = hits(a, key);
assert_eq!(h.len(), 1, "expected exactly one hit of {:?}", key.as_key());
h.pop().unwrap()
}
fn star_keys(h: &PatternHit) -> Vec<StarKey> {
h.stars.iter().map(|s| s.star).collect()
}
#[test]
fn wu_tan_tong_xing_needs_both_stars_together() {
let a = find_chart(|a| {
let v = view(a);
v.soul_and_body()
.iter()
.any(|p| v.has_all(*p, &[StarKey::WuquMaj, StarKey::TanlangMaj]))
});
let h = one(&a, PatternKey::WuTanTongXing);
let v = view(&a);
assert!(v.soul_and_body().contains(&h.palace));
assert!(matches!(
v.branch(h.palace),
EarthlyBranch::Chou | EarthlyBranch::Wei
));
assert_eq!(star_keys(&h), vec![StarKey::WuquMaj, StarKey::TanlangMaj]);
assert!(h.stars.iter().all(|s| s.palace == h.palace));
let b = find_chart(|a| {
let v = view(a);
v.has(v.soul(), StarKey::WuquMaj)
&& !v
.soul_and_body()
.iter()
.any(|p| v.has_all(*p, &[StarKey::WuquMaj, StarKey::TanlangMaj]))
});
assert!(hits(&b, PatternKey::WuTanTongXing).is_empty());
}
#[test]
fn ling_chang_tuo_wu_needs_all_four_in_surround() {
const FOUR: [StarKey; 4] = [
StarKey::LingxingMin,
StarKey::WenchangMin,
StarKey::TuoluoMin,
StarKey::WuquMaj,
];
let count = |a: &Astrolabe| {
let v = view(a);
FOUR.iter().filter(|k| v.in_surround(v.soul(), **k)).count()
};
let a = find_chart(|a| count(a) == 4);
let h = one(&a, PatternKey::LingChangTuoWu);
let v = view(&a);
assert_eq!(h.palace, v.soul());
assert_eq!(star_keys(&h), FOUR.to_vec());
assert!(
h.stars
.iter()
.all(|s| v.surround(v.soul()).contains(&s.palace))
);
let b = find_chart(|a| count(a) == 3);
assert!(hits(&b, PatternKey::LingChangTuoWu).is_empty());
}
#[test]
fn xing_qiu_jia_yin_needs_lian_xiang_and_a_xing_star() {
let lian_xiang =
|v: &ChartView, p: usize| v.has_all(p, &[StarKey::LianzhenMaj, StarKey::TianxiangMaj]);
let a = find_chart(|a| {
let v = view(a);
v.soul_and_body()
.iter()
.any(|p| lian_xiang(&v, *p) && v.has_any(*p, &XING))
});
let h = one(&a, PatternKey::XingQiuJiaYin);
let v = view(&a);
assert!(v.soul_and_body().contains(&h.palace));
assert!(matches!(
v.branch(h.palace),
EarthlyBranch::Zi | EarthlyBranch::Wu
));
assert!(star_keys(&h).starts_with(&[StarKey::LianzhenMaj, StarKey::TianxiangMaj]));
assert!(h.stars.iter().skip(2).all(|s| XING.contains(&s.star)));
let b = find_chart(|a| {
let v = view(a);
let s = v.soul();
lian_xiang(&v, s) && !v.soul_and_body().iter().any(|p| v.has_any(*p, &XING))
});
assert!(hits(&b, PatternKey::XingQiuJiaYin).is_empty());
}
#[test]
fn sheng_bu_feng_shi_reports_lianzhen_and_pojun_forms() {
let a = find_chart(|a| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::LianzhenMaj) && v.has_any(s, &KONG_WANG)
});
let h = one(&a, PatternKey::ShengBuFengShi);
assert_eq!(h.palace, view(&a).soul());
assert_eq!(h.variant, None);
assert_eq!(h.stars[0].star, StarKey::LianzhenMaj);
assert!(h.stars[1..].iter().all(|s| KONG_WANG.contains(&s.star)));
let b = find_chart(|a| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::PojunMaj)
&& !v.has(s, StarKey::LianzhenMaj)
&& v.has_any(s, &KONG_WANG)
});
assert_eq!(one(&b, PatternKey::ShengBuFengShi).variant, Some("pojun"));
let c = find_chart(|a| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::LianzhenMaj) && !v.has_any(s, &KONG_WANG)
});
assert!(hits(&c, PatternKey::ShengBuFengShi).is_empty());
}
#[test]
fn xiong_su_chao_yuan_limited_to_yin_shen_and_marks_sha() {
let at = |a: &Astrolabe, branches: [EarthlyBranch; 2]| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::LianzhenMaj) && branches.contains(&v.branch(s))
};
let clean = find_chart(|a| {
let v = view(a);
at(a, [EarthlyBranch::Yin, EarthlyBranch::Shen]) && !v.surround_has_any(v.soul(), &SHA6)
});
let h = one(&clean, PatternKey::XiongSuChaoYuan);
assert_eq!(h.palace, view(&clean).soul());
assert_eq!(star_keys(&h), vec![StarKey::LianzhenMaj]);
assert!(!h.broken);
let with_sha = find_chart(|a| {
let v = view(a);
at(a, [EarthlyBranch::Yin, EarthlyBranch::Shen]) && v.surround_has_any(v.soul(), &SHA6)
});
assert!(one(&with_sha, PatternKey::XiongSuChaoYuan).broken);
let wei = find_chart(|a| at(a, [EarthlyBranch::Wei, EarthlyBranch::Wei]));
assert!(hits(&wei, PatternKey::XiongSuChaoYuan).is_empty());
}
#[test]
fn fu_xiang_chao_yuan_reads_career_and_wealth() {
let fu_at_career = |a: &Astrolabe| {
let v = view(a);
v.has(v.index_of(Palace::Career), StarKey::TianfuMaj)
};
let a = find_chart(|a| fu_at_career(a) && !view(a).is_empty(view(a).soul()));
let h = one(&a, PatternKey::FuXiangChaoYuan);
let v = view(&a);
assert_eq!(h.palace, v.soul());
assert_eq!(h.variant, None);
assert_eq!(
star_keys(&h),
vec![StarKey::TianfuMaj, StarKey::TianxiangMaj]
);
assert_eq!(h.stars[0].palace, v.index_of(Palace::Career));
assert_eq!(h.stars[1].palace, v.index_of(Palace::Wealth));
let b = find_chart(|a| fu_at_career(a) && view(a).is_empty(view(a).soul()));
assert_eq!(
one(&b, PatternKey::FuXiangChaoYuan).variant,
Some("soul_empty")
);
let c = find_chart(|a| !fu_at_career(a));
assert!(hits(&c, PatternKey::FuXiangChaoYuan).is_empty());
}
#[test]
fn huo_tan_separates_same_palace_from_surround() {
let a = find_chart(|a| {
let v = view(a);
v.has_all(v.soul(), &[StarKey::TanlangMaj, StarKey::HuoxingMin])
});
let h = one(&a, PatternKey::HuoTan);
let v = view(&a);
assert_eq!(h.palace, v.soul());
assert_eq!(h.variant, None);
assert!(h.stars.iter().all(|s| s.palace == v.soul()));
let b = find_chart(|a| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::TanlangMaj)
&& !v.has(s, StarKey::HuoxingMin)
&& v.in_surround(s, StarKey::HuoxingMin)
});
let hb = one(&b, PatternKey::HuoTan);
assert_eq!(hb.variant, Some("surround"));
assert_ne!(hb.stars[1].palace, hb.palace);
let c = find_chart(|a| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::TanlangMaj) && !v.in_surround(s, StarKey::HuoxingMin)
});
assert!(hits(&c, PatternKey::HuoTan).is_empty());
}
#[test]
fn ling_tan_mirrors_huo_tan() {
let a = find_chart(|a| {
let v = view(a);
v.has_all(v.soul(), &[StarKey::TanlangMaj, StarKey::LingxingMin])
});
let h = one(&a, PatternKey::LingTan);
assert_eq!(h.variant, None);
assert_eq!(
star_keys(&h),
vec![StarKey::TanlangMaj, StarKey::LingxingMin]
);
let b = find_chart(|a| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::TanlangMaj)
&& !v.has(s, StarKey::LingxingMin)
&& v.in_surround(s, StarKey::LingxingMin)
});
assert_eq!(one(&b, PatternKey::LingTan).variant, Some("surround"));
let c = find_chart(|a| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::TanlangMaj) && !v.in_surround(s, StarKey::LingxingMin)
});
assert!(hits(&c, PatternKey::LingTan).is_empty());
}
#[test]
fn shi_zhong_yin_yu_limited_to_zi_wu() {
let a = find_chart(|a| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::JumenMaj)
&& matches!(v.branch(s), EarthlyBranch::Zi | EarthlyBranch::Wu)
});
let h = one(&a, PatternKey::ShiZhongYinYu);
assert_eq!(h.palace, view(&a).soul());
assert_eq!(star_keys(&h), vec![StarKey::JumenMaj]);
let b = find_chart(|a| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::JumenMaj)
&& v.branch(s) == EarthlyBranch::Hai
&& !v.soul_and_body().iter().any(|p| {
v.has(*p, StarKey::JumenMaj)
&& matches!(v.branch(*p), EarthlyBranch::Zi | EarthlyBranch::Wu)
})
});
assert!(hits(&b, PatternKey::ShiZhongYinYu).is_empty());
}
#[test]
fn liang_ma_piao_dang_needs_liang_and_ma_together() {
let a = find_chart(|a| {
let v = view(a);
v.soul_and_body()
.iter()
.any(|p| v.has_all(*p, &[StarKey::TianliangMaj, StarKey::TianmaMin]))
});
let h = one(&a, PatternKey::LiangMaPiaoDang);
let v = view(&a);
assert!(v.soul_and_body().contains(&h.palace));
assert_eq!(
star_keys(&h),
vec![StarKey::TianliangMaj, StarKey::TianmaMin]
);
let b = find_chart(|a| {
let v = view(a);
v.has(v.soul(), StarKey::TianliangMaj)
&& !v
.soul_and_body()
.iter()
.any(|p| v.has_all(*p, &[StarKey::TianliangMaj, StarKey::TianmaMin]))
});
assert!(hits(&b, PatternKey::LiangMaPiaoDang).is_empty());
}
#[test]
fn yang_liang_chang_lu_needs_all_four_in_surround() {
const FOUR: [StarKey; 4] = [
StarKey::TaiyangMaj,
StarKey::TianliangMaj,
StarKey::WenchangMin,
StarKey::LucunMin,
];
let count = |a: &Astrolabe| {
let v = view(a);
FOUR.iter().filter(|k| v.in_surround(v.soul(), **k)).count()
};
let a = find_chart(|a| count(a) == 4);
let h = one(&a, PatternKey::YangLiangChangLu);
assert_eq!(h.palace, view(&a).soul());
assert_eq!(star_keys(&h), FOUR.to_vec());
let b = find_chart(|a| count(a) == 3);
assert!(hits(&b, PatternKey::YangLiangChangLu).is_empty());
}
#[test]
fn sha_po_lang_hits_soul_and_body_separately() {
let a = find_chart(|a| {
let v = view(a);
v.has_any(v.soul(), &SPL)
});
let h = &hits(&a, PatternKey::ShaPoLang)[0];
let v = view(&a);
assert_eq!(h.palace, v.soul());
assert_eq!(star_keys(h), SPL.to_vec());
assert!(
h.stars
.iter()
.all(|s| v.surround(v.soul()).contains(&s.palace))
);
let b = find_chart(|a| {
let v = view(a);
let body = v.body();
!v.has_any(v.soul(), &SPL) && body.is_some_and(|b| v.has_any(b, &SPL))
});
let hb = one(&b, PatternKey::ShaPoLang);
assert_eq!(Some(hb.palace), view(&b).body());
let c = find_chart(|a| {
let v = view(a);
!v.soul_and_body().iter().any(|p| v.has_any(*p, &SPL))
});
assert!(hits(&c, PatternKey::ShaPoLang).is_empty());
}
#[test]
fn qi_sha_chao_dou_names_both_variants() {
let qi_sha_at = |branch: EarthlyBranch| {
move |a: &Astrolabe| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::QishaMaj) && v.branch(s) == branch
}
};
let zi = find_chart(qi_sha_at(EarthlyBranch::Zi));
assert_eq!(one(&zi, PatternKey::QiShaChaoDou).variant, Some("yang_dou"));
let shen = find_chart(qi_sha_at(EarthlyBranch::Shen));
let h = one(&shen, PatternKey::QiShaChaoDou);
assert_eq!(h.variant, Some("chao_dou"));
assert_eq!(star_keys(&h), vec![StarKey::QishaMaj]);
assert_eq!(h.palace, view(&shen).soul());
let chen = find_chart(qi_sha_at(EarthlyBranch::Chen));
assert!(hits(&chen, PatternKey::QiShaChaoDou).is_empty());
}
#[test]
fn ying_xing_ru_miao_limited_to_zi_wu() {
let po_at = |branches: [EarthlyBranch; 2]| {
move |a: &Astrolabe| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::PojunMaj) && branches.contains(&v.branch(s))
}
};
let a = find_chart(po_at([EarthlyBranch::Zi, EarthlyBranch::Wu]));
let h = one(&a, PatternKey::YingXingRuMiao);
assert_eq!(h.palace, view(&a).soul());
assert_eq!(
h.stars[0].brightness,
Some(crate::data::types::Brightness::Miao)
);
assert!(!h.broken);
let b = find_chart(po_at([EarthlyBranch::Yin, EarthlyBranch::Yin]));
assert!(hits(&b, PatternKey::YingXingRuMiao).is_empty());
}
#[test]
fn zhong_shui_chao_dong_needs_pojun_wenqu_in_yin_mao() {
let a = find_chart(|a| {
let v = view(a);
let s = v.soul();
v.has_all(s, &[StarKey::PojunMaj, StarKey::WenquMin])
&& matches!(v.branch(s), EarthlyBranch::Yin | EarthlyBranch::Mao)
});
let h = one(&a, PatternKey::ZhongShuiChaoDong);
assert_eq!(h.palace, view(&a).soul());
assert_eq!(star_keys(&h), vec![StarKey::PojunMaj, StarKey::WenquMin]);
let b = find_chart(|a| {
let v = view(a);
let s = v.soul();
v.has(s, StarKey::PojunMaj)
&& v.branch(s) == EarthlyBranch::Yin
&& !v.has(s, StarKey::WenquMin)
});
assert!(hits(&b, PatternKey::ZhongShuiChaoDong).is_empty());
}
}