use tyme4rs::tyme::Culture;
use tyme4rs::tyme::culture::Direction;
use tyme4rs::tyme::enums::YinYang;
use tyme4rs::tyme::sixtycycle::EarthBranch;
use tyme4rs::tyme::sixtycycle::HeavenStem;
use tyme4rs::tyme::sixtycycle::SixtyCycle;
use tyme4rs::tyme::solar::SolarTerm;
use tyme4rs::tyme::solar::SolarTime;
use crate::enums::QimenChartType;
use crate::enums::QimenDoor;
#[cfg(test)]
use crate::enums::QimenGod;
use crate::enums::QimenMethod;
use crate::enums::QimenStar;
use crate::enums::QimenYuan;
use crate::error::QimenError;
use crate::error::Result;
use crate::hexagram::Hexagram;
use crate::hexagram::Trigram;
use crate::palace::QimenDutyDoor;
use crate::palace::QimenDutyStar;
use crate::palace::QimenHeavenStemPlacement;
use crate::palace::QimenOptions;
use crate::palace::QimenPalace;
use crate::pattern::Pattern;
use crate::pattern::detect_patterns;
use crate::plate::GRID;
use crate::plate::PALACE_NAMES;
use crate::plate::TEN_KONG_BRANCHES;
use crate::plate::TEN_XUN_SHOU;
use crate::plate::TERM_JU;
use crate::plate::build_door_plate;
use crate::plate::build_earth_plate;
use crate::plate::build_god_plate;
use crate::plate::build_heaven_plate;
use crate::plate::build_hidden_plate;
use crate::plate::build_star_plate;
use crate::plate::find_door_palace;
use crate::plate::find_hour_stem_palace;
use crate::plate::find_stem_palace;
use crate::plate::palace_branch_indices;
use crate::shensha::ShenSha;
use crate::shensha::detect_shen_sha;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Qimen {
solar_time: SolarTime,
options: QimenOptions,
year: SixtyCycle,
month: SixtyCycle,
day: SixtyCycle,
hour: SixtyCycle,
term: SolarTerm,
yin_yang: YinYang,
ju: u8,
yuan: QimenYuan,
xun_shou: HeavenStem,
zhi_fu: QimenDutyStar,
zhi_shi: QimenDutyDoor,
kong_wang: [EarthBranch; 2],
palaces: [QimenPalace; 9],
}
impl Qimen {
pub fn new(solar_time: SolarTime) -> Self {
Self::with_options(solar_time, QimenOptions::default()).expect("default options must succeed")
}
pub fn with_options(solar_time: SolarTime, options: QimenOptions) -> Result<Self> {
validate_options(options)?;
let sixty_cycle_hour = solar_time.get_sixty_cycle_hour();
let year = sixty_cycle_hour.get_year();
let month = sixty_cycle_hour.get_month();
let day = sixty_cycle_hour.get_day();
let hour = sixty_cycle_hour.get_sixty_cycle();
let term = solar_time.get_term();
let yin_yang = compute_yin_yang(solar_time);
let yuan = compute_yuan(&day);
let ju = compute_ju(&term, yuan)?;
let earth = build_earth_plate(yin_yang, ju);
let xun_shou = compute_xun_shou(&hour);
let hour_heaven_stem = hour.get_heaven_stem();
let zhi_fu_original_palace = find_stem_palace(&earth, &xun_shou, true).unwrap_or(2);
let zhi_fu_palace = find_hour_stem_palace(&earth, &hour_heaven_stem, zhi_fu_original_palace);
let heaven = build_heaven_plate(&earth, yin_yang, zhi_fu_original_palace, zhi_fu_palace);
let stars = build_star_plate(zhi_fu_original_palace, zhi_fu_palace);
let doors = build_door_plate(yin_yang, zhi_fu_original_palace, &hour);
let gods = build_god_plate(yin_yang, zhi_fu_palace);
let hidden = build_hidden_plate(yin_yang);
let zhi_fu_star = stars.get(zhi_fu_palace).copied().unwrap_or(QimenStar::QinRui);
let zhi_shi_door = QimenDoor::from_palace(zhi_fu_original_palace).unwrap_or(QimenDoor::Death);
let zhi_shi_palace = find_door_palace(&doors, zhi_shi_door).unwrap_or(zhi_fu_palace);
let kong_wang = compute_kong_wang(&hour);
let mut palaces: [QimenPalace; 9] = std::array::from_fn(|i| {
let palace = (i + 1) as u8;
let star = stars.cloned(palace);
let door = doors.cloned(palace);
let god = gods.cloned(palace);
let heaven_heaven_stem = heaven
.cloned(palace)
.unwrap_or_else(|| earth.cloned(palace).unwrap_or_else(|| HeavenStem::from_index(0)));
QimenPalace {
number: palace,
name: PALACE_NAMES[palace as usize],
direction: Direction::from_index((palace as isize) - 1),
earth_branches: branches_for_palace(palace),
earth_heaven_stem: earth.cloned(palace).unwrap_or_else(|| HeavenStem::from_index(0)),
san_qi_liu_yi: earth.cloned(palace).unwrap_or_else(|| HeavenStem::from_index(0)),
heaven_heaven_stem,
hidden_heaven_stem: hidden.cloned(palace).unwrap_or_else(|| HeavenStem::from_index(0)),
star,
door,
god,
ten_star: None,
terrain: None,
hexagram: None,
patterns: Vec::new(),
shen_sha: Vec::new(),
}
});
let day_stem = day.get_heaven_stem();
for p in &mut palaces {
if p.number != 5 {
p.ten_star = Some(day_stem.get_ten_star(p.earth_heaven_stem.clone()));
}
p.terrain = p.earth_branches.first().map(|b| day_stem.get_terrain(b.clone()));
p.hexagram = (|| {
let lower = Trigram::from_palace(p.number)?;
let upper = Trigram::from_palace(p.door?.home_palace())?;
Some(Hexagram::new(upper, lower))
})();
}
let all_patterns = detect_patterns(zhi_fu_original_palace, zhi_fu_palace, &palaces, &kong_wang);
for pattern in all_patterns {
let n = pattern.palace() as usize;
if (1..=9).contains(&n) {
palaces[n - 1].patterns.push(pattern);
}
}
let all_shen_sha = detect_shen_sha(
&year.get_heaven_stem(),
&month.get_earth_branch(),
&day.get_heaven_stem(),
&day.get_earth_branch(),
&earth,
);
for shen_sha in all_shen_sha {
let n = shen_sha.palace() as usize;
if (1..=9).contains(&n) {
palaces[n - 1].shen_sha.push(shen_sha);
}
}
Ok(Self {
solar_time,
options,
year,
month,
day,
hour,
term,
yin_yang,
ju,
yuan,
xun_shou,
zhi_fu: QimenDutyStar { star: zhi_fu_star, original_palace: zhi_fu_original_palace, palace: zhi_fu_palace },
zhi_shi: QimenDutyDoor {
door: zhi_shi_door,
original_palace: zhi_fu_original_palace,
palace: zhi_shi_palace,
},
kong_wang,
palaces,
})
}
pub fn solar_time(&self) -> SolarTime { self.solar_time }
pub fn options(&self) -> QimenOptions { self.options }
pub fn year(&self) -> &SixtyCycle { &self.year }
pub fn month(&self) -> &SixtyCycle { &self.month }
pub fn day(&self) -> &SixtyCycle { &self.day }
pub fn hour(&self) -> &SixtyCycle { &self.hour }
pub fn term(&self) -> &SolarTerm { &self.term }
pub fn yin_yang(&self) -> YinYang { self.yin_yang }
pub fn ju(&self) -> u8 { self.ju }
pub fn yuan(&self) -> QimenYuan { self.yuan }
pub fn xun_shou(&self) -> &HeavenStem { &self.xun_shou }
pub fn zhi_fu(&self) -> &QimenDutyStar { &self.zhi_fu }
pub fn zhi_shi(&self) -> &QimenDutyDoor { &self.zhi_shi }
pub fn kong_wang(&self) -> &[EarthBranch; 2] { &self.kong_wang }
pub fn stem_palace(&self, stem: &HeavenStem) -> u8 {
let idx = stem.get_index();
if idx == 0 {
return self.zhi_fu.original_palace;
}
match self.palaces.iter().find(|p| p.earth_heaven_stem().get_index() == idx).map(QimenPalace::number) {
Some(5) => 2, Some(n) => n,
None => self.zhi_fu.original_palace,
}
}
pub fn self_palace(&self) -> u8 { self.stem_palace(&self.day.get_heaven_stem()) }
pub fn opponent_palace(&self) -> u8 { self.zhi_fu.palace }
pub fn palaces(&self) -> &[QimenPalace; 9] { &self.palaces }
pub fn palace(&self, number: u8) -> Option<&QimenPalace> {
if !(1..=9).contains(&number) {
return None;
}
Some(&self.palaces[number as usize - 1])
}
pub fn grid(&self) -> [[&QimenPalace; 3]; 3] {
std::array::from_fn(|row| std::array::from_fn(|col| &self.palaces[GRID[row][col] as usize - 1]))
}
pub fn san_qi_liu_yi(&self) -> Vec<QimenHeavenStemPlacement> {
self.palaces
.iter()
.map(|palace| QimenHeavenStemPlacement {
palace: palace.number(),
heaven_stem: palace.san_qi_liu_yi().clone(),
})
.collect()
}
pub fn tian_pan(&self) -> Vec<QimenHeavenStemPlacement> {
self.palaces
.iter()
.map(|palace| QimenHeavenStemPlacement {
palace: palace.number(),
heaven_stem: palace.heaven_heaven_stem().clone(),
})
.collect()
}
pub fn hidden_heaven_stems(&self) -> Vec<QimenHeavenStemPlacement> {
self.palaces
.iter()
.map(|palace| QimenHeavenStemPlacement {
palace: palace.number(),
heaven_stem: palace.hidden_heaven_stem().clone(),
})
.collect()
}
pub fn patterns(&self) -> impl Iterator<Item = &Pattern> { self.palaces.iter().flat_map(|p| p.patterns().iter()) }
pub fn shen_sha(&self) -> impl Iterator<Item = &ShenSha> { self.palaces.iter().flat_map(|p| p.shen_sha().iter()) }
}
pub trait SolarTimeQimenExt {
fn qimen(&self) -> Qimen;
fn qimen_with_options(&self, options: QimenOptions) -> Result<Qimen>;
}
impl SolarTimeQimenExt for SolarTime {
fn qimen(&self) -> Qimen { Qimen::new(*self) }
fn qimen_with_options(&self, options: QimenOptions) -> Result<Qimen> { Qimen::with_options(*self, options) }
}
fn validate_options(options: QimenOptions) -> Result<()> {
if options.method != QimenMethod::Time {
return Err(QimenError::UnsupportedMethod(options.method));
}
if options.chart_type != QimenChartType::SanYuan {
return Err(QimenError::UnsupportedChartType(options.chart_type));
}
Ok(())
}
fn compute_yin_yang(solar_time: SolarTime) -> YinYang {
let year = solar_time.get_year();
let winter = SolarTerm::from_index(year, 0).get_julian_day().get_solar_time();
let summer = SolarTerm::from_index(year, 12).get_julian_day().get_solar_time();
let next_winter = SolarTerm::from_index(year + 1, 0).get_julian_day().get_solar_time();
if (!solar_time.is_before(winter) && solar_time.is_before(summer)) || !solar_time.is_before(next_winter) {
YinYang::YANG
} else {
YinYang::YIN
}
}
fn compute_yuan(day_cycle: &SixtyCycle) -> QimenYuan {
match day_cycle.get_index() % 15 {
0..=4 => QimenYuan::Upper,
5..=9 => QimenYuan::Middle,
_ => QimenYuan::Lower,
}
}
fn compute_ju(term: &SolarTerm, yuan: QimenYuan) -> Result<u8> {
let idx = term.get_index();
if idx >= TERM_JU.len() {
return Err(QimenError::UnsupportedTerm(term.get_name()));
}
let row = TERM_JU[idx];
let column = match yuan {
QimenYuan::Upper => 0,
QimenYuan::Middle => 1,
QimenYuan::Lower => 2,
};
Ok(row[column])
}
fn compute_xun_shou(hour: &SixtyCycle) -> HeavenStem {
let ten_idx = hour.get_ten().get_index().min(5);
HeavenStem::from_index(TEN_XUN_SHOU[ten_idx] as isize)
}
fn compute_kong_wang(hour: &SixtyCycle) -> [EarthBranch; 2] {
let ten_idx = hour.get_ten().get_index().min(5);
let pair = TEN_KONG_BRANCHES[ten_idx];
[EarthBranch::from_index(pair[0] as isize), EarthBranch::from_index(pair[1] as isize)]
}
fn branches_for_palace(palace: u8) -> Vec<EarthBranch> {
palace_branch_indices(palace).iter().map(|&i| EarthBranch::from_index(i as isize)).collect()
}
#[cfg(test)]
mod tests {
use tyme4rs::tyme::Tyme;
use tyme4rs::tyme::solar::SolarTime;
use super::*;
use crate::auspice::Auspicious;
use crate::pattern::Pattern;
use crate::shensha::ShenShaKind;
fn names(items: &[QimenHeavenStemPlacement], palace: u8) -> String {
items.iter().find(|item| item.palace() == palace).map(|item| item.heaven_stem().to_string()).unwrap_or_default()
}
#[test]
fn test_yuan_by_day_branch() {
assert_eq!(QimenYuan::Upper, compute_yuan(&SixtyCycle::from_name("甲子")));
assert_eq!(QimenYuan::Middle, compute_yuan(&SixtyCycle::from_name("己巳")));
assert_eq!(QimenYuan::Lower, compute_yuan(&SixtyCycle::from_name("甲戌")));
assert_eq!(QimenYuan::Middle, compute_yuan(&SixtyCycle::from_name("戊子")));
}
#[test]
fn test_ju_table() {
let term = SolarTerm::from_name(2026, "小寒");
assert_eq!(2, compute_ju(&term, QimenYuan::Upper).unwrap());
assert_eq!(8, compute_ju(&term, QimenYuan::Middle).unwrap());
assert_eq!(5, compute_ju(&term, QimenYuan::Lower).unwrap());
let term = SolarTerm::from_name(2026, "霜降");
assert_eq!(5, compute_ju(&term, QimenYuan::Upper).unwrap());
assert_eq!(8, compute_ju(&term, QimenYuan::Middle).unwrap());
assert_eq!(2, compute_ju(&term, QimenYuan::Lower).unwrap());
}
#[test]
fn test_solar_term_boundary_yin_yang() {
let winter = SolarTerm::from_index(2027, 0).get_julian_day().get_solar_time();
assert_eq!(YinYang::YANG, compute_yin_yang(winter));
assert_eq!(YinYang::YIN, compute_yin_yang(winter.next(-1)));
let summer = SolarTerm::from_name(2026, "夏至").get_julian_day().get_solar_time();
assert_eq!(YinYang::YIN, compute_yin_yang(summer));
assert_eq!(YinYang::YANG, compute_yin_yang(summer.next(-1)));
}
#[test]
fn test_xun_shou() {
assert_eq!("戊", compute_xun_shou(&SixtyCycle::from_name("甲子")).to_string());
assert_eq!("己", compute_xun_shou(&SixtyCycle::from_name("乙亥")).to_string());
assert_eq!("庚", compute_xun_shou(&SixtyCycle::from_name("戊子")).to_string());
assert_eq!("辛", compute_xun_shou(&SixtyCycle::from_name("癸卯")).to_string());
assert_eq!("壬", compute_xun_shou(&SixtyCycle::from_name("壬子")).to_string());
assert_eq!("癸", compute_xun_shou(&SixtyCycle::from_name("辛酉")).to_string());
}
#[test]
fn test_yang_snapshot() {
let qimen = SolarTime::from_ymd_hms(2026, 1, 14, 18, 45, 0).qimen();
assert_eq!("小寒", qimen.term().to_string());
assert_eq!(YinYang::YANG, qimen.yin_yang());
assert_eq!(QimenYuan::Middle, qimen.yuan());
assert_eq!(8, qimen.ju());
assert_eq!("癸", qimen.xun_shou().to_string());
assert_eq!("乙巳", qimen.year().to_string());
assert_eq!("己丑", qimen.month().to_string());
assert_eq!("戊子", qimen.day().to_string());
assert_eq!("辛酉", qimen.hour().to_string());
assert_eq!(QimenStar::TianFu, qimen.zhi_fu().star());
assert_eq!(4, qimen.zhi_fu().original_palace());
assert_eq!(2, qimen.zhi_fu().palace());
assert_eq!(QimenDoor::Block, qimen.zhi_shi().door());
assert_eq!(4, qimen.zhi_shi().original_palace());
assert_eq!(2, qimen.zhi_shi().palace());
let san_qi = qimen.san_qi_liu_yi();
assert_eq!("庚", names(&san_qi, 1));
assert_eq!("辛", names(&san_qi, 2));
assert_eq!("壬", names(&san_qi, 3));
assert_eq!("癸", names(&san_qi, 4));
assert_eq!("丁", names(&san_qi, 5));
assert_eq!("丙", names(&san_qi, 6));
assert_eq!("乙", names(&san_qi, 7));
assert_eq!("戊", names(&san_qi, 8));
assert_eq!("己", names(&san_qi, 9));
let tian = qimen.tian_pan();
assert_eq!("乙", names(&tian, 1));
assert_eq!("癸", names(&tian, 2));
assert_eq!("庚", names(&tian, 3));
assert_eq!("戊", names(&tian, 4));
assert_eq!("丁", names(&tian, 5));
assert_eq!("辛", names(&tian, 6));
assert_eq!("己", names(&tian, 7));
assert_eq!("丙", names(&tian, 8));
assert_eq!("壬", names(&tian, 9));
let hidden = qimen.hidden_heaven_stems();
assert_eq!("庚", names(&hidden, 1));
assert_eq!("辛", names(&hidden, 2));
assert_eq!("壬", names(&hidden, 3));
assert_eq!("癸", names(&hidden, 4));
assert_eq!("丁", names(&hidden, 5));
assert_eq!("丙", names(&hidden, 6));
assert_eq!("乙", names(&hidden, 7));
assert_eq!("戊", names(&hidden, 8));
assert_eq!("己", names(&hidden, 9));
let kw = qimen.kong_wang();
assert_eq!(["子", "丑"], [kw[0].to_string().as_str(), kw[1].to_string().as_str()]);
let kong_wang_palaces: Vec<u8> = qimen
.patterns()
.filter_map(|p| match p {
Pattern::KongWang { palace, .. } => Some(*palace),
_ => None,
})
.collect();
assert!(kong_wang_palaces.contains(&1));
assert!(kong_wang_palaces.contains(&8));
let yi_ma: Vec<&ShenSha> = qimen.shen_sha().filter(|s| s.kind() == ShenShaKind::YiMa).collect();
assert_eq!(yi_ma.len(), 1);
assert_eq!(yi_ma[0].palace(), 8);
let expected: [(u8, _, _, _, &str, &str); 8] = [
(1, QimenStar::TianZhu, QimenDoor::Fear, QimenGod::LiuHe, "乙", "庚"),
(2, QimenStar::TianFu, QimenDoor::Block, QimenGod::ZhiFu, "癸", "辛"),
(3, QimenStar::TianPeng, QimenDoor::Rest, QimenGod::XuanWu, "庚", "壬"),
(4, QimenStar::TianRen, QimenDoor::Life, QimenGod::JiuDi, "戊", "癸"),
(6, QimenStar::QinRui, QimenDoor::Death, QimenGod::TaiYin, "辛", "丙"),
(7, QimenStar::TianYing, QimenDoor::View, QimenGod::TengShe, "己", "乙"),
(8, QimenStar::TianXin, QimenDoor::Open, QimenGod::BaiHu, "丙", "戊"),
(9, QimenStar::TianChong, QimenDoor::Hurt, QimenGod::JiuTian, "壬", "己"),
];
for (number, star, door, god, heaven, earth) in expected {
let palace = qimen.palace(number).unwrap();
assert_eq!(Some(star), palace.star());
assert_eq!(Some(door), palace.door());
assert_eq!(Some(god), palace.god());
assert_eq!(heaven, palace.heaven_heaven_stem().to_string());
assert_eq!(earth, palace.earth_heaven_stem().to_string());
}
let center = qimen.palace(5).unwrap();
assert_eq!(None, center.star());
assert_eq!(None, center.door());
assert_eq!(None, center.god());
assert_eq!("丁", center.heaven_heaven_stem().to_string());
assert_eq!("丁", center.earth_heaven_stem().to_string());
}
#[test]
fn test_yin_snapshot() {
let qimen = SolarTime::from_ymd_hms(2026, 10, 31, 12, 2, 0).qimen();
assert_eq!("霜降", qimen.term().to_string());
assert_eq!(YinYang::YIN, qimen.yin_yang());
assert_eq!(QimenYuan::Lower, qimen.yuan());
assert_eq!(2, qimen.ju());
assert_eq!("癸", qimen.xun_shou().to_string());
let zhi_fu_palace = qimen.zhi_fu().palace();
assert_eq!(Some(QimenGod::ZhiFu), qimen.palace(zhi_fu_palace).unwrap().god());
assert_eq!(qimen.zhi_shi().door(), qimen.palace(qimen.zhi_shi().palace()).unwrap().door().unwrap());
let hidden = qimen.hidden_heaven_stems();
assert_eq!("丙", names(&hidden, 1));
assert_eq!("癸", names(&hidden, 3));
assert_eq!("庚", names(&hidden, 6));
assert_eq!("己", names(&hidden, 7));
assert_eq!("戊", names(&hidden, 8));
let kw = qimen.kong_wang();
assert_eq!(["子", "丑"], [kw[0].to_string().as_str(), kw[1].to_string().as_str()]);
}
#[test]
fn test_grid_and_options() {
let solar_time = SolarTime::from_ymd_hms(2026, 3, 2, 18, 30, 0);
let qimen = solar_time.qimen();
let grid = qimen.grid();
assert_eq!(4, grid[0][0].number());
assert_eq!(5, grid[1][1].number());
assert_eq!(6, grid[2][2].number());
let err = solar_time
.qimen_with_options(QimenOptions { method: QimenMethod::Day, chart_type: QimenChartType::SanYuan })
.unwrap_err();
assert_eq!("unsupported qimen method: 日家", err.to_string());
}
#[test]
fn test_palace_lookup_bounds() {
let qimen = SolarTime::from_ymd_hms(2026, 1, 14, 18, 45, 0).qimen();
assert!(qimen.palace(0).is_none());
assert!(qimen.palace(10).is_none());
for n in 1..=9u8 {
assert!(qimen.palace(n).is_some());
}
}
#[test]
fn test_self_palace_non_jia_day() {
let qimen = SolarTime::from_ymd_hms(2026, 1, 14, 18, 45, 0).qimen();
assert_eq!(qimen.day().get_heaven_stem().to_string(), "戊");
assert_eq!(qimen.self_palace(), 8);
}
#[test]
fn test_self_palace_jia_day_uses_zhi_fu_orig() {
let qimen = SolarTime::from_ymd_hms(2025, 5, 5, 5, 5, 0).qimen();
assert_eq!(qimen.day().get_heaven_stem().to_string(), "甲");
assert_eq!(qimen.self_palace(), qimen.zhi_fu().original_palace());
}
#[test]
fn test_opponent_palace_is_zhi_fu_palace() {
let qimen = SolarTime::from_ymd_hms(2026, 1, 14, 18, 45, 0).qimen();
assert_eq!(qimen.opponent_palace(), qimen.zhi_fu().palace());
}
#[test]
fn test_stem_palace_center_returns_2() {
let qimen = SolarTime::from_ymd_hms(2025, 5, 5, 5, 5, 0).qimen();
let center_stem = qimen.palace(5).unwrap().earth_heaven_stem().clone();
assert_eq!(qimen.stem_palace(¢er_stem), 2);
}
#[test]
fn test_stem_palace_jia_returns_zhi_fu_orig() {
let qimen = SolarTime::from_ymd_hms(2026, 1, 14, 18, 45, 0).qimen();
let jia = HeavenStem::from_name("甲");
assert_eq!(qimen.stem_palace(&jia), qimen.zhi_fu().original_palace());
}
#[test]
fn test_ten_stars_center_is_none() {
let qimen = SolarTime::from_ymd_hms(2026, 1, 14, 18, 45, 0).qimen();
for n in 1..=9u8 {
let palace = qimen.palace(n).unwrap();
if n == 5 {
assert!(palace.ten_star().is_none(), "center palace must be None");
} else {
assert!(palace.ten_star().is_some(), "palace {n} must have ten star");
}
}
}
#[test]
fn test_ten_star_values_match_tyme4rs() {
let qimen = SolarTime::from_ymd_hms(2026, 1, 14, 18, 45, 0).qimen();
for n in 1..=9u8 {
let palace = qimen.palace(n).unwrap();
if n == 5 {
assert!(palace.ten_star().is_none());
} else {
let star = palace.ten_star().expect("ten star expected");
let name = star.to_string();
assert!(
["比肩", "劫财", "食神", "伤官", "偏财", "正财", "七杀", "正官", "偏印", "正印"]
.contains(&name.as_str()),
"unexpected ten star name: {name}"
);
}
}
}
#[test]
fn test_hexagrams_snapshot_yang_dun_1() {
let qimen = SolarTime::from_ymd_hms(2027, 1, 2, 3, 40, 0).qimen();
let name = |i: usize| qimen.palace((i + 1) as u8).and_then(|p| p.hexagram()).map_or("", |h| h.name());
assert_eq!(name(0), "泽水困"); assert_eq!(name(1), "风地观"); assert_eq!(name(2), "水雷屯"); assert_eq!(name(3), "山风蛊"); assert!(qimen.palace(5).unwrap().hexagram().is_none(), "中宫 5 无卦");
assert_eq!(name(5), "地天泰"); assert_eq!(name(6), "火泽睽"); assert_eq!(name(7), "天山遁"); assert_eq!(name(8), "雷火丰"); }
#[test]
fn test_hexagrams_snapshot_pure_8() {
let qimen = SolarTime::from_ymd_hms(2026, 5, 11, 15, 30, 0).qimen();
let name = |i: usize| qimen.palace((i + 1) as u8).and_then(|p| p.hexagram()).map_or("", |h| h.name());
assert_eq!(name(0), "坎为水");
assert_eq!(name(1), "坤为地");
assert_eq!(name(2), "震为雷");
assert_eq!(name(3), "巽为风");
assert!(qimen.palace(5).unwrap().hexagram().is_none());
assert_eq!(name(5), "乾为天");
assert_eq!(name(6), "兑为泽");
assert_eq!(name(7), "艮为山");
assert_eq!(name(8), "离为火");
}
#[test]
fn test_hexagrams_snapshot_yin_dun_3() {
let qimen = SolarTime::from_ymd_hms(2025, 9, 15, 10, 0, 0).qimen();
let name = |i: usize| qimen.palace((i + 1) as u8).and_then(|p| p.hexagram()).map_or("", |h| h.name());
assert_eq!(name(0), "风水涣"); assert_eq!(name(1), "水地比"); assert_eq!(name(2), "地雷复"); assert_eq!(name(3), "泽风大过"); assert!(qimen.palace(5).unwrap().hexagram().is_none());
assert_eq!(name(5), "雷天大壮"); assert_eq!(name(6), "山泽损"); assert_eq!(name(7), "火山旅"); assert_eq!(name(8), "天火同人"); }
#[test]
fn test_hexagrams_snapshot_yin_dun_8() {
let qimen = SolarTime::from_ymd_hms(2025, 8, 20, 14, 30, 0).qimen();
let name = |i: usize| qimen.palace((i + 1) as u8).and_then(|p| p.hexagram()).map_or("", |h| h.name());
assert_eq!(name(0), "地水师"); assert_eq!(name(1), "雷地豫"); assert_eq!(name(2), "天雷无妄"); assert_eq!(name(3), "水风井"); assert!(qimen.palace(5).unwrap().hexagram().is_none());
assert_eq!(name(5), "火天大有"); assert_eq!(name(6), "风泽中孚"); assert_eq!(name(7), "泽山咸"); assert_eq!(name(8), "山火贲"); }
#[test]
fn test_hexagrams_snapshot_yang_dun_3() {
let qimen = SolarTime::from_ymd_hms(2026, 3, 4, 9, 13, 0).qimen();
let name = |i: usize| qimen.palace((i + 1) as u8).and_then(|p| p.hexagram()).map_or("", |h| h.name());
assert_eq!(name(0), "地水师"); assert_eq!(name(1), "雷地豫"); assert_eq!(name(2), "天雷无妄"); assert_eq!(name(3), "水风井"); assert!(qimen.palace(5).unwrap().hexagram().is_none(), "中宫 5 无卦");
assert_eq!(name(5), "火天大有"); assert_eq!(name(6), "风泽中孚"); assert_eq!(name(7), "泽山咸"); assert_eq!(name(8), "山火贲"); }
#[test]
fn test_shen_sha_includes_yi_ma_and_others() {
let qimen = SolarTime::from_ymd_hms(2026, 1, 14, 18, 45, 0).qimen();
let kinds: std::collections::HashSet<_> = qimen.shen_sha().map(ShenSha::kind).collect();
for k in [
ShenShaKind::YiMa,
ShenShaKind::TaoHua,
ShenShaKind::HuaGai,
ShenShaKind::TianYi,
ShenShaKind::TianDe,
ShenShaKind::YueDe,
ShenShaKind::GuoYin,
ShenShaKind::WenChang,
ShenShaKind::LuShen,
] {
assert!(kinds.contains(&k), "missing {}", k.name());
}
}
}