1use serde::Serialize;
29
30use super::bazi::{bazi, nayin, BaziOptions, LunarDate};
31use super::calendar::ChineseCalendar;
32use super::{cycle_index, year_cycle_index, BRANCHES, STEMS};
33use crate::error::EngineError;
34use crate::instant::UtcInstant;
35
36const PALACES: [&str; 12] = [
38 "life", "siblings", "spouse", "children", "wealth", "health", "travel", "friends", "career",
39 "property", "fortune", "parents",
40];
41
42const ZI_WEI: &str = "zi_wei";
43const TIAN_JI: &str = "tian_ji";
44const TAI_YANG: &str = "tai_yang";
45const WU_QU: &str = "wu_qu";
46const TIAN_TONG: &str = "tian_tong";
47const LIAN_ZHEN: &str = "lian_zhen";
48const TIAN_FU: &str = "tian_fu";
49const TAI_YIN: &str = "tai_yin";
50const TAN_LANG: &str = "tan_lang";
51const JU_MEN: &str = "ju_men";
52const TIAN_XIANG: &str = "tian_xiang";
53const TIAN_LIANG: &str = "tian_liang";
54const QI_SHA: &str = "qi_sha";
55const PO_JUN: &str = "po_jun";
56const ZUO_FU: &str = "zuo_fu";
57const YOU_BI: &str = "you_bi";
58const WEN_CHANG: &str = "wen_chang";
59const WEN_QU: &str = "wen_qu";
60const TIAN_KUI: &str = "tian_kui";
61const TIAN_YUE: &str = "tian_yue";
62const LU_CUN: &str = "lu_cun";
63const TIAN_MA: &str = "tian_ma";
64const QING_YANG: &str = "qing_yang";
65const TUO_LUO: &str = "tuo_luo";
66const HUO_XING: &str = "huo_xing";
67const LING_XING: &str = "ling_xing";
68const DI_KONG: &str = "di_kong";
69const DI_JIE: &str = "di_jie";
70
71const ZI_WEI_GROUP: [(&str, usize); 6] = [
73 (ZI_WEI, 0),
74 (TIAN_JI, 1),
75 (TAI_YANG, 3),
76 (WU_QU, 4),
77 (TIAN_TONG, 5),
78 (LIAN_ZHEN, 8),
79];
80
81const TIAN_FU_GROUP: [(&str, usize); 8] = [
83 (TIAN_FU, 0),
84 (TAI_YIN, 1),
85 (TAN_LANG, 2),
86 (JU_MEN, 3),
87 (TIAN_XIANG, 4),
88 (TIAN_LIANG, 5),
89 (QI_SHA, 6),
90 (PO_JUN, 10),
91];
92
93const TRANSFORMATIONS: [[&str; 4]; 10] = [
95 [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], ];
106
107const TRANSFORMATION_KINDS: [&str; 4] = ["lu", "quan", "ke", "ji"];
108
109const MIAO: Option<&str> = Some("miao");
110const WANG: Option<&str> = Some("wang");
111const DE: Option<&str> = Some("de");
112const LI: Option<&str> = Some("li");
113const PING: Option<&str> = Some("ping");
114const BU: Option<&str> = Some("bu");
115const XIAN: Option<&str> = Some("xian");
116
117const BRIGHTNESS: [(&str, [Option<&str>; 12]); 20] = [
119 (
120 ZI_WEI,
121 [
122 WANG, WANG, DE, WANG, MIAO, MIAO, WANG, WANG, DE, WANG, PING, MIAO,
123 ],
124 ),
125 (
126 TIAN_JI,
127 [
128 DE, WANG, LI, PING, MIAO, XIAN, DE, WANG, LI, PING, MIAO, XIAN,
129 ],
130 ),
131 (
132 TAI_YANG,
133 [
134 WANG, MIAO, WANG, WANG, WANG, DE, DE, PING, BU, XIAN, XIAN, BU,
135 ],
136 ),
137 (
138 WU_QU,
139 [
140 DE, LI, MIAO, PING, WANG, MIAO, DE, LI, MIAO, PING, WANG, MIAO,
141 ],
142 ),
143 (
144 TIAN_TONG,
145 [
146 LI, PING, PING, MIAO, XIAN, BU, WANG, PING, PING, MIAO, WANG, BU,
147 ],
148 ),
149 (
150 LIAN_ZHEN,
151 [
152 MIAO, PING, LI, XIAN, PING, LI, MIAO, PING, LI, XIAN, PING, LI,
153 ],
154 ),
155 (
156 TIAN_FU,
157 [
158 MIAO, DE, MIAO, DE, WANG, MIAO, DE, WANG, MIAO, DE, MIAO, MIAO,
159 ],
160 ),
161 (
162 TAI_YIN,
163 [
164 WANG, XIAN, XIAN, XIAN, BU, BU, LI, WANG, WANG, MIAO, MIAO, MIAO,
165 ],
166 ),
167 (
168 TAN_LANG,
169 [
170 PING, LI, MIAO, XIAN, WANG, MIAO, PING, LI, MIAO, XIAN, WANG, MIAO,
171 ],
172 ),
173 (
174 JU_MEN,
175 [
176 MIAO, MIAO, XIAN, WANG, WANG, BU, MIAO, MIAO, XIAN, WANG, WANG, BU,
177 ],
178 ),
179 (
180 TIAN_XIANG,
181 [MIAO, XIAN, DE, DE, MIAO, DE, MIAO, XIAN, DE, DE, MIAO, MIAO],
182 ),
183 (
184 TIAN_LIANG,
185 [
186 MIAO, MIAO, MIAO, XIAN, MIAO, WANG, XIAN, DE, MIAO, XIAN, MIAO, WANG,
187 ],
188 ),
189 (
190 QI_SHA,
191 [
192 MIAO, WANG, MIAO, PING, WANG, MIAO, MIAO, WANG, MIAO, PING, WANG, MIAO,
193 ],
194 ),
195 (
196 PO_JUN,
197 [
198 DE, XIAN, WANG, PING, MIAO, WANG, DE, XIAN, WANG, PING, MIAO, WANG,
199 ],
200 ),
201 (
202 WEN_CHANG,
203 [XIAN, LI, DE, MIAO, XIAN, LI, DE, MIAO, XIAN, LI, DE, MIAO],
204 ),
205 (
206 WEN_QU,
207 [
208 PING, WANG, DE, MIAO, XIAN, WANG, DE, MIAO, XIAN, WANG, DE, MIAO,
209 ],
210 ),
211 (
212 HUO_XING,
213 [MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE],
214 ),
215 (
216 LING_XING,
217 [MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE],
218 ),
219 (
220 QING_YANG,
221 [
222 None, XIAN, MIAO, None, XIAN, MIAO, None, XIAN, MIAO, None, XIAN, MIAO,
223 ],
224 ),
225 (
226 TUO_LUO,
227 [
228 XIAN, None, MIAO, XIAN, None, MIAO, XIAN, None, MIAO, XIAN, None, MIAO,
229 ],
230 ),
231];
232
233const LIFE_MASTERS: [&str; 12] = [
235 TAN_LANG, JU_MEN, LU_CUN, WEN_QU, LIAN_ZHEN, WU_QU, PO_JUN, WU_QU, LIAN_ZHEN, WEN_QU, LU_CUN,
236 JU_MEN,
237];
238
239const BODY_MASTERS: [&str; 12] = [
241 HUO_XING, TIAN_XIANG, TIAN_LIANG, TIAN_TONG, WEN_CHANG, TIAN_JI, HUO_XING, TIAN_XIANG,
242 TIAN_LIANG, TIAN_TONG, WEN_CHANG, TIAN_JI,
243];
244
245const ZI: usize = 0;
247const CHOU: usize = 1;
248const YIN: usize = 2;
249const MAO: usize = 3;
250const CHEN: usize = 4;
251const SI: usize = 5;
252const WU: usize = 6;
253const WEI: usize = 7;
254const SHEN: usize = 8;
255const YOU: usize = 9;
256const XU: usize = 10;
257const HAI: usize = 11;
258
259#[derive(Debug, Clone)]
261pub struct ZiWeiOptions<'a> {
262 pub solar_time: bool,
265 pub zi_hour: &'a str,
268 pub sex: Option<&'a str>,
271 pub leap_month: &'a str,
274}
275
276impl Default for ZiWeiOptions<'_> {
277 fn default() -> Self {
278 ZiWeiOptions {
279 solar_time: true,
280 zi_hour: "next_day",
281 sex: None,
282 leap_month: "split",
283 }
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Serialize)]
289pub struct ZiWeiStar {
290 pub star: &'static str,
292 pub kind: &'static str,
294 pub brightness: Option<&'static str>,
297 pub transformation: Option<&'static str>,
299}
300
301#[derive(Debug, Clone, PartialEq, Serialize)]
303pub struct AgeRange {
304 pub start: u8,
306 pub end: u8,
308}
309
310#[derive(Debug, Clone, PartialEq, Serialize)]
312pub struct ZiWeiPalace {
313 pub name: &'static str,
315 pub branch: &'static str,
317 pub stem: &'static str,
319 pub is_body: bool,
321 pub stars: Vec<ZiWeiStar>,
323 pub decade: Option<AgeRange>,
325 pub small_limit_ages: Vec<u8>,
327}
328
329#[derive(Debug, Clone, PartialEq, Serialize)]
331pub struct ZiWeiTransformation {
332 pub kind: &'static str,
334 pub star: &'static str,
336 pub palace: &'static str,
338}
339
340#[derive(Debug, Clone, PartialEq, Serialize)]
342pub struct Bureau {
343 pub element: &'static str,
345 pub number: u8,
347}
348
349#[derive(Debug, Clone, PartialEq, Serialize)]
351pub struct ZiWei {
352 pub time_basis: &'static str,
354 pub zi_hour: &'static str,
356 pub lunar_date: LunarDate,
358 pub month: u8,
360 pub hour_branch: &'static str,
362 pub life_palace: &'static str,
364 pub body_palace: &'static str,
366 pub bureau: Bureau,
368 pub life_master: &'static str,
370 pub body_master: &'static str,
372 pub palaces: Vec<ZiWeiPalace>,
374 pub transformations: Vec<ZiWeiTransformation>,
376 pub decade_direction: Option<&'static str>,
379}
380
381fn invalid(message: &str) -> EngineError {
382 EngineError::InvalidInput(message.to_string())
383}
384
385fn wrap(index: i64) -> usize {
386 index.rem_euclid(12) as usize
387}
388
389pub(crate) fn zi_wei_branch(bureau: u8, day: u8) -> usize {
391 let (bureau, day) = (i64::from(bureau), i64::from(day));
392 let quotient = (day + bureau - 1) / bureau;
393 let added = quotient * bureau - day;
394 let base = YIN as i64 + quotient - 1;
395 wrap(if added % 2 == 1 {
396 base - added
397 } else {
398 base + added
399 })
400}
401
402fn bureau_of(stem: usize, branch: usize) -> Bureau {
404 let nayin = nayin(cycle_index(stem, branch) / 2);
405 let (element, number) = [
406 ("water", 2),
407 ("wood", 3),
408 ("metal", 4),
409 ("earth", 5),
410 ("fire", 6),
411 ]
412 .into_iter()
413 .find(|(element, _)| nayin.ends_with(element))
414 .unwrap_or(("fire", 6));
415 Bureau { element, number }
416}
417
418fn brightness(star: &str, branch: usize) -> Option<&'static str> {
419 BRIGHTNESS
420 .iter()
421 .find(|(s, _)| *s == star)
422 .and_then(|(_, table)| table[wrap(branch as i64 - YIN as i64)])
423}
424
425pub fn zi_wei(
428 calendar: &ChineseCalendar,
429 birth: UtcInstant,
430 longitude: f64,
431 utc_offset_minutes: f64,
432 options: &ZiWeiOptions,
433) -> Result<ZiWei, EngineError> {
434 let split_leap = match options.leap_month {
435 "" | "split" => true,
436 "same" => false,
437 _ => return Err(invalid("leap_month must be \"split\" or \"same\"")),
438 };
439 let male = match options.sex {
440 None | Some("") => None,
441 Some("male") => Some(true),
442 Some("female") => Some(false),
443 Some(_) => return Err(invalid("sex must be \"male\" or \"female\"")),
444 };
445 let pillars = bazi(
446 calendar,
447 birth,
448 longitude,
449 utc_offset_minutes,
450 &BaziOptions {
451 solar_time: options.solar_time,
452 zi_hour: options.zi_hour,
453 sex: None,
454 luck_pillars: 0,
455 },
456 )?;
457 let lunar = pillars
458 .lunar_date
459 .clone()
460 .ok_or_else(|| invalid("the calendar does not cover the birth date"))?;
461 let hour = BRANCHES
462 .iter()
463 .position(|b| *b == pillars.hour.branch)
464 .unwrap_or(ZI);
465 let month = if lunar.leap && split_leap && lunar.day > 15 {
466 lunar.month % 12 + 1
467 } else {
468 lunar.month
469 };
470 let year = year_cycle_index(lunar.year);
471 let (year_stem, year_branch) = (year % 10, year % 12);
472
473 let month_branch = YIN as i64 + i64::from(month) - 1;
475 let life = wrap(month_branch - hour as i64);
476 let body = wrap(month_branch + hour as i64);
477 let yin_stem = year_stem % 5 * 2 + 2;
478 let stem_of = |branch: usize| (yin_stem + wrap(branch as i64 - YIN as i64)) % 10;
479 let bureau = bureau_of(stem_of(life), life);
480
481 let mut placed: Vec<(usize, &'static str, &'static str)> = Vec::with_capacity(28);
483 let zi_wei_at = zi_wei_branch(bureau.number, lunar.day) as i64;
484 for (star, offset) in ZI_WEI_GROUP {
485 placed.push((wrap(zi_wei_at - offset as i64), star, "major"));
486 }
487 let tian_fu_at = 4 - zi_wei_at;
488 for (star, offset) in TIAN_FU_GROUP {
489 placed.push((wrap(tian_fu_at + offset as i64), star, "major"));
490 }
491 let (m, h) = (i64::from(month) - 1, hour as i64);
492 let (kui, yue) = match year_stem {
493 0 | 4 | 6 => (CHOU, WEI),
494 1 | 5 => (ZI, SHEN),
495 2 | 3 => (HAI, YOU),
496 7 => (WU, YIN),
497 _ => (MAO, SI),
498 };
499 let lu_cun = [YIN, MAO, SI, WU, SI, WU, SHEN, YOU, HAI, ZI][year_stem] as i64;
500 let trine = year_branch % 4; let (horse, fire, bell, small_start) = match trine {
502 0 => (YIN, YIN, XU, XU), 1 => (HAI, MAO, XU, WEI), 2 => (SHEN, CHOU, MAO, CHEN), _ => (SI, YOU, XU, CHOU), };
507 for (branch, star, kind) in [
508 (wrap(CHEN as i64 + m), ZUO_FU, "auxiliary"),
509 (wrap(XU as i64 - m), YOU_BI, "auxiliary"),
510 (wrap(XU as i64 - h), WEN_CHANG, "auxiliary"),
511 (wrap(CHEN as i64 + h), WEN_QU, "auxiliary"),
512 (kui, TIAN_KUI, "auxiliary"),
513 (yue, TIAN_YUE, "auxiliary"),
514 (wrap(lu_cun), LU_CUN, "auxiliary"),
515 (horse, TIAN_MA, "auxiliary"),
516 (wrap(lu_cun + 1), QING_YANG, "malefic"),
517 (wrap(lu_cun - 1), TUO_LUO, "malefic"),
518 (wrap(fire as i64 + h), HUO_XING, "malefic"),
519 (wrap(bell as i64 + h), LING_XING, "malefic"),
520 (wrap(HAI as i64 - h), DI_KONG, "malefic"),
521 (wrap(HAI as i64 + h), DI_JIE, "malefic"),
522 ] {
523 placed.push((branch, star, kind));
524 }
525
526 let transformed = TRANSFORMATIONS[year_stem];
527 let transformation_of = |star: &str| {
528 transformed
529 .iter()
530 .position(|s| *s == star)
531 .map(|k| TRANSFORMATION_KINDS[k])
532 };
533
534 let forward = male.map(|male| male == (year_stem % 2 == 0));
536 let palace_name = |branch: usize| PALACES[wrap(life as i64 - branch as i64)];
537
538 let palaces = (0..12)
539 .map(|i| {
540 let branch = wrap(life as i64 - i);
541 let stars = placed
542 .iter()
543 .filter(|(b, ..)| *b == branch)
544 .map(|&(_, star, kind)| ZiWeiStar {
545 star,
546 kind,
547 brightness: brightness(star, branch),
548 transformation: transformation_of(star),
549 })
550 .collect();
551 let decade = forward.map(|forward| {
552 let steps = if forward {
553 wrap(branch as i64 - life as i64)
554 } else {
555 wrap(life as i64 - branch as i64)
556 } as u8;
557 let start = bureau.number + 10 * steps;
558 AgeRange {
559 start,
560 end: start + 9,
561 }
562 });
563 let small_limit_ages = male
564 .map(|male| {
565 let steps = if male {
566 wrap(branch as i64 - small_start as i64)
567 } else {
568 wrap(small_start as i64 - branch as i64)
569 } as u8;
570 (0..10).map(|j| steps + 1 + 12 * j).collect()
571 })
572 .unwrap_or_default();
573 ZiWeiPalace {
574 name: PALACES[i as usize],
575 branch: BRANCHES[branch],
576 stem: STEMS[stem_of(branch)],
577 is_body: branch == body,
578 stars,
579 decade,
580 small_limit_ages,
581 }
582 })
583 .collect();
584
585 let transformations = transformed
586 .iter()
587 .zip(TRANSFORMATION_KINDS)
588 .filter_map(|(&star, kind)| {
589 let &(branch, ..) = placed.iter().find(|(_, s, _)| *s == star)?;
590 Some(ZiWeiTransformation {
591 kind,
592 star,
593 palace: palace_name(branch),
594 })
595 })
596 .collect();
597
598 Ok(ZiWei {
599 time_basis: pillars.time_basis,
600 zi_hour: pillars.zi_hour,
601 lunar_date: lunar,
602 month,
603 hour_branch: BRANCHES[hour],
604 life_palace: BRANCHES[life],
605 body_palace: BRANCHES[body],
606 bureau,
607 life_master: LIFE_MASTERS[life],
608 body_master: BODY_MASTERS[year_branch],
609 palaces,
610 transformations,
611 decade_direction: forward.map(|f| if f { "forward" } else { "backward" }),
612 })
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn zi_wei_from_the_bureau_and_the_day() {
621 assert_eq!(BRANCHES[zi_wei_branch(3, 27)], "xu");
624 assert_eq!(BRANCHES[zi_wei_branch(6, 13)], "hai");
625 assert_eq!(BRANCHES[zi_wei_branch(5, 6)], "wei");
626 assert_eq!(BRANCHES[zi_wei_branch(2, 1)], "chou");
627 assert_eq!(BRANCHES[zi_wei_branch(2, 2)], "yin");
628 assert_eq!(BRANCHES[zi_wei_branch(6, 1)], "you");
629 }
630
631 #[test]
632 fn the_bureau_is_the_nayin_of_the_life_palace() {
633 assert_eq!(
635 bureau_of(9, 7),
636 Bureau {
637 element: "wood",
638 number: 3
639 }
640 );
641 assert_eq!(bureau_of(0, 0).number, 4);
643 assert_eq!(bureau_of(2, 0).number, 2);
645 }
646}