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. Given a date, the horoscope goes down to its month
36//!   (流月, by 斗君: month 1 is counted from the year's palace back to the birth month and
37//!   forward to the birth hour) and its day (流日), from an embedded lunar calendar.
38
39use 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
48/// The twelve palaces, counted backward through the branches from the life palace.
49const 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
83/// The Zi Wei group: each star's offset from Zi Wei, counted backward.
84const 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
93/// The Tian Fu group: each star's offset from Tian Fu, counted forward.
94const 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
105/// The Four Transformations (祿, 權, 科, 忌) of each year stem.
106const TRANSFORMATIONS: [[&str; 4]; 10] = [
107    [LIAN_ZHEN, PO_JUN, WU_QU, TAI_YANG],       // 甲
108    [TIAN_JI, TIAN_LIANG, ZI_WEI, TAI_YIN],     // 乙
109    [TIAN_TONG, TIAN_JI, WEN_CHANG, LIAN_ZHEN], // 丙
110    [TAI_YIN, TIAN_TONG, TIAN_JI, JU_MEN],      // 丁
111    [TAN_LANG, TAI_YIN, YOU_BI, TIAN_JI],       // 戊
112    [WU_QU, TAN_LANG, TIAN_LIANG, WEN_QU],      // 己
113    [TAI_YANG, WU_QU, TAI_YIN, TIAN_TONG],      // 庚
114    [JU_MEN, TAI_YANG, WEN_QU, WEN_CHANG],      // 辛
115    [TIAN_LIANG, ZI_WEI, ZUO_FU, WU_QU],        // 壬
116    [PO_JUN, JU_MEN, TAI_YIN, TAN_LANG],        // 癸
117];
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
129/// Brightness of a star in each branch, from 寅 (as the iztro project publishes it).
130const 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
245/// The life master (命主) by the life palace's branch.
246const 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
251/// The body master (身主) by the year branch.
252const 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
257/// The 博士 cycle, from 祿存.
258const 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
273/// The 歲前 cycle, from the year's branch.
274const 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
279/// The 將前 cycle, from the general star of the year's trine.
280const 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
295/// Branch indices.
296const 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/// How to cast the chart.
310#[derive(Debug, Clone)]
311pub struct ZiWeiOptions<'a> {
312    /// Take the birth hour on true solar time at the birthplace (`true`, the default) or on
313    /// civil time, as for the Four Pillars.
314    pub solar_time: bool,
315    /// "next_day" (default): from 23:00 the birth counts on the next lunar day; "split": the
316    /// day changes at midnight.
317    pub zi_hour: &'a str,
318    /// "male" or "female": sets the direction of the decade and small limits, which are left
319    /// out without it.
320    pub sex: Option<&'a str>,
321    /// "split" (default): a birth after the 15th of a leap month counts in the next month;
322    /// "same": every day of a leap month counts in the month it repeats.
323    pub leap_month: &'a str,
324    /// A lunar year to cast the horoscope (decade, small limit, year) for.
325    pub year: Option<i32>,
326    /// A date, `YYYY-MM-DD`, to cast the horoscope for down to its month (流月) and day
327    /// (流日); its lunar year replaces `year`. Dates from the lunar years 1900 to 2100.
328    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/// A star in a palace.
345#[derive(Debug, Clone, PartialEq, Serialize)]
346pub struct ZiWeiStar {
347    /// Star code, e.g. "zi_wei", "wen_chang".
348    pub star: &'static str,
349    /// "major" (the fourteen), "auxiliary" or "malefic".
350    pub kind: &'static str,
351    /// Brightness in this branch: "miao", "wang", "de", "li", "ping", "bu" or "xian"; `None`
352    /// where the table gives none.
353    pub brightness: Option<&'static str>,
354    /// The natal transformation it takes: "lu", "quan", "ke" or "ji".
355    pub transformation: Option<&'static str>,
356}
357
358/// A range of nominal ages (虛歲), inclusive.
359#[derive(Debug, Clone, PartialEq, Serialize)]
360pub struct AgeRange {
361    /// First age.
362    pub start: u8,
363    /// Last age.
364    pub end: u8,
365}
366
367/// One of the twelve palaces.
368#[derive(Debug, Clone, PartialEq, Serialize)]
369pub struct ZiWeiPalace {
370    /// Palace code, from "life" to "parents".
371    pub name: &'static str,
372    /// Earthly Branch of its place on the board.
373    pub branch: &'static str,
374    /// Its Heavenly Stem.
375    pub stem: &'static str,
376    /// Whether the body palace falls here.
377    pub is_body: bool,
378    /// Its stars: the major ones first.
379    pub stars: Vec<ZiWeiStar>,
380    /// The decade limit it rules; `None` without the native's sex.
381    pub decade: Option<AgeRange>,
382    /// The nominal ages whose small limit falls here, up to 120; empty without the sex.
383    pub small_limit_ages: Vec<u8>,
384    /// The minor stars (雜曜) here.
385    pub minor_stars: Vec<&'static str>,
386    /// The gods of the four cycles of twelve here.
387    pub gods: ZiWeiGods,
388    /// The four transformations this palace's stem flies (飛化), to the palaces of the
389    /// stars they transform.
390    pub flying: Vec<ZiWeiTransformation>,
391}
392
393/// The gods of the four cycles of twelve in a palace.
394#[derive(Debug, Clone, PartialEq, Serialize)]
395pub struct ZiWeiGods {
396    /// Stage of the 長生 cycle, from the bureau (as Ba Zi's `life_stage` codes); `None`
397    /// without the native's sex.
398    pub life_stage: Option<&'static str>,
399    /// God of the 博士 cycle, from 祿存; `None` without the native's sex.
400    pub boshi: Option<&'static str>,
401    /// God of the 歲前 cycle of the birth year.
402    pub suiqian: &'static str,
403    /// God of the 將前 cycle of the birth year.
404    pub jiangqian: &'static str,
405}
406
407/// A moving star of a horoscope period.
408#[derive(Debug, Clone, PartialEq, Serialize)]
409pub struct ZiWeiFlowStar {
410    /// Star code: "tian_kui", "tian_yue", "wen_chang", "wen_qu", "lu_cun", "qing_yang",
411    /// "tuo_luo", "tian_ma", "hong_luan" or "tian_xi".
412    pub star: &'static str,
413    /// Branch of the palace it moves to.
414    pub branch: &'static str,
415}
416
417/// A god of a yearly cycle of twelve.
418#[derive(Debug, Clone, PartialEq, Serialize)]
419pub struct ZiWeiGod {
420    /// God code.
421    pub god: &'static str,
422    /// Branch of its palace.
423    pub branch: &'static str,
424}
425
426/// One period of a horoscope: a decade, the childhood limit or a year.
427#[derive(Debug, Clone, PartialEq, Serialize)]
428pub struct ZiWeiPeriod {
429    /// Branch of the palace the period's life palace falls in.
430    pub branch: &'static str,
431    /// The period's stem (the decade palace's, or the year's).
432    pub stem: &'static str,
433    /// The palace names of the period, by branch, life first.
434    pub palaces: Vec<ZiWeiGod>,
435    /// The four transformations of the period's stem, with the natal palace of each star.
436    pub transformations: Vec<ZiWeiTransformation>,
437    /// The period's moving stars.
438    pub stars: Vec<ZiWeiFlowStar>,
439}
440
441/// The horoscope of a year.
442#[derive(Debug, Clone, PartialEq, Serialize)]
443pub struct ZiWeiHoroscope {
444    /// The lunar year.
445    pub year: i32,
446    /// Nominal age (虛歲) in that year.
447    pub nominal_age: i32,
448    /// The decade limit running in that year; `None` without the native's sex or outside
449    /// the decades.
450    pub decade: Option<ZiWeiPeriod>,
451    /// Whether `decade` is the childhood limit (童限), before the first decade starts.
452    pub childhood: bool,
453    /// Branch of the small limit's palace; `None` without the native's sex.
454    pub small_limit: Option<&'static str>,
455    /// The year (流年): its life palace is the year's branch.
456    pub yearly: ZiWeiPeriod,
457    /// The 歲前 gods of the year.
458    pub suiqian: Vec<ZiWeiGod>,
459    /// The 將前 gods of the year.
460    pub jiangqian: Vec<ZiWeiGod>,
461    /// The date read, when one was given.
462    pub date: Option<String>,
463    /// Its lunar date.
464    pub lunar_date: Option<LunarDate>,
465    /// The month (流月) of the date: month 1 is counted from the year's palace back to the
466    /// birth month and forward to the birth hour (斗君), one palace a month.
467    pub monthly: Option<ZiWeiPeriod>,
468    /// The day (流日) of the date: from the month's palace, one palace a day.
469    pub daily: Option<ZiWeiPeriod>,
470}
471
472/// A natal transformation.
473#[derive(Debug, Clone, PartialEq, Serialize)]
474pub struct ZiWeiTransformation {
475    /// "lu" (祿), "quan" (權), "ke" (科) or "ji" (忌).
476    pub kind: &'static str,
477    /// The star transformed.
478    pub star: &'static str,
479    /// The palace it is in.
480    pub palace: &'static str,
481}
482
483/// The five-element bureau.
484#[derive(Debug, Clone, PartialEq, Serialize)]
485pub struct Bureau {
486    /// "water", "wood", "metal", "earth" or "fire".
487    pub element: &'static str,
488    /// 2 (water) to 6 (fire): the age the first decade starts at.
489    pub number: u8,
490}
491
492/// The Purple Star chart of a birth.
493#[derive(Debug, Clone, PartialEq, Serialize)]
494pub struct ZiWei {
495    /// "solar" or "civil": the clock the birth hour was taken on.
496    pub time_basis: &'static str,
497    /// "next_day" or "split".
498    pub zi_hour: &'static str,
499    /// The lunar date of birth.
500    pub lunar_date: LunarDate,
501    /// The month the chart is cast with (a leap month may count as the next one).
502    pub month: u8,
503    /// The birth hour's branch.
504    pub hour_branch: &'static str,
505    /// Branch of the life palace (命宮).
506    pub life_palace: &'static str,
507    /// Branch of the body palace (身宮).
508    pub body_palace: &'static str,
509    /// The five-element bureau (五行局).
510    pub bureau: Bureau,
511    /// The life master (命主), a star code.
512    pub life_master: &'static str,
513    /// The body master (身主), a star code.
514    pub body_master: &'static str,
515    /// The twelve palaces, from the life palace in palace order.
516    pub palaces: Vec<ZiWeiPalace>,
517    /// The four natal transformations, 祿 first.
518    pub transformations: Vec<ZiWeiTransformation>,
519    /// "forward" or "backward" (through the branches) for the decade limits; `None`
520    /// without the native's sex.
521    pub decade_direction: Option<&'static str>,
522    /// The horoscope of the requested year; `None` when none was asked for.
523    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
534/// The branch Zi Wei sits in for a bureau and a lunar day (1-30).
535pub(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
547/// The bureau of a life palace's stem and branch: its NaYin element.
548fn 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
570/// 天魁 and 天鉞 of a stem.
571fn 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
581/// 祿存 of a stem.
582fn lu_cun_of(stem: usize) -> usize {
583    [YIN, MAO, SI, WU, SI, WU, SHEN, YOU, HAI, ZI][stem]
584}
585
586/// The trine of a branch: 0 申子辰, 1 巳酉丑, 2 寅午戌, 3 亥卯未.
587fn trine_of(branch: usize) -> usize {
588    branch % 4
589}
590
591/// 天馬 of a branch.
592fn horse_of(branch: usize) -> usize {
593    [YIN, HAI, SHEN, SI][trine_of(branch)]
594}
595
596/// 紅鸞 and 天喜 of a branch.
597fn 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
602/// The moving 文昌 and 文曲 of a stem (流昌, 流曲).
603fn 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
616/// The ten moving stars of a period's stem and branch.
617fn 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
642/// A cycle of twelve gods laid from `start`, forward or backward.
643fn 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
652/// The 歲前 and 將前 gods of a year's branch.
653fn 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/// The 38 minor stars, by branch.
662#[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    // 天傷 sits in the friends palace, 天使 in the health palace.
686    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
741/// Cast the Purple Star chart of a birth at `birth` (UTC), at `longitude` (degrees east)
742/// where civil time was `utc_offset_minutes` ahead of UTC, from its `calendar`.
743pub 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    // Life and body palaces, palace stems and the bureau.
792    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    // Stars, by branch.
800    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),     // 申子辰
815        1 => (MAO, XU, WEI),    // 巳酉丑
816        2 => (CHOU, MAO, CHEN), // 寅午戌
817        _ => (YOU, XU, CHOU),   // 亥卯未
818    };
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    // Limits and the cycles of twelve.
865    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    // The horoscope of the requested year.
961    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            // The month and day of the date read.
1020            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        // The examples of the placement rhyme: day 27, wood 3 → 戌; day 13, fire 6 → 亥;
1086        // day 6, earth 5 → 未; and the first two days of the water bureau.
1087        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        // 癸未 is willow wood: the wood bureau, 3.
1098        assert_eq!(
1099            bureau_of(9, 7),
1100            Bureau {
1101                element: "wood",
1102                number: 3
1103            }
1104        );
1105        // 甲子 is sea metal: 4.
1106        assert_eq!(bureau_of(0, 0).number, 4);
1107        // 丙子 is stream water: 2.
1108        assert_eq!(bureau_of(2, 0).number, 2);
1109    }
1110}