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//! - The 38 minor stars (雜曜) and the four cycles of twelve gods (長生, 博士, 歲前 and 將前)
28//!   follow the common rules, as the iztro project publishes them (its default school, with
29//!   截路 and 空亡 as two stars).
30//! - Each palace's stem flies its own four transformations (飛化) to the palaces of the
31//!   stars it transforms.
32//! - Given a year, the chart carries its horoscope: the decade (or, before the first decade,
33//!   the childhood limit), the small limit and the year (流年), each with its palace, its
34//!   palace names, its four transformations and its moving stars (魁鉞昌曲祿羊陀馬鸞喜), and
35//!   for the year the 歲前 and 將前 gods.
36
37use serde::Serialize;
38
39use super::bazi::{bazi, nayin, BaziOptions, LunarDate, LIFE_STAGES};
40use super::calendar::ChineseCalendar;
41use super::{cycle_index, year_cycle_index, BRANCHES, STEMS};
42use crate::error::EngineError;
43use crate::instant::UtcInstant;
44
45/// The twelve palaces, counted backward through the branches from the life palace.
46const PALACES: [&str; 12] = [
47    "life", "siblings", "spouse", "children", "wealth", "health", "travel", "friends", "career",
48    "property", "fortune", "parents",
49];
50
51const ZI_WEI: &str = "zi_wei";
52const TIAN_JI: &str = "tian_ji";
53const TAI_YANG: &str = "tai_yang";
54const WU_QU: &str = "wu_qu";
55const TIAN_TONG: &str = "tian_tong";
56const LIAN_ZHEN: &str = "lian_zhen";
57const TIAN_FU: &str = "tian_fu";
58const TAI_YIN: &str = "tai_yin";
59const TAN_LANG: &str = "tan_lang";
60const JU_MEN: &str = "ju_men";
61const TIAN_XIANG: &str = "tian_xiang";
62const TIAN_LIANG: &str = "tian_liang";
63const QI_SHA: &str = "qi_sha";
64const PO_JUN: &str = "po_jun";
65const ZUO_FU: &str = "zuo_fu";
66const YOU_BI: &str = "you_bi";
67const WEN_CHANG: &str = "wen_chang";
68const WEN_QU: &str = "wen_qu";
69const TIAN_KUI: &str = "tian_kui";
70const TIAN_YUE: &str = "tian_yue";
71const LU_CUN: &str = "lu_cun";
72const TIAN_MA: &str = "tian_ma";
73const QING_YANG: &str = "qing_yang";
74const TUO_LUO: &str = "tuo_luo";
75const HUO_XING: &str = "huo_xing";
76const LING_XING: &str = "ling_xing";
77const DI_KONG: &str = "di_kong";
78const DI_JIE: &str = "di_jie";
79
80/// The Zi Wei group: each star's offset from Zi Wei, counted backward.
81const ZI_WEI_GROUP: [(&str, usize); 6] = [
82    (ZI_WEI, 0),
83    (TIAN_JI, 1),
84    (TAI_YANG, 3),
85    (WU_QU, 4),
86    (TIAN_TONG, 5),
87    (LIAN_ZHEN, 8),
88];
89
90/// The Tian Fu group: each star's offset from Tian Fu, counted forward.
91const TIAN_FU_GROUP: [(&str, usize); 8] = [
92    (TIAN_FU, 0),
93    (TAI_YIN, 1),
94    (TAN_LANG, 2),
95    (JU_MEN, 3),
96    (TIAN_XIANG, 4),
97    (TIAN_LIANG, 5),
98    (QI_SHA, 6),
99    (PO_JUN, 10),
100];
101
102/// The Four Transformations (祿, 權, 科, 忌) of each year stem.
103const TRANSFORMATIONS: [[&str; 4]; 10] = [
104    [LIAN_ZHEN, PO_JUN, WU_QU, TAI_YANG],       // 甲
105    [TIAN_JI, TIAN_LIANG, ZI_WEI, TAI_YIN],     // 乙
106    [TIAN_TONG, TIAN_JI, WEN_CHANG, LIAN_ZHEN], // 丙
107    [TAI_YIN, TIAN_TONG, TIAN_JI, JU_MEN],      // 丁
108    [TAN_LANG, TAI_YIN, YOU_BI, TIAN_JI],       // 戊
109    [WU_QU, TAN_LANG, TIAN_LIANG, WEN_QU],      // 己
110    [TAI_YANG, WU_QU, TAI_YIN, TIAN_TONG],      // 庚
111    [JU_MEN, TAI_YANG, WEN_QU, WEN_CHANG],      // 辛
112    [TIAN_LIANG, ZI_WEI, ZUO_FU, WU_QU],        // 壬
113    [PO_JUN, JU_MEN, TAI_YIN, TAN_LANG],        // 癸
114];
115
116const TRANSFORMATION_KINDS: [&str; 4] = ["lu", "quan", "ke", "ji"];
117
118const MIAO: Option<&str> = Some("miao");
119const WANG: Option<&str> = Some("wang");
120const DE: Option<&str> = Some("de");
121const LI: Option<&str> = Some("li");
122const PING: Option<&str> = Some("ping");
123const BU: Option<&str> = Some("bu");
124const XIAN: Option<&str> = Some("xian");
125
126/// Brightness of a star in each branch, from 寅 (as the iztro project publishes it).
127const BRIGHTNESS: [(&str, [Option<&str>; 12]); 20] = [
128    (
129        ZI_WEI,
130        [
131            WANG, WANG, DE, WANG, MIAO, MIAO, WANG, WANG, DE, WANG, PING, MIAO,
132        ],
133    ),
134    (
135        TIAN_JI,
136        [
137            DE, WANG, LI, PING, MIAO, XIAN, DE, WANG, LI, PING, MIAO, XIAN,
138        ],
139    ),
140    (
141        TAI_YANG,
142        [
143            WANG, MIAO, WANG, WANG, WANG, DE, DE, PING, BU, XIAN, XIAN, BU,
144        ],
145    ),
146    (
147        WU_QU,
148        [
149            DE, LI, MIAO, PING, WANG, MIAO, DE, LI, MIAO, PING, WANG, MIAO,
150        ],
151    ),
152    (
153        TIAN_TONG,
154        [
155            LI, PING, PING, MIAO, XIAN, BU, WANG, PING, PING, MIAO, WANG, BU,
156        ],
157    ),
158    (
159        LIAN_ZHEN,
160        [
161            MIAO, PING, LI, XIAN, PING, LI, MIAO, PING, LI, XIAN, PING, LI,
162        ],
163    ),
164    (
165        TIAN_FU,
166        [
167            MIAO, DE, MIAO, DE, WANG, MIAO, DE, WANG, MIAO, DE, MIAO, MIAO,
168        ],
169    ),
170    (
171        TAI_YIN,
172        [
173            WANG, XIAN, XIAN, XIAN, BU, BU, LI, WANG, WANG, MIAO, MIAO, MIAO,
174        ],
175    ),
176    (
177        TAN_LANG,
178        [
179            PING, LI, MIAO, XIAN, WANG, MIAO, PING, LI, MIAO, XIAN, WANG, MIAO,
180        ],
181    ),
182    (
183        JU_MEN,
184        [
185            MIAO, MIAO, XIAN, WANG, WANG, BU, MIAO, MIAO, XIAN, WANG, WANG, BU,
186        ],
187    ),
188    (
189        TIAN_XIANG,
190        [MIAO, XIAN, DE, DE, MIAO, DE, MIAO, XIAN, DE, DE, MIAO, MIAO],
191    ),
192    (
193        TIAN_LIANG,
194        [
195            MIAO, MIAO, MIAO, XIAN, MIAO, WANG, XIAN, DE, MIAO, XIAN, MIAO, WANG,
196        ],
197    ),
198    (
199        QI_SHA,
200        [
201            MIAO, WANG, MIAO, PING, WANG, MIAO, MIAO, WANG, MIAO, PING, WANG, MIAO,
202        ],
203    ),
204    (
205        PO_JUN,
206        [
207            DE, XIAN, WANG, PING, MIAO, WANG, DE, XIAN, WANG, PING, MIAO, WANG,
208        ],
209    ),
210    (
211        WEN_CHANG,
212        [XIAN, LI, DE, MIAO, XIAN, LI, DE, MIAO, XIAN, LI, DE, MIAO],
213    ),
214    (
215        WEN_QU,
216        [
217            PING, WANG, DE, MIAO, XIAN, WANG, DE, MIAO, XIAN, WANG, DE, MIAO,
218        ],
219    ),
220    (
221        HUO_XING,
222        [MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE],
223    ),
224    (
225        LING_XING,
226        [MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE, MIAO, LI, XIAN, DE],
227    ),
228    (
229        QING_YANG,
230        [
231            None, XIAN, MIAO, None, XIAN, MIAO, None, XIAN, MIAO, None, XIAN, MIAO,
232        ],
233    ),
234    (
235        TUO_LUO,
236        [
237            XIAN, None, MIAO, XIAN, None, MIAO, XIAN, None, MIAO, XIAN, None, MIAO,
238        ],
239    ),
240];
241
242/// The life master (命主) by the life palace's branch.
243const LIFE_MASTERS: [&str; 12] = [
244    TAN_LANG, JU_MEN, LU_CUN, WEN_QU, LIAN_ZHEN, WU_QU, PO_JUN, WU_QU, LIAN_ZHEN, WEN_QU, LU_CUN,
245    JU_MEN,
246];
247
248/// The body master (身主) by the year branch.
249const BODY_MASTERS: [&str; 12] = [
250    HUO_XING, TIAN_XIANG, TIAN_LIANG, TIAN_TONG, WEN_CHANG, TIAN_JI, HUO_XING, TIAN_XIANG,
251    TIAN_LIANG, TIAN_TONG, WEN_CHANG, TIAN_JI,
252];
253
254/// The 博士 cycle, from 祿存.
255const BOSHI: [&str; 12] = [
256    "bo_shi",
257    "li_shi",
258    "qing_long",
259    "xiao_hao",
260    "jiang_jun",
261    "zou_shu",
262    "fei_lian",
263    "xi_shen",
264    "bing_fu",
265    "da_hao",
266    "fu_bing",
267    "guan_fu",
268];
269
270/// The 歲前 cycle, from the year's branch.
271const SUIQIAN: [&str; 12] = [
272    "sui_jian", "hui_qi", "sang_men", "guan_suo", "guan_fu", "xiao_hao", "da_hao", "long_de",
273    "bai_hu", "tian_de", "diao_ke", "bing_fu",
274];
275
276/// The 將前 cycle, from the general star of the year's trine.
277const JIANGQIAN: [&str; 12] = [
278    "jiang_xing",
279    "pan_an",
280    "sui_yi",
281    "xi_shen",
282    "hua_gai",
283    "jie_sha",
284    "zai_sha",
285    "tian_sha",
286    "zhi_bei",
287    "xian_chi",
288    "yue_sha",
289    "wang_shen",
290];
291
292/// Branch indices.
293const ZI: usize = 0;
294const CHOU: usize = 1;
295const YIN: usize = 2;
296const MAO: usize = 3;
297const CHEN: usize = 4;
298const SI: usize = 5;
299const WU: usize = 6;
300const WEI: usize = 7;
301const SHEN: usize = 8;
302const YOU: usize = 9;
303const XU: usize = 10;
304const HAI: usize = 11;
305
306/// How to cast the chart.
307#[derive(Debug, Clone)]
308pub struct ZiWeiOptions<'a> {
309    /// Take the birth hour on true solar time at the birthplace (`true`, the default) or on
310    /// civil time, as for the Four Pillars.
311    pub solar_time: bool,
312    /// "next_day" (default): from 23:00 the birth counts on the next lunar day; "split": the
313    /// day changes at midnight.
314    pub zi_hour: &'a str,
315    /// "male" or "female": sets the direction of the decade and small limits, which are left
316    /// out without it.
317    pub sex: Option<&'a str>,
318    /// "split" (default): a birth after the 15th of a leap month counts in the next month;
319    /// "same": every day of a leap month counts in the month it repeats.
320    pub leap_month: &'a str,
321    /// A lunar year to cast the horoscope (decade, small limit, year) for.
322    pub year: Option<i32>,
323}
324
325impl Default for ZiWeiOptions<'_> {
326    fn default() -> Self {
327        ZiWeiOptions {
328            solar_time: true,
329            zi_hour: "next_day",
330            sex: None,
331            leap_month: "split",
332            year: None,
333        }
334    }
335}
336
337/// A star in a palace.
338#[derive(Debug, Clone, PartialEq, Serialize)]
339pub struct ZiWeiStar {
340    /// Star code, e.g. "zi_wei", "wen_chang".
341    pub star: &'static str,
342    /// "major" (the fourteen), "auxiliary" or "malefic".
343    pub kind: &'static str,
344    /// Brightness in this branch: "miao", "wang", "de", "li", "ping", "bu" or "xian"; `None`
345    /// where the table gives none.
346    pub brightness: Option<&'static str>,
347    /// The natal transformation it takes: "lu", "quan", "ke" or "ji".
348    pub transformation: Option<&'static str>,
349}
350
351/// A range of nominal ages (虛歲), inclusive.
352#[derive(Debug, Clone, PartialEq, Serialize)]
353pub struct AgeRange {
354    /// First age.
355    pub start: u8,
356    /// Last age.
357    pub end: u8,
358}
359
360/// One of the twelve palaces.
361#[derive(Debug, Clone, PartialEq, Serialize)]
362pub struct ZiWeiPalace {
363    /// Palace code, from "life" to "parents".
364    pub name: &'static str,
365    /// Earthly Branch of its place on the board.
366    pub branch: &'static str,
367    /// Its Heavenly Stem.
368    pub stem: &'static str,
369    /// Whether the body palace falls here.
370    pub is_body: bool,
371    /// Its stars: the major ones first.
372    pub stars: Vec<ZiWeiStar>,
373    /// The decade limit it rules; `None` without the native's sex.
374    pub decade: Option<AgeRange>,
375    /// The nominal ages whose small limit falls here, up to 120; empty without the sex.
376    pub small_limit_ages: Vec<u8>,
377    /// The minor stars (雜曜) here.
378    pub minor_stars: Vec<&'static str>,
379    /// The gods of the four cycles of twelve here.
380    pub gods: ZiWeiGods,
381    /// The four transformations this palace's stem flies (飛化), to the palaces of the
382    /// stars they transform.
383    pub flying: Vec<ZiWeiTransformation>,
384}
385
386/// The gods of the four cycles of twelve in a palace.
387#[derive(Debug, Clone, PartialEq, Serialize)]
388pub struct ZiWeiGods {
389    /// Stage of the 長生 cycle, from the bureau (as Ba Zi's `life_stage` codes); `None`
390    /// without the native's sex.
391    pub life_stage: Option<&'static str>,
392    /// God of the 博士 cycle, from 祿存; `None` without the native's sex.
393    pub boshi: Option<&'static str>,
394    /// God of the 歲前 cycle of the birth year.
395    pub suiqian: &'static str,
396    /// God of the 將前 cycle of the birth year.
397    pub jiangqian: &'static str,
398}
399
400/// A moving star of a horoscope period.
401#[derive(Debug, Clone, PartialEq, Serialize)]
402pub struct ZiWeiFlowStar {
403    /// Star code: "tian_kui", "tian_yue", "wen_chang", "wen_qu", "lu_cun", "qing_yang",
404    /// "tuo_luo", "tian_ma", "hong_luan" or "tian_xi".
405    pub star: &'static str,
406    /// Branch of the palace it moves to.
407    pub branch: &'static str,
408}
409
410/// A god of a yearly cycle of twelve.
411#[derive(Debug, Clone, PartialEq, Serialize)]
412pub struct ZiWeiGod {
413    /// God code.
414    pub god: &'static str,
415    /// Branch of its palace.
416    pub branch: &'static str,
417}
418
419/// One period of a horoscope: a decade, the childhood limit or a year.
420#[derive(Debug, Clone, PartialEq, Serialize)]
421pub struct ZiWeiPeriod {
422    /// Branch of the palace the period's life palace falls in.
423    pub branch: &'static str,
424    /// The period's stem (the decade palace's, or the year's).
425    pub stem: &'static str,
426    /// The palace names of the period, by branch, life first.
427    pub palaces: Vec<ZiWeiGod>,
428    /// The four transformations of the period's stem, with the natal palace of each star.
429    pub transformations: Vec<ZiWeiTransformation>,
430    /// The period's moving stars.
431    pub stars: Vec<ZiWeiFlowStar>,
432}
433
434/// The horoscope of a year.
435#[derive(Debug, Clone, PartialEq, Serialize)]
436pub struct ZiWeiHoroscope {
437    /// The lunar year.
438    pub year: i32,
439    /// Nominal age (虛歲) in that year.
440    pub nominal_age: i32,
441    /// The decade limit running in that year; `None` without the native's sex or outside
442    /// the decades.
443    pub decade: Option<ZiWeiPeriod>,
444    /// Whether `decade` is the childhood limit (童限), before the first decade starts.
445    pub childhood: bool,
446    /// Branch of the small limit's palace; `None` without the native's sex.
447    pub small_limit: Option<&'static str>,
448    /// The year (流年): its life palace is the year's branch.
449    pub yearly: ZiWeiPeriod,
450    /// The 歲前 gods of the year.
451    pub suiqian: Vec<ZiWeiGod>,
452    /// The 將前 gods of the year.
453    pub jiangqian: Vec<ZiWeiGod>,
454}
455
456/// A natal transformation.
457#[derive(Debug, Clone, PartialEq, Serialize)]
458pub struct ZiWeiTransformation {
459    /// "lu" (祿), "quan" (權), "ke" (科) or "ji" (忌).
460    pub kind: &'static str,
461    /// The star transformed.
462    pub star: &'static str,
463    /// The palace it is in.
464    pub palace: &'static str,
465}
466
467/// The five-element bureau.
468#[derive(Debug, Clone, PartialEq, Serialize)]
469pub struct Bureau {
470    /// "water", "wood", "metal", "earth" or "fire".
471    pub element: &'static str,
472    /// 2 (water) to 6 (fire): the age the first decade starts at.
473    pub number: u8,
474}
475
476/// The Purple Star chart of a birth.
477#[derive(Debug, Clone, PartialEq, Serialize)]
478pub struct ZiWei {
479    /// "solar" or "civil": the clock the birth hour was taken on.
480    pub time_basis: &'static str,
481    /// "next_day" or "split".
482    pub zi_hour: &'static str,
483    /// The lunar date of birth.
484    pub lunar_date: LunarDate,
485    /// The month the chart is cast with (a leap month may count as the next one).
486    pub month: u8,
487    /// The birth hour's branch.
488    pub hour_branch: &'static str,
489    /// Branch of the life palace (命宮).
490    pub life_palace: &'static str,
491    /// Branch of the body palace (身宮).
492    pub body_palace: &'static str,
493    /// The five-element bureau (五行局).
494    pub bureau: Bureau,
495    /// The life master (命主), a star code.
496    pub life_master: &'static str,
497    /// The body master (身主), a star code.
498    pub body_master: &'static str,
499    /// The twelve palaces, from the life palace in palace order.
500    pub palaces: Vec<ZiWeiPalace>,
501    /// The four natal transformations, 祿 first.
502    pub transformations: Vec<ZiWeiTransformation>,
503    /// "forward" or "backward" (through the branches) for the decade limits; `None`
504    /// without the native's sex.
505    pub decade_direction: Option<&'static str>,
506    /// The horoscope of the requested year; `None` when none was asked for.
507    pub horoscope: Option<ZiWeiHoroscope>,
508}
509
510fn invalid(message: &str) -> EngineError {
511    EngineError::InvalidInput(message.to_string())
512}
513
514fn wrap(index: i64) -> usize {
515    index.rem_euclid(12) as usize
516}
517
518/// The branch Zi Wei sits in for a bureau and a lunar day (1-30).
519pub(crate) fn zi_wei_branch(bureau: u8, day: u8) -> usize {
520    let (bureau, day) = (i64::from(bureau), i64::from(day));
521    let quotient = (day + bureau - 1) / bureau;
522    let added = quotient * bureau - day;
523    let base = YIN as i64 + quotient - 1;
524    wrap(if added % 2 == 1 {
525        base - added
526    } else {
527        base + added
528    })
529}
530
531/// The bureau of a life palace's stem and branch: its NaYin element.
532fn bureau_of(stem: usize, branch: usize) -> Bureau {
533    let nayin = nayin(cycle_index(stem, branch) / 2);
534    let (element, number) = [
535        ("water", 2),
536        ("wood", 3),
537        ("metal", 4),
538        ("earth", 5),
539        ("fire", 6),
540    ]
541    .into_iter()
542    .find(|(element, _)| nayin.ends_with(element))
543    .unwrap_or(("fire", 6));
544    Bureau { element, number }
545}
546
547fn brightness(star: &str, branch: usize) -> Option<&'static str> {
548    BRIGHTNESS
549        .iter()
550        .find(|(s, _)| *s == star)
551        .and_then(|(_, table)| table[wrap(branch as i64 - YIN as i64)])
552}
553
554/// 天魁 and 天鉞 of a stem.
555fn kui_yue(stem: usize) -> (usize, usize) {
556    match stem {
557        0 | 4 | 6 => (CHOU, WEI),
558        1 | 5 => (ZI, SHEN),
559        2 | 3 => (HAI, YOU),
560        7 => (WU, YIN),
561        _ => (MAO, SI),
562    }
563}
564
565/// 祿存 of a stem.
566fn lu_cun_of(stem: usize) -> usize {
567    [YIN, MAO, SI, WU, SI, WU, SHEN, YOU, HAI, ZI][stem]
568}
569
570/// The trine of a branch: 0 申子辰, 1 巳酉丑, 2 寅午戌, 3 亥卯未.
571fn trine_of(branch: usize) -> usize {
572    branch % 4
573}
574
575/// 天馬 of a branch.
576fn horse_of(branch: usize) -> usize {
577    [YIN, HAI, SHEN, SI][trine_of(branch)]
578}
579
580/// 紅鸞 and 天喜 of a branch.
581fn luan_xi(branch: usize) -> (usize, usize) {
582    let luan = wrap(MAO as i64 - branch as i64);
583    (luan, wrap(luan as i64 + 6))
584}
585
586/// The moving 文昌 and 文曲 of a stem (流昌, 流曲).
587fn flow_chang_qu(stem: usize) -> (usize, usize) {
588    match stem {
589        0 => (SI, YOU),
590        1 => (WU, SHEN),
591        2 | 4 => (SHEN, WU),
592        3 | 5 => (YOU, SI),
593        6 => (HAI, MAO),
594        7 => (ZI, YIN),
595        8 => (YIN, ZI),
596        _ => (MAO, HAI),
597    }
598}
599
600/// The ten moving stars of a period's stem and branch.
601fn flow_stars(stem: usize, branch: usize) -> Vec<ZiWeiFlowStar> {
602    let (kui, yue) = kui_yue(stem);
603    let (chang, qu) = flow_chang_qu(stem);
604    let lu = lu_cun_of(stem) as i64;
605    let (luan, xi) = luan_xi(branch);
606    [
607        (TIAN_KUI, kui),
608        (TIAN_YUE, yue),
609        (WEN_CHANG, chang),
610        (WEN_QU, qu),
611        (LU_CUN, wrap(lu)),
612        (QING_YANG, wrap(lu + 1)),
613        (TUO_LUO, wrap(lu - 1)),
614        (TIAN_MA, horse_of(branch)),
615        ("hong_luan", luan),
616        ("tian_xi", xi),
617    ]
618    .into_iter()
619    .map(|(star, b)| ZiWeiFlowStar {
620        star,
621        branch: BRANCHES[b],
622    })
623    .collect()
624}
625
626/// A cycle of twelve gods laid from `start`, forward or backward.
627fn cycle(gods: &[&'static str; 12], start: usize, forward: bool) -> Vec<ZiWeiGod> {
628    (0..12)
629        .map(|i| ZiWeiGod {
630            god: gods[i],
631            branch: BRANCHES[wrap(start as i64 + if forward { i as i64 } else { -(i as i64) })],
632        })
633        .collect()
634}
635
636/// The 歲前 and 將前 gods of a year's branch.
637fn yearly_gods(branch: usize) -> (Vec<ZiWeiGod>, Vec<ZiWeiGod>) {
638    let general = [ZI, YOU, WU, MAO][trine_of(branch)];
639    (
640        cycle(&SUIQIAN, branch, true),
641        cycle(&JIANGQIAN, general, true),
642    )
643}
644
645/// The 38 minor stars, by branch.
646#[allow(clippy::too_many_arguments)]
647fn minor_stars(
648    year_stem: usize,
649    year_branch: usize,
650    month: i64,
651    day: i64,
652    hour: i64,
653    life: usize,
654    body: usize,
655    anchors: [usize; 4],
656) -> Vec<(usize, &'static str)> {
657    let (s, b, m) = (year_stem, year_branch as i64, month as usize);
658    let [zuo, you, chang, qu] = anchors.map(|a| a as i64);
659    let d = day - 1;
660    let (luan, xi) = luan_xi(year_branch);
661    let (hua_gai, xian_chi) =
662        [(CHEN, YOU), (CHOU, WU), (XU, MAO), (WEI, ZI)][trine_of(year_branch)];
663    let (gu_chen, gua_su) =
664        [(SI, CHOU), (SHEN, CHEN), (HAI, WEI), (YIN, XU)][(year_branch + 10) % 12 / 3];
665    let mut xun_kong = wrap(b + 10 - s as i64);
666    if xun_kong % 2 != year_branch % 2 {
667        xun_kong = wrap(xun_kong as i64 + 1);
668    }
669    // 天傷 sits in the friends palace, 天使 in the health palace.
670    let palace_at = |offset: i64| wrap(life as i64 - offset);
671    vec![
672        (luan, "hong_luan"),
673        (xi, "tian_xi"),
674        (wrap(CHOU as i64 + month), "tian_yao"),
675        (xian_chi, "xian_chi"),
676        ([SHEN, XU, ZI, YIN, CHEN, WU][m / 2], "jie_shen"),
677        (wrap(zuo + d), "san_tai"),
678        (wrap(you - d), "ba_zuo"),
679        (wrap(chang + d - 1), "en_guang"),
680        (wrap(qu + d - 1), "tian_gui"),
681        (wrap(CHEN as i64 + b), "long_chi"),
682        (wrap(XU as i64 - b), "feng_ge"),
683        (wrap(life as i64 + b), "tian_cai"),
684        (wrap(body as i64 + b), "tian_shou"),
685        (wrap(WU as i64 + hour), "tai_fu"),
686        (wrap(YIN as i64 + hour), "feng_gao"),
687        ([SI, SHEN, YIN, HAI][m % 4], "tian_wu"),
688        (hua_gai, "hua_gai"),
689        (
690            [WEI, CHEN, SI, YIN, MAO, YOU, HAI, YOU, XU, WU][s],
691            "tian_guan",
692        ),
693        (
694            [YOU, SHEN, ZI, HAI, MAO, YIN, WU, SI, WU, SI][s],
695            "tian_fu_blessing",
696        ),
697        ([SI, WU, ZI, SI, WU, SHEN, YIN, WU, YOU, HAI][s], "tian_chu"),
698        (
699            [XU, SI, CHEN, YIN, WEI, MAO, HAI, WEI, YIN, WU, XU, YIN][m],
700            "tian_yue_moon",
701        ),
702        (wrap(YOU as i64 + b), "tian_de"),
703        (wrap(SI as i64 + b), "yue_de"),
704        (wrap(b + 1), "tian_kong"),
705        (xun_kong, "xun_kong"),
706        ([SHEN, WU, CHEN, YIN, ZI][s % 5], "jie_lu"),
707        ([YOU, WEI, SI, MAO, CHOU][s % 5], "kong_wang"),
708        (gu_chen, "gu_chen"),
709        (gua_su, "gua_su"),
710        (
711            [SHEN, YOU, XU, SI, WU, WEI, YIN, MAO, CHEN, HAI, ZI, CHOU][year_branch],
712            "fei_lian",
713        ),
714        ([SI, CHOU, YOU][year_branch % 3], "po_sui"),
715        (wrap(YOU as i64 + month), "tian_xing"),
716        ([YIN, ZI, XU, SHEN, WU, CHEN][m % 6], "yin_sha"),
717        (wrap(WU as i64 - b), "tian_ku"),
718        (wrap(WU as i64 + b), "tian_xu"),
719        (palace_at(5), "tian_shi"),
720        (palace_at(7), "tian_shang"),
721        (wrap(XU as i64 - b), "nian_jie"),
722    ]
723}
724
725/// Cast the Purple Star chart of a birth at `birth` (UTC), at `longitude` (degrees east)
726/// where civil time was `utc_offset_minutes` ahead of UTC, from its `calendar`.
727pub fn zi_wei(
728    calendar: &ChineseCalendar,
729    birth: UtcInstant,
730    longitude: f64,
731    utc_offset_minutes: f64,
732    options: &ZiWeiOptions,
733) -> Result<ZiWei, EngineError> {
734    let split_leap = match options.leap_month {
735        "" | "split" => true,
736        "same" => false,
737        _ => return Err(invalid("leap_month must be \"split\" or \"same\"")),
738    };
739    let male = match options.sex {
740        None | Some("") => None,
741        Some("male") => Some(true),
742        Some("female") => Some(false),
743        Some(_) => return Err(invalid("sex must be \"male\" or \"female\"")),
744    };
745    let pillars = bazi(
746        calendar,
747        birth,
748        longitude,
749        utc_offset_minutes,
750        &BaziOptions {
751            solar_time: options.solar_time,
752            zi_hour: options.zi_hour,
753            sex: None,
754            luck_pillars: 0,
755            year: None,
756        },
757    )?;
758    let lunar = pillars
759        .lunar_date
760        .clone()
761        .ok_or_else(|| invalid("the calendar does not cover the birth date"))?;
762    let hour = BRANCHES
763        .iter()
764        .position(|b| *b == pillars.hour.branch)
765        .unwrap_or(ZI);
766    let month = if lunar.leap && split_leap && lunar.day > 15 {
767        lunar.month % 12 + 1
768    } else {
769        lunar.month
770    };
771    let year = year_cycle_index(lunar.year);
772    let (year_stem, year_branch) = (year % 10, year % 12);
773
774    // Life and body palaces, palace stems and the bureau.
775    let month_branch = YIN as i64 + i64::from(month) - 1;
776    let life = wrap(month_branch - hour as i64);
777    let body = wrap(month_branch + hour as i64);
778    let yin_stem = year_stem % 5 * 2 + 2;
779    let stem_of = |branch: usize| (yin_stem + wrap(branch as i64 - YIN as i64)) % 10;
780    let bureau = bureau_of(stem_of(life), life);
781
782    // Stars, by branch.
783    let mut placed: Vec<(usize, &'static str, &'static str)> = Vec::with_capacity(28);
784    let zi_wei_at = zi_wei_branch(bureau.number, lunar.day) as i64;
785    for (star, offset) in ZI_WEI_GROUP {
786        placed.push((wrap(zi_wei_at - offset as i64), star, "major"));
787    }
788    let tian_fu_at = 4 - zi_wei_at;
789    for (star, offset) in TIAN_FU_GROUP {
790        placed.push((wrap(tian_fu_at + offset as i64), star, "major"));
791    }
792    let (m, h) = (i64::from(month) - 1, hour as i64);
793    let (kui, yue) = kui_yue(year_stem);
794    let lu_cun = lu_cun_of(year_stem) as i64;
795    let horse = horse_of(year_branch);
796    let (fire, bell, small_start) = match trine_of(year_branch) {
797        0 => (YIN, XU, XU),     // 申子辰
798        1 => (MAO, XU, WEI),    // 巳酉丑
799        2 => (CHOU, MAO, CHEN), // 寅午戌
800        _ => (YOU, XU, CHOU),   // 亥卯未
801    };
802    let anchors = [
803        wrap(CHEN as i64 + m),
804        wrap(XU as i64 - m),
805        wrap(XU as i64 - h),
806        wrap(CHEN as i64 + h),
807    ];
808    let [zuo, you, chang, qu] = anchors;
809    for (branch, star, kind) in [
810        (zuo, ZUO_FU, "auxiliary"),
811        (you, YOU_BI, "auxiliary"),
812        (chang, WEN_CHANG, "auxiliary"),
813        (qu, WEN_QU, "auxiliary"),
814        (kui, TIAN_KUI, "auxiliary"),
815        (yue, TIAN_YUE, "auxiliary"),
816        (wrap(lu_cun), LU_CUN, "auxiliary"),
817        (horse, TIAN_MA, "auxiliary"),
818        (wrap(lu_cun + 1), QING_YANG, "malefic"),
819        (wrap(lu_cun - 1), TUO_LUO, "malefic"),
820        (wrap(fire as i64 + h), HUO_XING, "malefic"),
821        (wrap(bell as i64 + h), LING_XING, "malefic"),
822        (wrap(HAI as i64 - h), DI_KONG, "malefic"),
823        (wrap(HAI as i64 + h), DI_JIE, "malefic"),
824    ] {
825        placed.push((branch, star, kind));
826    }
827
828    let transformed = TRANSFORMATIONS[year_stem];
829    let transformation_of = |star: &str| {
830        transformed
831            .iter()
832            .position(|s| *s == star)
833            .map(|k| TRANSFORMATION_KINDS[k])
834    };
835
836    let minor = minor_stars(
837        year_stem,
838        year_branch,
839        m,
840        i64::from(lunar.day),
841        h,
842        life,
843        body,
844        anchors,
845    );
846
847    // Limits and the cycles of twelve.
848    let forward = male.map(|male| male == (year_stem % 2 == 0));
849    let palace_name = |branch: usize| PALACES[wrap(life as i64 - branch as i64)];
850    let star_branch = |star: &str| placed.iter().find(|(_, s, _)| *s == star).map(|p| p.0);
851    let transformations_of = |stem: usize| -> Vec<ZiWeiTransformation> {
852        TRANSFORMATIONS[stem]
853            .iter()
854            .zip(TRANSFORMATION_KINDS)
855            .filter_map(|(&star, kind)| {
856                Some(ZiWeiTransformation {
857                    kind,
858                    star,
859                    palace: palace_name(star_branch(star)?),
860                })
861            })
862            .collect()
863    };
864    let life_stages = forward.map(|forward| {
865        let start = match bureau.number {
866            2 | 5 => SHEN,
867            3 => HAI,
868            4 => SI,
869            _ => YIN,
870        };
871        cycle(&LIFE_STAGES, start, forward)
872    });
873    let boshi = forward.map(|forward| cycle(&BOSHI, lu_cun as usize, forward));
874    let (suiqian, jiangqian) = yearly_gods(year_branch);
875    let god_at = |gods: &[ZiWeiGod], branch: usize| {
876        gods.iter()
877            .find(|g| g.branch == BRANCHES[branch])
878            .map(|g| g.god)
879            .unwrap_or_default()
880    };
881
882    let palaces: Vec<ZiWeiPalace> = (0..12)
883        .map(|i| {
884            let branch = wrap(life as i64 - i);
885            let stars = placed
886                .iter()
887                .filter(|(b, ..)| *b == branch)
888                .map(|&(_, star, kind)| ZiWeiStar {
889                    star,
890                    kind,
891                    brightness: brightness(star, branch),
892                    transformation: transformation_of(star),
893                })
894                .collect();
895            let decade = forward.map(|forward| {
896                let steps = if forward {
897                    wrap(branch as i64 - life as i64)
898                } else {
899                    wrap(life as i64 - branch as i64)
900                } as u8;
901                let start = bureau.number + 10 * steps;
902                AgeRange {
903                    start,
904                    end: start + 9,
905                }
906            });
907            let small_limit_ages = male
908                .map(|male| {
909                    let steps = if male {
910                        wrap(branch as i64 - small_start as i64)
911                    } else {
912                        wrap(small_start as i64 - branch as i64)
913                    } as u8;
914                    (0..10).map(|j| steps + 1 + 12 * j).collect()
915                })
916                .unwrap_or_default();
917            ZiWeiPalace {
918                name: PALACES[i as usize],
919                branch: BRANCHES[branch],
920                stem: STEMS[stem_of(branch)],
921                is_body: branch == body,
922                stars,
923                decade,
924                small_limit_ages,
925                minor_stars: minor
926                    .iter()
927                    .filter(|(b, _)| *b == branch)
928                    .map(|&(_, star)| star)
929                    .collect(),
930                gods: ZiWeiGods {
931                    life_stage: life_stages.as_ref().map(|g| god_at(g, branch)),
932                    boshi: boshi.as_ref().map(|g| god_at(g, branch)),
933                    suiqian: god_at(&suiqian, branch),
934                    jiangqian: god_at(&jiangqian, branch),
935                },
936                flying: transformations_of(stem_of(branch)),
937            }
938        })
939        .collect();
940
941    let transformations = transformations_of(year_stem);
942
943    // The horoscope of the requested year.
944    let horoscope = options.year.map(|target| {
945        let age = target - lunar.year + 1;
946        let period = |branch: usize, stem: usize, flow_branch: usize| ZiWeiPeriod {
947            branch: BRANCHES[branch],
948            stem: STEMS[stem],
949            palaces: (0..12)
950                .map(|k| ZiWeiGod {
951                    god: PALACES[k],
952                    branch: BRANCHES[wrap(branch as i64 - k as i64)],
953                })
954                .collect(),
955            transformations: transformations_of(stem),
956            stars: flow_stars(stem, flow_branch),
957        };
958        let decade_branch = palaces.iter().find_map(|p: &ZiWeiPalace| {
959            let d = p.decade.as_ref()?;
960            (i32::from(d.start) <= age && age <= i32::from(d.end)).then_some(p.branch)
961        });
962        let childhood = forward.is_some()
963            && decade_branch.is_none()
964            && (1..i32::from(bureau.number)).contains(&age)
965            && age <= 6;
966        let decade_at = decade_branch
967            .and_then(|b| BRANCHES.iter().position(|x| *x == b))
968            .or_else(|| {
969                childhood.then(|| {
970                    let name = ["life", "wealth", "health", "spouse", "fortune", "career"]
971                        [(age - 1) as usize];
972                    wrap(life as i64 - PALACES.iter().position(|p| *p == name).unwrap_or(0) as i64)
973                })
974            });
975        let small_limit = palaces
976            .iter()
977            .find(|p: &&ZiWeiPalace| {
978                u8::try_from(age).is_ok_and(|a| p.small_limit_ages.contains(&a))
979            })
980            .map(|p| p.branch);
981        let cycle_index = year_cycle_index(target);
982        let (y_stem, y_branch) = (cycle_index % 10, cycle_index % 12);
983        let (suiqian, jiangqian) = yearly_gods(y_branch);
984        ZiWeiHoroscope {
985            year: target,
986            nominal_age: age,
987            decade: decade_at.map(|b| period(b, stem_of(b), b)),
988            childhood: childhood && decade_branch.is_none(),
989            small_limit,
990            yearly: period(y_branch, y_stem, y_branch),
991            suiqian,
992            jiangqian,
993        }
994    });
995
996    Ok(ZiWei {
997        time_basis: pillars.time_basis,
998        zi_hour: pillars.zi_hour,
999        lunar_date: lunar,
1000        month,
1001        hour_branch: BRANCHES[hour],
1002        life_palace: BRANCHES[life],
1003        body_palace: BRANCHES[body],
1004        bureau,
1005        life_master: LIFE_MASTERS[life],
1006        body_master: BODY_MASTERS[year_branch],
1007        palaces,
1008        transformations,
1009        decade_direction: forward.map(|f| if f { "forward" } else { "backward" }),
1010        horoscope,
1011    })
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016    use super::*;
1017
1018    #[test]
1019    fn zi_wei_from_the_bureau_and_the_day() {
1020        // The examples of the placement rhyme: day 27, wood 3 → 戌; day 13, fire 6 → 亥;
1021        // day 6, earth 5 → 未; and the first two days of the water bureau.
1022        assert_eq!(BRANCHES[zi_wei_branch(3, 27)], "xu");
1023        assert_eq!(BRANCHES[zi_wei_branch(6, 13)], "hai");
1024        assert_eq!(BRANCHES[zi_wei_branch(5, 6)], "wei");
1025        assert_eq!(BRANCHES[zi_wei_branch(2, 1)], "chou");
1026        assert_eq!(BRANCHES[zi_wei_branch(2, 2)], "yin");
1027        assert_eq!(BRANCHES[zi_wei_branch(6, 1)], "you");
1028    }
1029
1030    #[test]
1031    fn the_bureau_is_the_nayin_of_the_life_palace() {
1032        // 癸未 is willow wood: the wood bureau, 3.
1033        assert_eq!(
1034            bureau_of(9, 7),
1035            Bureau {
1036                element: "wood",
1037                number: 3
1038            }
1039        );
1040        // 甲子 is sea metal: 4.
1041        assert_eq!(bureau_of(0, 0).number, 4);
1042        // 丙子 is stream water: 2.
1043        assert_eq!(bureau_of(2, 0).number, 2);
1044    }
1045}