1use serde::Serialize;
40
41use super::bazi::{bazi, nayin, BaziOptions, LunarDate, LIFE_STAGES};
42use super::calendar::{day_number, ChineseCalendar};
43use super::lunar_table::lunar_date_of;
44use super::{cycle_index, year_cycle_index, BRANCHES, STEMS};
45use crate::error::EngineError;
46use crate::instant::UtcInstant;
47
48const PALACES: [&str; 12] = [
50 "life", "siblings", "spouse", "children", "wealth", "health", "travel", "friends", "career",
51 "property", "fortune", "parents",
52];
53
54const ZI_WEI: &str = "zi_wei";
55const TIAN_JI: &str = "tian_ji";
56const TAI_YANG: &str = "tai_yang";
57const WU_QU: &str = "wu_qu";
58const TIAN_TONG: &str = "tian_tong";
59const LIAN_ZHEN: &str = "lian_zhen";
60const TIAN_FU: &str = "tian_fu";
61const TAI_YIN: &str = "tai_yin";
62const TAN_LANG: &str = "tan_lang";
63const JU_MEN: &str = "ju_men";
64const TIAN_XIANG: &str = "tian_xiang";
65const TIAN_LIANG: &str = "tian_liang";
66const QI_SHA: &str = "qi_sha";
67const PO_JUN: &str = "po_jun";
68const ZUO_FU: &str = "zuo_fu";
69const YOU_BI: &str = "you_bi";
70const WEN_CHANG: &str = "wen_chang";
71const WEN_QU: &str = "wen_qu";
72const TIAN_KUI: &str = "tian_kui";
73const TIAN_YUE: &str = "tian_yue";
74const LU_CUN: &str = "lu_cun";
75const TIAN_MA: &str = "tian_ma";
76const QING_YANG: &str = "qing_yang";
77const TUO_LUO: &str = "tuo_luo";
78const HUO_XING: &str = "huo_xing";
79const LING_XING: &str = "ling_xing";
80const DI_KONG: &str = "di_kong";
81const DI_JIE: &str = "di_jie";
82
83const ZI_WEI_GROUP: [(&str, usize); 6] = [
85 (ZI_WEI, 0),
86 (TIAN_JI, 1),
87 (TAI_YANG, 3),
88 (WU_QU, 4),
89 (TIAN_TONG, 5),
90 (LIAN_ZHEN, 8),
91];
92
93const TIAN_FU_GROUP: [(&str, usize); 8] = [
95 (TIAN_FU, 0),
96 (TAI_YIN, 1),
97 (TAN_LANG, 2),
98 (JU_MEN, 3),
99 (TIAN_XIANG, 4),
100 (TIAN_LIANG, 5),
101 (QI_SHA, 6),
102 (PO_JUN, 10),
103];
104
105const TRANSFORMATIONS: [[&str; 4]; 10] = [
107 [LIAN_ZHEN, PO_JUN, WU_QU, TAI_YANG], [TIAN_JI, TIAN_LIANG, ZI_WEI, TAI_YIN], [TIAN_TONG, TIAN_JI, WEN_CHANG, LIAN_ZHEN], [TAI_YIN, TIAN_TONG, TIAN_JI, JU_MEN], [TAN_LANG, TAI_YIN, YOU_BI, TIAN_JI], [WU_QU, TAN_LANG, TIAN_LIANG, WEN_QU], [TAI_YANG, WU_QU, TAI_YIN, TIAN_TONG], [JU_MEN, TAI_YANG, WEN_QU, WEN_CHANG], [TIAN_LIANG, ZI_WEI, ZUO_FU, WU_QU], [PO_JUN, JU_MEN, TAI_YIN, TAN_LANG], ];
118
119const TRANSFORMATION_KINDS: [&str; 4] = ["lu", "quan", "ke", "ji"];
120
121const MIAO: Option<&str> = Some("miao");
122const WANG: Option<&str> = Some("wang");
123const DE: Option<&str> = Some("de");
124const LI: Option<&str> = Some("li");
125const PING: Option<&str> = Some("ping");
126const BU: Option<&str> = Some("bu");
127const XIAN: Option<&str> = Some("xian");
128
129const BRIGHTNESS: [(&str, [Option<&str>; 12]); 20] = [
131 (
132 ZI_WEI,
133 [
134 WANG, WANG, DE, WANG, MIAO, MIAO, WANG, WANG, DE, WANG, PING, MIAO,
135 ],
136 ),
137 (
138 TIAN_JI,
139 [
140 DE, WANG, LI, PING, MIAO, XIAN, DE, WANG, LI, PING, MIAO, XIAN,
141 ],
142 ),
143 (
144 TAI_YANG,
145 [
146 WANG, MIAO, WANG, WANG, WANG, DE, DE, PING, BU, XIAN, XIAN, BU,
147 ],
148 ),
149 (
150 WU_QU,
151 [
152 DE, LI, MIAO, PING, WANG, MIAO, DE, LI, MIAO, PING, WANG, MIAO,
153 ],
154 ),
155 (
156 TIAN_TONG,
157 [
158 LI, PING, PING, MIAO, XIAN, BU, WANG, PING, PING, MIAO, WANG, BU,
159 ],
160 ),
161 (
162 LIAN_ZHEN,
163 [
164 MIAO, PING, LI, XIAN, PING, LI, MIAO, PING, LI, XIAN, PING, LI,
165 ],
166 ),
167 (
168 TIAN_FU,
169 [
170 MIAO, DE, MIAO, DE, WANG, MIAO, DE, WANG, MIAO, DE, MIAO, MIAO,
171 ],
172 ),
173 (
174 TAI_YIN,
175 [
176 WANG, XIAN, XIAN, XIAN, BU, BU, LI, WANG, WANG, MIAO, MIAO, MIAO,
177 ],
178 ),
179 (
180 TAN_LANG,
181 [
182 PING, LI, MIAO, XIAN, WANG, MIAO, PING, LI, MIAO, XIAN, WANG, MIAO,
183 ],
184 ),
185 (
186 JU_MEN,
187 [
188 MIAO, MIAO, XIAN, WANG, WANG, BU, MIAO, MIAO, XIAN, WANG, WANG, BU,
189 ],
190 ),
191 (
192 TIAN_XIANG,
193 [MIAO, XIAN, DE, DE, MIAO, DE, MIAO, XIAN, DE, DE, MIAO, MIAO],
194 ),
195 (
196 TIAN_LIANG,
197 [
198 MIAO, MIAO, MIAO, XIAN, MIAO, WANG, XIAN, DE, MIAO, XIAN, MIAO, WANG,
199 ],
200 ),
201 (
202 QI_SHA,
203 [
204 MIAO, WANG, MIAO, PING, WANG, MIAO, MIAO, WANG, MIAO, PING, WANG, MIAO,
205 ],
206 ),
207 (
208 PO_JUN,
209 [
210 DE, XIAN, WANG, PING, MIAO, WANG, DE, XIAN, WANG, PING, MIAO, WANG,
211 ],
212 ),
213 (
214 WEN_CHANG,
215 [XIAN, LI, DE, MIAO, XIAN, LI, DE, MIAO, XIAN, LI, DE, MIAO],
216 ),
217 (
218 WEN_QU,
219 [
220 PING, WANG, DE, MIAO, XIAN, WANG, DE, MIAO, XIAN, WANG, DE, MIAO,
221 ],
222 ),
223 (
224 HUO_XING,
225 [MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE],
226 ),
227 (
228 LING_XING,
229 [MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE],
230 ),
231 (
232 QING_YANG,
233 [
234 None, XIAN, MIAO, None, XIAN, MIAO, None, XIAN, MIAO, None, XIAN, MIAO,
235 ],
236 ),
237 (
238 TUO_LUO,
239 [
240 XIAN, None, MIAO, XIAN, None, MIAO, XIAN, None, MIAO, XIAN, None, MIAO,
241 ],
242 ),
243];
244
245const LIFE_MASTERS: [&str; 12] = [
247 TAN_LANG, JU_MEN, LU_CUN, WEN_QU, LIAN_ZHEN, WU_QU, PO_JUN, WU_QU, LIAN_ZHEN, WEN_QU, LU_CUN,
248 JU_MEN,
249];
250
251const BODY_MASTERS: [&str; 12] = [
253 HUO_XING, TIAN_XIANG, TIAN_LIANG, TIAN_TONG, WEN_CHANG, TIAN_JI, HUO_XING, TIAN_XIANG,
254 TIAN_LIANG, TIAN_TONG, WEN_CHANG, TIAN_JI,
255];
256
257const BOSHI: [&str; 12] = [
259 "bo_shi",
260 "li_shi",
261 "qing_long",
262 "xiao_hao",
263 "jiang_jun",
264 "zou_shu",
265 "fei_lian",
266 "xi_shen",
267 "bing_fu",
268 "da_hao",
269 "fu_bing",
270 "guan_fu",
271];
272
273const SUIQIAN: [&str; 12] = [
275 "sui_jian", "hui_qi", "sang_men", "guan_suo", "guan_fu", "xiao_hao", "da_hao", "long_de",
276 "bai_hu", "tian_de", "diao_ke", "bing_fu",
277];
278
279const JIANGQIAN: [&str; 12] = [
281 "jiang_xing",
282 "pan_an",
283 "sui_yi",
284 "xi_shen",
285 "hua_gai",
286 "jie_sha",
287 "zai_sha",
288 "tian_sha",
289 "zhi_bei",
290 "xian_chi",
291 "yue_sha",
292 "wang_shen",
293];
294
295const ZI: usize = 0;
297const CHOU: usize = 1;
298const YIN: usize = 2;
299const MAO: usize = 3;
300const CHEN: usize = 4;
301const SI: usize = 5;
302const WU: usize = 6;
303const WEI: usize = 7;
304const SHEN: usize = 8;
305const YOU: usize = 9;
306const XU: usize = 10;
307const HAI: usize = 11;
308
309#[derive(Debug, Clone)]
311pub struct ZiWeiOptions<'a> {
312 pub solar_time: bool,
315 pub zi_hour: &'a str,
318 pub sex: Option<&'a str>,
321 pub leap_month: &'a str,
324 pub year: Option<i32>,
326 pub date: Option<&'a str>,
329}
330
331impl Default for ZiWeiOptions<'_> {
332 fn default() -> Self {
333 ZiWeiOptions {
334 solar_time: true,
335 zi_hour: "next_day",
336 sex: None,
337 leap_month: "split",
338 year: None,
339 date: None,
340 }
341 }
342}
343
344#[derive(Debug, Clone, PartialEq, Serialize)]
346pub struct ZiWeiStar {
347 pub star: &'static str,
349 pub kind: &'static str,
351 pub brightness: Option<&'static str>,
354 pub transformation: Option<&'static str>,
356}
357
358#[derive(Debug, Clone, PartialEq, Serialize)]
360pub struct AgeRange {
361 pub start: u8,
363 pub end: u8,
365}
366
367#[derive(Debug, Clone, PartialEq, Serialize)]
369pub struct ZiWeiPalace {
370 pub name: &'static str,
372 pub branch: &'static str,
374 pub stem: &'static str,
376 pub is_body: bool,
378 pub stars: Vec<ZiWeiStar>,
380 pub decade: Option<AgeRange>,
382 pub small_limit_ages: Vec<u8>,
384 pub minor_stars: Vec<&'static str>,
386 pub gods: ZiWeiGods,
388 pub flying: Vec<ZiWeiTransformation>,
391}
392
393#[derive(Debug, Clone, PartialEq, Serialize)]
395pub struct ZiWeiGods {
396 pub life_stage: Option<&'static str>,
399 pub boshi: Option<&'static str>,
401 pub suiqian: &'static str,
403 pub jiangqian: &'static str,
405}
406
407#[derive(Debug, Clone, PartialEq, Serialize)]
409pub struct ZiWeiFlowStar {
410 pub star: &'static str,
413 pub branch: &'static str,
415}
416
417#[derive(Debug, Clone, PartialEq, Serialize)]
419pub struct ZiWeiGod {
420 pub god: &'static str,
422 pub branch: &'static str,
424}
425
426#[derive(Debug, Clone, PartialEq, Serialize)]
428pub struct ZiWeiPeriod {
429 pub branch: &'static str,
431 pub stem: &'static str,
433 pub palaces: Vec<ZiWeiGod>,
435 pub transformations: Vec<ZiWeiTransformation>,
437 pub stars: Vec<ZiWeiFlowStar>,
439}
440
441#[derive(Debug, Clone, PartialEq, Serialize)]
443pub struct ZiWeiHoroscope {
444 pub year: i32,
446 pub nominal_age: i32,
448 pub decade: Option<ZiWeiPeriod>,
451 pub childhood: bool,
453 pub small_limit: Option<&'static str>,
455 pub yearly: ZiWeiPeriod,
457 pub suiqian: Vec<ZiWeiGod>,
459 pub jiangqian: Vec<ZiWeiGod>,
461 pub date: Option<String>,
463 pub lunar_date: Option<LunarDate>,
465 pub monthly: Option<ZiWeiPeriod>,
468 pub daily: Option<ZiWeiPeriod>,
470}
471
472#[derive(Debug, Clone, PartialEq, Serialize)]
474pub struct ZiWeiTransformation {
475 pub kind: &'static str,
477 pub star: &'static str,
479 pub palace: &'static str,
481}
482
483#[derive(Debug, Clone, PartialEq, Serialize)]
485pub struct Bureau {
486 pub element: &'static str,
488 pub number: u8,
490}
491
492#[derive(Debug, Clone, PartialEq, Serialize)]
494pub struct ZiWei {
495 pub time_basis: &'static str,
497 pub zi_hour: &'static str,
499 pub lunar_date: LunarDate,
501 pub month: u8,
503 pub hour_branch: &'static str,
505 pub life_palace: &'static str,
507 pub body_palace: &'static str,
509 pub bureau: Bureau,
511 pub life_master: &'static str,
513 pub body_master: &'static str,
515 pub palaces: Vec<ZiWeiPalace>,
517 pub transformations: Vec<ZiWeiTransformation>,
519 pub decade_direction: Option<&'static str>,
522 pub horoscope: Option<ZiWeiHoroscope>,
524}
525
526fn invalid(message: &str) -> EngineError {
527 EngineError::InvalidInput(message.to_string())
528}
529
530fn wrap(index: i64) -> usize {
531 index.rem_euclid(12) as usize
532}
533
534pub(crate) fn zi_wei_branch(bureau: u8, day: u8) -> usize {
536 let (bureau, day) = (i64::from(bureau), i64::from(day));
537 let quotient = (day + bureau - 1) / bureau;
538 let added = quotient * bureau - day;
539 let base = YIN as i64 + quotient - 1;
540 wrap(if added % 2 == 1 {
541 base - added
542 } else {
543 base + added
544 })
545}
546
547fn bureau_of(stem: usize, branch: usize) -> Bureau {
549 let nayin = nayin(cycle_index(stem, branch) / 2);
550 let (element, number) = [
551 ("water", 2),
552 ("wood", 3),
553 ("metal", 4),
554 ("earth", 5),
555 ("fire", 6),
556 ]
557 .into_iter()
558 .find(|(element, _)| nayin.ends_with(element))
559 .unwrap_or(("fire", 6));
560 Bureau { element, number }
561}
562
563fn brightness(star: &str, branch: usize) -> Option<&'static str> {
564 BRIGHTNESS
565 .iter()
566 .find(|(s, _)| *s == star)
567 .and_then(|(_, table)| table[wrap(branch as i64 - YIN as i64)])
568}
569
570fn kui_yue(stem: usize) -> (usize, usize) {
572 match stem {
573 0 | 4 | 6 => (CHOU, WEI),
574 1 | 5 => (ZI, SHEN),
575 2 | 3 => (HAI, YOU),
576 7 => (WU, YIN),
577 _ => (MAO, SI),
578 }
579}
580
581fn lu_cun_of(stem: usize) -> usize {
583 [YIN, MAO, SI, WU, SI, WU, SHEN, YOU, HAI, ZI][stem]
584}
585
586fn trine_of(branch: usize) -> usize {
588 branch % 4
589}
590
591fn horse_of(branch: usize) -> usize {
593 [YIN, HAI, SHEN, SI][trine_of(branch)]
594}
595
596fn luan_xi(branch: usize) -> (usize, usize) {
598 let luan = wrap(MAO as i64 - branch as i64);
599 (luan, wrap(luan as i64 + 6))
600}
601
602fn flow_chang_qu(stem: usize) -> (usize, usize) {
604 match stem {
605 0 => (SI, YOU),
606 1 => (WU, SHEN),
607 2 | 4 => (SHEN, WU),
608 3 | 5 => (YOU, SI),
609 6 => (HAI, MAO),
610 7 => (ZI, YIN),
611 8 => (YIN, ZI),
612 _ => (MAO, HAI),
613 }
614}
615
616fn flow_stars(stem: usize, branch: usize) -> Vec<ZiWeiFlowStar> {
618 let (kui, yue) = kui_yue(stem);
619 let (chang, qu) = flow_chang_qu(stem);
620 let lu = lu_cun_of(stem) as i64;
621 let (luan, xi) = luan_xi(branch);
622 [
623 (TIAN_KUI, kui),
624 (TIAN_YUE, yue),
625 (WEN_CHANG, chang),
626 (WEN_QU, qu),
627 (LU_CUN, wrap(lu)),
628 (QING_YANG, wrap(lu + 1)),
629 (TUO_LUO, wrap(lu - 1)),
630 (TIAN_MA, horse_of(branch)),
631 ("hong_luan", luan),
632 ("tian_xi", xi),
633 ]
634 .into_iter()
635 .map(|(star, b)| ZiWeiFlowStar {
636 star,
637 branch: BRANCHES[b],
638 })
639 .collect()
640}
641
642fn cycle(gods: &[&'static str; 12], start: usize, forward: bool) -> Vec<ZiWeiGod> {
644 (0..12)
645 .map(|i| ZiWeiGod {
646 god: gods[i],
647 branch: BRANCHES[wrap(start as i64 + if forward { i as i64 } else { -(i as i64) })],
648 })
649 .collect()
650}
651
652fn yearly_gods(branch: usize) -> (Vec<ZiWeiGod>, Vec<ZiWeiGod>) {
654 let general = [ZI, YOU, WU, MAO][trine_of(branch)];
655 (
656 cycle(&SUIQIAN, branch, true),
657 cycle(&JIANGQIAN, general, true),
658 )
659}
660
661#[allow(clippy::too_many_arguments)]
663fn minor_stars(
664 year_stem: usize,
665 year_branch: usize,
666 month: i64,
667 day: i64,
668 hour: i64,
669 life: usize,
670 body: usize,
671 anchors: [usize; 4],
672) -> Vec<(usize, &'static str)> {
673 let (s, b, m) = (year_stem, year_branch as i64, month as usize);
674 let [zuo, you, chang, qu] = anchors.map(|a| a as i64);
675 let d = day - 1;
676 let (luan, xi) = luan_xi(year_branch);
677 let (hua_gai, xian_chi) =
678 [(CHEN, YOU), (CHOU, WU), (XU, MAO), (WEI, ZI)][trine_of(year_branch)];
679 let (gu_chen, gua_su) =
680 [(SI, CHOU), (SHEN, CHEN), (HAI, WEI), (YIN, XU)][(year_branch + 10) % 12 / 3];
681 let mut xun_kong = wrap(b + 10 - s as i64);
682 if xun_kong % 2 != year_branch % 2 {
683 xun_kong = wrap(xun_kong as i64 + 1);
684 }
685 let palace_at = |offset: i64| wrap(life as i64 - offset);
687 vec![
688 (luan, "hong_luan"),
689 (xi, "tian_xi"),
690 (wrap(CHOU as i64 + month), "tian_yao"),
691 (xian_chi, "xian_chi"),
692 ([SHEN, XU, ZI, YIN, CHEN, WU][m / 2], "jie_shen"),
693 (wrap(zuo + d), "san_tai"),
694 (wrap(you - d), "ba_zuo"),
695 (wrap(chang + d - 1), "en_guang"),
696 (wrap(qu + d - 1), "tian_gui"),
697 (wrap(CHEN as i64 + b), "long_chi"),
698 (wrap(XU as i64 - b), "feng_ge"),
699 (wrap(life as i64 + b), "tian_cai"),
700 (wrap(body as i64 + b), "tian_shou"),
701 (wrap(WU as i64 + hour), "tai_fu"),
702 (wrap(YIN as i64 + hour), "feng_gao"),
703 ([SI, SHEN, YIN, HAI][m % 4], "tian_wu"),
704 (hua_gai, "hua_gai"),
705 (
706 [WEI, CHEN, SI, YIN, MAO, YOU, HAI, YOU, XU, WU][s],
707 "tian_guan",
708 ),
709 (
710 [YOU, SHEN, ZI, HAI, MAO, YIN, WU, SI, WU, SI][s],
711 "tian_fu_blessing",
712 ),
713 ([SI, WU, ZI, SI, WU, SHEN, YIN, WU, YOU, HAI][s], "tian_chu"),
714 (
715 [XU, SI, CHEN, YIN, WEI, MAO, HAI, WEI, YIN, WU, XU, YIN][m],
716 "tian_yue_moon",
717 ),
718 (wrap(YOU as i64 + b), "tian_de"),
719 (wrap(SI as i64 + b), "yue_de"),
720 (wrap(b + 1), "tian_kong"),
721 (xun_kong, "xun_kong"),
722 ([SHEN, WU, CHEN, YIN, ZI][s % 5], "jie_lu"),
723 ([YOU, WEI, SI, MAO, CHOU][s % 5], "kong_wang"),
724 (gu_chen, "gu_chen"),
725 (gua_su, "gua_su"),
726 (
727 [SHEN, YOU, XU, SI, WU, WEI, YIN, MAO, CHEN, HAI, ZI, CHOU][year_branch],
728 "fei_lian",
729 ),
730 ([SI, CHOU, YOU][year_branch % 3], "po_sui"),
731 (wrap(YOU as i64 + month), "tian_xing"),
732 ([YIN, ZI, XU, SHEN, WU, CHEN][m % 6], "yin_sha"),
733 (wrap(WU as i64 - b), "tian_ku"),
734 (wrap(WU as i64 + b), "tian_xu"),
735 (palace_at(5), "tian_shi"),
736 (palace_at(7), "tian_shang"),
737 (wrap(XU as i64 - b), "nian_jie"),
738 ]
739}
740
741pub fn zi_wei(
744 calendar: &ChineseCalendar,
745 birth: UtcInstant,
746 longitude: f64,
747 utc_offset_minutes: f64,
748 options: &ZiWeiOptions,
749) -> Result<ZiWei, EngineError> {
750 let split_leap = match options.leap_month {
751 "" | "split" => true,
752 "same" => false,
753 _ => return Err(invalid("leap_month must be \"split\" or \"same\"")),
754 };
755 let male = match options.sex {
756 None | Some("") => None,
757 Some("male") => Some(true),
758 Some("female") => Some(false),
759 Some(_) => return Err(invalid("sex must be \"male\" or \"female\"")),
760 };
761 let pillars = bazi(
762 calendar,
763 birth,
764 longitude,
765 utc_offset_minutes,
766 &BaziOptions {
767 solar_time: options.solar_time,
768 zi_hour: options.zi_hour,
769 sex: None,
770 luck_pillars: 0,
771 year: None,
772 date: None,
773 },
774 )?;
775 let lunar = pillars
776 .lunar_date
777 .clone()
778 .ok_or_else(|| invalid("the calendar does not cover the birth date"))?;
779 let hour = BRANCHES
780 .iter()
781 .position(|b| *b == pillars.hour.branch)
782 .unwrap_or(ZI);
783 let month = if lunar.leap && split_leap && lunar.day > 15 {
784 lunar.month % 12 + 1
785 } else {
786 lunar.month
787 };
788 let year = year_cycle_index(lunar.year);
789 let (year_stem, year_branch) = (year % 10, year % 12);
790
791 let month_branch = YIN as i64 + i64::from(month) - 1;
793 let life = wrap(month_branch - hour as i64);
794 let body = wrap(month_branch + hour as i64);
795 let yin_stem = year_stem % 5 * 2 + 2;
796 let stem_of = |branch: usize| (yin_stem + wrap(branch as i64 - YIN as i64)) % 10;
797 let bureau = bureau_of(stem_of(life), life);
798
799 let mut placed: Vec<(usize, &'static str, &'static str)> = Vec::with_capacity(28);
801 let zi_wei_at = zi_wei_branch(bureau.number, lunar.day) as i64;
802 for (star, offset) in ZI_WEI_GROUP {
803 placed.push((wrap(zi_wei_at - offset as i64), star, "major"));
804 }
805 let tian_fu_at = 4 - zi_wei_at;
806 for (star, offset) in TIAN_FU_GROUP {
807 placed.push((wrap(tian_fu_at + offset as i64), star, "major"));
808 }
809 let (m, h) = (i64::from(month) - 1, hour as i64);
810 let (kui, yue) = kui_yue(year_stem);
811 let lu_cun = lu_cun_of(year_stem) as i64;
812 let horse = horse_of(year_branch);
813 let (fire, bell, small_start) = match trine_of(year_branch) {
814 0 => (YIN, XU, XU), 1 => (MAO, XU, WEI), 2 => (CHOU, MAO, CHEN), _ => (YOU, XU, CHOU), };
819 let anchors = [
820 wrap(CHEN as i64 + m),
821 wrap(XU as i64 - m),
822 wrap(XU as i64 - h),
823 wrap(CHEN as i64 + h),
824 ];
825 let [zuo, you, chang, qu] = anchors;
826 for (branch, star, kind) in [
827 (zuo, ZUO_FU, "auxiliary"),
828 (you, YOU_BI, "auxiliary"),
829 (chang, WEN_CHANG, "auxiliary"),
830 (qu, WEN_QU, "auxiliary"),
831 (kui, TIAN_KUI, "auxiliary"),
832 (yue, TIAN_YUE, "auxiliary"),
833 (wrap(lu_cun), LU_CUN, "auxiliary"),
834 (horse, TIAN_MA, "auxiliary"),
835 (wrap(lu_cun + 1), QING_YANG, "malefic"),
836 (wrap(lu_cun - 1), TUO_LUO, "malefic"),
837 (wrap(fire as i64 + h), HUO_XING, "malefic"),
838 (wrap(bell as i64 + h), LING_XING, "malefic"),
839 (wrap(HAI as i64 - h), DI_KONG, "malefic"),
840 (wrap(HAI as i64 + h), DI_JIE, "malefic"),
841 ] {
842 placed.push((branch, star, kind));
843 }
844
845 let transformed = TRANSFORMATIONS[year_stem];
846 let transformation_of = |star: &str| {
847 transformed
848 .iter()
849 .position(|s| *s == star)
850 .map(|k| TRANSFORMATION_KINDS[k])
851 };
852
853 let minor = minor_stars(
854 year_stem,
855 year_branch,
856 m,
857 i64::from(lunar.day),
858 h,
859 life,
860 body,
861 anchors,
862 );
863
864 let forward = male.map(|male| male == (year_stem % 2 == 0));
866 let palace_name = |branch: usize| PALACES[wrap(life as i64 - branch as i64)];
867 let star_branch = |star: &str| placed.iter().find(|(_, s, _)| *s == star).map(|p| p.0);
868 let transformations_of = |stem: usize| -> Vec<ZiWeiTransformation> {
869 TRANSFORMATIONS[stem]
870 .iter()
871 .zip(TRANSFORMATION_KINDS)
872 .filter_map(|(&star, kind)| {
873 Some(ZiWeiTransformation {
874 kind,
875 star,
876 palace: palace_name(star_branch(star)?),
877 })
878 })
879 .collect()
880 };
881 let life_stages = forward.map(|forward| {
882 let start = match bureau.number {
883 2 | 5 => SHEN,
884 3 => HAI,
885 4 => SI,
886 _ => YIN,
887 };
888 cycle(&LIFE_STAGES, start, forward)
889 });
890 let boshi = forward.map(|forward| cycle(&BOSHI, lu_cun as usize, forward));
891 let (suiqian, jiangqian) = yearly_gods(year_branch);
892 let god_at = |gods: &[ZiWeiGod], branch: usize| {
893 gods.iter()
894 .find(|g| g.branch == BRANCHES[branch])
895 .map(|g| g.god)
896 .unwrap_or_default()
897 };
898
899 let palaces: Vec<ZiWeiPalace> = (0..12)
900 .map(|i| {
901 let branch = wrap(life as i64 - i);
902 let stars = placed
903 .iter()
904 .filter(|(b, ..)| *b == branch)
905 .map(|&(_, star, kind)| ZiWeiStar {
906 star,
907 kind,
908 brightness: brightness(star, branch),
909 transformation: transformation_of(star),
910 })
911 .collect();
912 let decade = forward.map(|forward| {
913 let steps = if forward {
914 wrap(branch as i64 - life as i64)
915 } else {
916 wrap(life as i64 - branch as i64)
917 } as u8;
918 let start = bureau.number + 10 * steps;
919 AgeRange {
920 start,
921 end: start + 9,
922 }
923 });
924 let small_limit_ages = male
925 .map(|male| {
926 let steps = if male {
927 wrap(branch as i64 - small_start as i64)
928 } else {
929 wrap(small_start as i64 - branch as i64)
930 } as u8;
931 (0..10).map(|j| steps + 1 + 12 * j).collect()
932 })
933 .unwrap_or_default();
934 ZiWeiPalace {
935 name: PALACES[i as usize],
936 branch: BRANCHES[branch],
937 stem: STEMS[stem_of(branch)],
938 is_body: branch == body,
939 stars,
940 decade,
941 small_limit_ages,
942 minor_stars: minor
943 .iter()
944 .filter(|(b, _)| *b == branch)
945 .map(|&(_, star)| star)
946 .collect(),
947 gods: ZiWeiGods {
948 life_stage: life_stages.as_ref().map(|g| god_at(g, branch)),
949 boshi: boshi.as_ref().map(|g| god_at(g, branch)),
950 suiqian: god_at(&suiqian, branch),
951 jiangqian: god_at(&jiangqian, branch),
952 },
953 flying: transformations_of(stem_of(branch)),
954 }
955 })
956 .collect();
957
958 let transformations = transformations_of(year_stem);
959
960 let target_date = options
962 .date
963 .map(|text| {
964 let day = UtcInstant::parse(&format!("{text}T00:00:00Z"))
965 .map(day_number)
966 .map_err(|_| invalid("date must be YYYY-MM-DD"))?;
967 let (y, m, leap, d) = lunar_date_of(day)
968 .ok_or_else(|| invalid("date is outside the lunar years 1900-2100"))?;
969 Ok::<_, EngineError>((text.to_string(), day, y, m, leap, d))
970 })
971 .transpose()?;
972 let horoscope = target_date
973 .as_ref()
974 .map(|t| t.2)
975 .or(options.year)
976 .map(|target| {
977 let age = target - lunar.year + 1;
978 let period = |branch: usize, stem: usize, flow_branch: usize| ZiWeiPeriod {
979 branch: BRANCHES[branch],
980 stem: STEMS[stem],
981 palaces: (0..12)
982 .map(|k| ZiWeiGod {
983 god: PALACES[k],
984 branch: BRANCHES[wrap(branch as i64 - k as i64)],
985 })
986 .collect(),
987 transformations: transformations_of(stem),
988 stars: flow_stars(stem, flow_branch),
989 };
990 let decade_branch = palaces.iter().find_map(|p: &ZiWeiPalace| {
991 let d = p.decade.as_ref()?;
992 (i32::from(d.start) <= age && age <= i32::from(d.end)).then_some(p.branch)
993 });
994 let childhood = forward.is_some()
995 && decade_branch.is_none()
996 && (1..i32::from(bureau.number)).contains(&age)
997 && age <= 6;
998 let decade_at = decade_branch
999 .and_then(|b| BRANCHES.iter().position(|x| *x == b))
1000 .or_else(|| {
1001 childhood.then(|| {
1002 let name = ["life", "wealth", "health", "spouse", "fortune", "career"]
1003 [(age - 1) as usize];
1004 wrap(
1005 life as i64
1006 - PALACES.iter().position(|p| *p == name).unwrap_or(0) as i64,
1007 )
1008 })
1009 });
1010 let small_limit = palaces
1011 .iter()
1012 .find(|p: &&ZiWeiPalace| {
1013 u8::try_from(age).is_ok_and(|a| p.small_limit_ages.contains(&a))
1014 })
1015 .map(|p| p.branch);
1016 let cycle_index = year_cycle_index(target);
1017 let (y_stem, y_branch) = (cycle_index % 10, cycle_index % 12);
1018 let (suiqian, jiangqian) = yearly_gods(y_branch);
1019 let flows = target_date.as_ref().map(|(_, day, _, m, leap, d)| {
1021 let m = if *leap && *d > 15 { m % 12 + 1 } else { *m };
1022 let month_palace =
1023 wrap(y_branch as i64 - i64::from(month) + hour as i64 + i64::from(m));
1024 let month_branch = wrap(YIN as i64 + i64::from(m) - 1);
1025 let month_stem = (y_stem % 5 * 2 + 2 + usize::from(m) - 1) % 10;
1026 let day_cycle = (day - 11).rem_euclid(60) as usize;
1027 let day_palace = wrap(month_palace as i64 + i64::from(*d) - 1);
1028 (
1029 period(month_palace, month_stem, month_branch),
1030 period(day_palace, day_cycle % 10, day_cycle % 12),
1031 )
1032 });
1033 let (monthly, daily) = flows.map_or((None, None), |(m, d)| (Some(m), Some(d)));
1034 ZiWeiHoroscope {
1035 year: target,
1036 nominal_age: age,
1037 decade: decade_at.map(|b| period(b, stem_of(b), b)),
1038 childhood: childhood && decade_branch.is_none(),
1039 small_limit,
1040 yearly: period(y_branch, y_stem, y_branch),
1041 suiqian,
1042 jiangqian,
1043 date: target_date.as_ref().map(|t| t.0.clone()),
1044 lunar_date: target_date.as_ref().map(|&(_, _, y, m, leap, d)| {
1045 let cycle = year_cycle_index(y);
1046 LunarDate {
1047 year: y,
1048 month: m,
1049 leap,
1050 day: d,
1051 year_stem: STEMS[cycle % 10],
1052 year_branch: BRANCHES[cycle % 12],
1053 animal: super::ANIMALS[cycle % 12],
1054 }
1055 }),
1056 monthly,
1057 daily,
1058 }
1059 });
1060
1061 Ok(ZiWei {
1062 time_basis: pillars.time_basis,
1063 zi_hour: pillars.zi_hour,
1064 lunar_date: lunar,
1065 month,
1066 hour_branch: BRANCHES[hour],
1067 life_palace: BRANCHES[life],
1068 body_palace: BRANCHES[body],
1069 bureau,
1070 life_master: LIFE_MASTERS[life],
1071 body_master: BODY_MASTERS[year_branch],
1072 palaces,
1073 transformations,
1074 decade_direction: forward.map(|f| if f { "forward" } else { "backward" }),
1075 horoscope,
1076 })
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081 use super::*;
1082
1083 #[test]
1084 fn zi_wei_from_the_bureau_and_the_day() {
1085 assert_eq!(BRANCHES[zi_wei_branch(3, 27)], "xu");
1088 assert_eq!(BRANCHES[zi_wei_branch(6, 13)], "hai");
1089 assert_eq!(BRANCHES[zi_wei_branch(5, 6)], "wei");
1090 assert_eq!(BRANCHES[zi_wei_branch(2, 1)], "chou");
1091 assert_eq!(BRANCHES[zi_wei_branch(2, 2)], "yin");
1092 assert_eq!(BRANCHES[zi_wei_branch(6, 1)], "you");
1093 }
1094
1095 #[test]
1096 fn the_bureau_is_the_nayin_of_the_life_palace() {
1097 assert_eq!(
1099 bureau_of(9, 7),
1100 Bureau {
1101 element: "wood",
1102 number: 3
1103 }
1104 );
1105 assert_eq!(bureau_of(0, 0).number, 4);
1107 assert_eq!(bureau_of(2, 0).number, 2);
1109 }
1110}