Skip to main content

astroceleste_engine/chinese/
ziwei.rs

1//! Zi Wei Dou Shu (紫微斗數), the Purple Star chart of a birth, cast from its
2//! [`ChineseCalendar`]. No kernel is needed.
3//!
4//! The chart follows the classic rules of the *Zi Wei Dou Shu Quanshu*:
5//!
6//! - The year is the lunar year (it changes at the New Year), the month and day are the lunar
7//!   ones, and the hour is the birth hour's branch on the clock the Four Pillars use (true
8//!   solar or civil time, either Zi-hour convention). A birth in the second half of a leap
9//!   month counts in the next month (`leap_month = "split"`, the default) or in the month it
10//!   repeats (`"same"`).
11//! - The life palace (命宮) is counted from 寅 forward to the month and back to the hour, the
12//!   body palace (身宮) forward to the hour. Palace stems follow from the year stem (五虎遁).
13//! - The five-element bureau (五行局) is the NaYin element of the life palace's stem and
14//!   branch. Zi Wei is placed from the bureau and the lunar day, Tian Fu opposite it across
15//!   the 寅-申 axis, and each leads its group of stars.
16//! - The auxiliary and malefic stars follow the month (左輔, 右弼), the hour (文昌, 文曲,
17//!   地空, 地劫), the year stem (天魁, 天鉞, 祿存, 擎羊, 陀羅) and the year branch (天馬,
18//!   and with the hour 火星 and 鈴星).
19//! - The Four Transformations (四化) are those of the year stem; for 庚 the 科 is 太陰 and
20//!   the 忌 天同, for 壬 the 科 is 左輔.
21//! - Brightness (廟旺得利平不陷) follows the common table, as published by the iztro project
22//!   (MIT, see NOTICE).
23//! - The decade limits (大限) run forward from the life palace for a yang year and a man or
24//!   a yin year and a woman, backward otherwise, from the bureau's number of years (nominal
25//!   age, 虛歲). The small limits (小限) start from 辰, 戌, 未 or 丑 by the year branch, forward
26//!   for a man and backward for a woman.
27
28use 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
36/// The twelve palaces, counted backward through the branches from the life palace.
37const 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
71/// The Zi Wei group: each star's offset from Zi Wei, counted backward.
72const 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
81/// The Tian Fu group: each star's offset from Tian Fu, counted forward.
82const 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
93/// The Four Transformations (祿, 權, 科, 忌) of each year stem.
94const TRANSFORMATIONS: [[&str; 4]; 10] = [
95    [LIAN_ZHEN, PO_JUN, WU_QU, TAI_YANG],       // 甲
96    [TIAN_JI, TIAN_LIANG, ZI_WEI, TAI_YIN],     // 乙
97    [TIAN_TONG, TIAN_JI, WEN_CHANG, LIAN_ZHEN], // 丙
98    [TAI_YIN, TIAN_TONG, TIAN_JI, JU_MEN],      // 丁
99    [TAN_LANG, TAI_YIN, YOU_BI, TIAN_JI],       // 戊
100    [WU_QU, TAN_LANG, TIAN_LIANG, WEN_QU],      // 己
101    [TAI_YANG, WU_QU, TAI_YIN, TIAN_TONG],      // 庚
102    [JU_MEN, TAI_YANG, WEN_QU, WEN_CHANG],      // 辛
103    [TIAN_LIANG, ZI_WEI, ZUO_FU, WU_QU],        // 壬
104    [PO_JUN, JU_MEN, TAI_YIN, TAN_LANG],        // 癸
105];
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
117/// Brightness of a star in each branch, from 寅 (as the iztro project publishes it).
118const 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
233/// The life master (命主) by the life palace's branch.
234const 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
239/// The body master (身主) by the year branch.
240const 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
245/// Branch indices.
246const 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/// How to cast the chart.
260#[derive(Debug, Clone)]
261pub struct ZiWeiOptions<'a> {
262    /// Take the birth hour on true solar time at the birthplace (`true`, the default) or on
263    /// civil time, as for the Four Pillars.
264    pub solar_time: bool,
265    /// "next_day" (default): from 23:00 the birth counts on the next lunar day; "split": the
266    /// day changes at midnight.
267    pub zi_hour: &'a str,
268    /// "male" or "female": sets the direction of the decade and small limits, which are left
269    /// out without it.
270    pub sex: Option<&'a str>,
271    /// "split" (default): a birth after the 15th of a leap month counts in the next month;
272    /// "same": every day of a leap month counts in the month it repeats.
273    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/// A star in a palace.
288#[derive(Debug, Clone, PartialEq, Serialize)]
289pub struct ZiWeiStar {
290    /// Star code, e.g. "zi_wei", "wen_chang".
291    pub star: &'static str,
292    /// "major" (the fourteen), "auxiliary" or "malefic".
293    pub kind: &'static str,
294    /// Brightness in this branch: "miao", "wang", "de", "li", "ping", "bu" or "xian"; `None`
295    /// where the table gives none.
296    pub brightness: Option<&'static str>,
297    /// The natal transformation it takes: "lu", "quan", "ke" or "ji".
298    pub transformation: Option<&'static str>,
299}
300
301/// A range of nominal ages (虛歲), inclusive.
302#[derive(Debug, Clone, PartialEq, Serialize)]
303pub struct AgeRange {
304    /// First age.
305    pub start: u8,
306    /// Last age.
307    pub end: u8,
308}
309
310/// One of the twelve palaces.
311#[derive(Debug, Clone, PartialEq, Serialize)]
312pub struct ZiWeiPalace {
313    /// Palace code, from "life" to "parents".
314    pub name: &'static str,
315    /// Earthly Branch of its place on the board.
316    pub branch: &'static str,
317    /// Its Heavenly Stem.
318    pub stem: &'static str,
319    /// Whether the body palace falls here.
320    pub is_body: bool,
321    /// Its stars: the major ones first.
322    pub stars: Vec<ZiWeiStar>,
323    /// The decade limit it rules; `None` without the native's sex.
324    pub decade: Option<AgeRange>,
325    /// The nominal ages whose small limit falls here, up to 120; empty without the sex.
326    pub small_limit_ages: Vec<u8>,
327}
328
329/// A natal transformation.
330#[derive(Debug, Clone, PartialEq, Serialize)]
331pub struct ZiWeiTransformation {
332    /// "lu" (祿), "quan" (權), "ke" (科) or "ji" (忌).
333    pub kind: &'static str,
334    /// The star transformed.
335    pub star: &'static str,
336    /// The palace it is in.
337    pub palace: &'static str,
338}
339
340/// The five-element bureau.
341#[derive(Debug, Clone, PartialEq, Serialize)]
342pub struct Bureau {
343    /// "water", "wood", "metal", "earth" or "fire".
344    pub element: &'static str,
345    /// 2 (water) to 6 (fire): the age the first decade starts at.
346    pub number: u8,
347}
348
349/// The Purple Star chart of a birth.
350#[derive(Debug, Clone, PartialEq, Serialize)]
351pub struct ZiWei {
352    /// "solar" or "civil": the clock the birth hour was taken on.
353    pub time_basis: &'static str,
354    /// "next_day" or "split".
355    pub zi_hour: &'static str,
356    /// The lunar date of birth.
357    pub lunar_date: LunarDate,
358    /// The month the chart is cast with (a leap month may count as the next one).
359    pub month: u8,
360    /// The birth hour's branch.
361    pub hour_branch: &'static str,
362    /// Branch of the life palace (命宮).
363    pub life_palace: &'static str,
364    /// Branch of the body palace (身宮).
365    pub body_palace: &'static str,
366    /// The five-element bureau (五行局).
367    pub bureau: Bureau,
368    /// The life master (命主), a star code.
369    pub life_master: &'static str,
370    /// The body master (身主), a star code.
371    pub body_master: &'static str,
372    /// The twelve palaces, from the life palace in palace order.
373    pub palaces: Vec<ZiWeiPalace>,
374    /// The four natal transformations, 祿 first.
375    pub transformations: Vec<ZiWeiTransformation>,
376    /// "forward" or "backward" (through the branches) for the decade limits; `None`
377    /// without the native's sex.
378    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
389/// The branch Zi Wei sits in for a bureau and a lunar day (1-30).
390pub(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
402/// The bureau of a life palace's stem and branch: its NaYin element.
403fn 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
425/// Cast the Purple Star chart of a birth at `birth` (UTC), at `longitude` (degrees east)
426/// where civil time was `utc_offset_minutes` ahead of UTC, from its `calendar`.
427pub 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    // Life and body palaces, palace stems and the bureau.
474    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    // Stars, by branch.
482    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; // 申子辰, 巳酉丑, 寅午戌, 亥卯未
501    let (horse, fire, bell, small_start) = match trine {
502        0 => (YIN, YIN, XU, XU),      // 申子辰
503        1 => (HAI, MAO, XU, WEI),     // 巳酉丑
504        2 => (SHEN, CHOU, MAO, CHEN), // 寅午戌
505        _ => (SI, YOU, XU, CHOU),     // 亥卯未
506    };
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    // Limits.
535    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        // The examples of the placement rhyme: day 27, wood 3 → 戌; day 13, fire 6 → 亥;
622        // day 6, earth 5 → 未; and the first two days of the water bureau.
623        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        // 癸未 is willow wood: the wood bureau, 3.
634        assert_eq!(
635            bureau_of(9, 7),
636            Bureau {
637                element: "wood",
638                number: 3
639            }
640        );
641        // 甲子 is sea metal: 4.
642        assert_eq!(bureau_of(0, 0).number, 4);
643        // 丙子 is stream water: 2.
644        assert_eq!(bureau_of(2, 0).number, 2);
645    }
646}