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        },
773    )?;
774    let lunar = pillars
775        .lunar_date
776        .clone()
777        .ok_or_else(|| invalid("the calendar does not cover the birth date"))?;
778    let hour = BRANCHES
779        .iter()
780        .position(|b| *b == pillars.hour.branch)
781        .unwrap_or(ZI);
782    let month = if lunar.leap && split_leap && lunar.day > 15 {
783        lunar.month % 12 + 1
784    } else {
785        lunar.month
786    };
787    let year = year_cycle_index(lunar.year);
788    let (year_stem, year_branch) = (year % 10, year % 12);
789
790    // Life and body palaces, palace stems and the bureau.
791    let month_branch = YIN as i64 + i64::from(month) - 1;
792    let life = wrap(month_branch - hour as i64);
793    let body = wrap(month_branch + hour as i64);
794    let yin_stem = year_stem % 5 * 2 + 2;
795    let stem_of = |branch: usize| (yin_stem + wrap(branch as i64 - YIN as i64)) % 10;
796    let bureau = bureau_of(stem_of(life), life);
797
798    // Stars, by branch.
799    let mut placed: Vec<(usize, &'static str, &'static str)> = Vec::with_capacity(28);
800    let zi_wei_at = zi_wei_branch(bureau.number, lunar.day) as i64;
801    for (star, offset) in ZI_WEI_GROUP {
802        placed.push((wrap(zi_wei_at - offset as i64), star, "major"));
803    }
804    let tian_fu_at = 4 - zi_wei_at;
805    for (star, offset) in TIAN_FU_GROUP {
806        placed.push((wrap(tian_fu_at + offset as i64), star, "major"));
807    }
808    let (m, h) = (i64::from(month) - 1, hour as i64);
809    let (kui, yue) = kui_yue(year_stem);
810    let lu_cun = lu_cun_of(year_stem) as i64;
811    let horse = horse_of(year_branch);
812    let (fire, bell, small_start) = match trine_of(year_branch) {
813        0 => (YIN, XU, XU),     // 申子辰
814        1 => (MAO, XU, WEI),    // 巳酉丑
815        2 => (CHOU, MAO, CHEN), // 寅午戌
816        _ => (YOU, XU, CHOU),   // 亥卯未
817    };
818    let anchors = [
819        wrap(CHEN as i64 + m),
820        wrap(XU as i64 - m),
821        wrap(XU as i64 - h),
822        wrap(CHEN as i64 + h),
823    ];
824    let [zuo, you, chang, qu] = anchors;
825    for (branch, star, kind) in [
826        (zuo, ZUO_FU, "auxiliary"),
827        (you, YOU_BI, "auxiliary"),
828        (chang, WEN_CHANG, "auxiliary"),
829        (qu, WEN_QU, "auxiliary"),
830        (kui, TIAN_KUI, "auxiliary"),
831        (yue, TIAN_YUE, "auxiliary"),
832        (wrap(lu_cun), LU_CUN, "auxiliary"),
833        (horse, TIAN_MA, "auxiliary"),
834        (wrap(lu_cun + 1), QING_YANG, "malefic"),
835        (wrap(lu_cun - 1), TUO_LUO, "malefic"),
836        (wrap(fire as i64 + h), HUO_XING, "malefic"),
837        (wrap(bell as i64 + h), LING_XING, "malefic"),
838        (wrap(HAI as i64 - h), DI_KONG, "malefic"),
839        (wrap(HAI as i64 + h), DI_JIE, "malefic"),
840    ] {
841        placed.push((branch, star, kind));
842    }
843
844    let transformed = TRANSFORMATIONS[year_stem];
845    let transformation_of = |star: &str| {
846        transformed
847            .iter()
848            .position(|s| *s == star)
849            .map(|k| TRANSFORMATION_KINDS[k])
850    };
851
852    let minor = minor_stars(
853        year_stem,
854        year_branch,
855        m,
856        i64::from(lunar.day),
857        h,
858        life,
859        body,
860        anchors,
861    );
862
863    // Limits and the cycles of twelve.
864    let forward = male.map(|male| male == (year_stem % 2 == 0));
865    let palace_name = |branch: usize| PALACES[wrap(life as i64 - branch as i64)];
866    let star_branch = |star: &str| placed.iter().find(|(_, s, _)| *s == star).map(|p| p.0);
867    let transformations_of = |stem: usize| -> Vec<ZiWeiTransformation> {
868        TRANSFORMATIONS[stem]
869            .iter()
870            .zip(TRANSFORMATION_KINDS)
871            .filter_map(|(&star, kind)| {
872                Some(ZiWeiTransformation {
873                    kind,
874                    star,
875                    palace: palace_name(star_branch(star)?),
876                })
877            })
878            .collect()
879    };
880    let life_stages = forward.map(|forward| {
881        let start = match bureau.number {
882            2 | 5 => SHEN,
883            3 => HAI,
884            4 => SI,
885            _ => YIN,
886        };
887        cycle(&LIFE_STAGES, start, forward)
888    });
889    let boshi = forward.map(|forward| cycle(&BOSHI, lu_cun as usize, forward));
890    let (suiqian, jiangqian) = yearly_gods(year_branch);
891    let god_at = |gods: &[ZiWeiGod], branch: usize| {
892        gods.iter()
893            .find(|g| g.branch == BRANCHES[branch])
894            .map(|g| g.god)
895            .unwrap_or_default()
896    };
897
898    let palaces: Vec<ZiWeiPalace> = (0..12)
899        .map(|i| {
900            let branch = wrap(life as i64 - i);
901            let stars = placed
902                .iter()
903                .filter(|(b, ..)| *b == branch)
904                .map(|&(_, star, kind)| ZiWeiStar {
905                    star,
906                    kind,
907                    brightness: brightness(star, branch),
908                    transformation: transformation_of(star),
909                })
910                .collect();
911            let decade = forward.map(|forward| {
912                let steps = if forward {
913                    wrap(branch as i64 - life as i64)
914                } else {
915                    wrap(life as i64 - branch as i64)
916                } as u8;
917                let start = bureau.number + 10 * steps;
918                AgeRange {
919                    start,
920                    end: start + 9,
921                }
922            });
923            let small_limit_ages = male
924                .map(|male| {
925                    let steps = if male {
926                        wrap(branch as i64 - small_start as i64)
927                    } else {
928                        wrap(small_start as i64 - branch as i64)
929                    } as u8;
930                    (0..10).map(|j| steps + 1 + 12 * j).collect()
931                })
932                .unwrap_or_default();
933            ZiWeiPalace {
934                name: PALACES[i as usize],
935                branch: BRANCHES[branch],
936                stem: STEMS[stem_of(branch)],
937                is_body: branch == body,
938                stars,
939                decade,
940                small_limit_ages,
941                minor_stars: minor
942                    .iter()
943                    .filter(|(b, _)| *b == branch)
944                    .map(|&(_, star)| star)
945                    .collect(),
946                gods: ZiWeiGods {
947                    life_stage: life_stages.as_ref().map(|g| god_at(g, branch)),
948                    boshi: boshi.as_ref().map(|g| god_at(g, branch)),
949                    suiqian: god_at(&suiqian, branch),
950                    jiangqian: god_at(&jiangqian, branch),
951                },
952                flying: transformations_of(stem_of(branch)),
953            }
954        })
955        .collect();
956
957    let transformations = transformations_of(year_stem);
958
959    // The horoscope of the requested year.
960    let target_date = options
961        .date
962        .map(|text| {
963            let day = UtcInstant::parse(&format!("{text}T00:00:00Z"))
964                .map(day_number)
965                .map_err(|_| invalid("date must be YYYY-MM-DD"))?;
966            let (y, m, leap, d) = lunar_date_of(day)
967                .ok_or_else(|| invalid("date is outside the lunar years 1900-2100"))?;
968            Ok::<_, EngineError>((text.to_string(), day, y, m, leap, d))
969        })
970        .transpose()?;
971    let horoscope = target_date
972        .as_ref()
973        .map(|t| t.2)
974        .or(options.year)
975        .map(|target| {
976            let age = target - lunar.year + 1;
977            let period = |branch: usize, stem: usize, flow_branch: usize| ZiWeiPeriod {
978                branch: BRANCHES[branch],
979                stem: STEMS[stem],
980                palaces: (0..12)
981                    .map(|k| ZiWeiGod {
982                        god: PALACES[k],
983                        branch: BRANCHES[wrap(branch as i64 - k as i64)],
984                    })
985                    .collect(),
986                transformations: transformations_of(stem),
987                stars: flow_stars(stem, flow_branch),
988            };
989            let decade_branch = palaces.iter().find_map(|p: &ZiWeiPalace| {
990                let d = p.decade.as_ref()?;
991                (i32::from(d.start) <= age && age <= i32::from(d.end)).then_some(p.branch)
992            });
993            let childhood = forward.is_some()
994                && decade_branch.is_none()
995                && (1..i32::from(bureau.number)).contains(&age)
996                && age <= 6;
997            let decade_at = decade_branch
998                .and_then(|b| BRANCHES.iter().position(|x| *x == b))
999                .or_else(|| {
1000                    childhood.then(|| {
1001                        let name = ["life", "wealth", "health", "spouse", "fortune", "career"]
1002                            [(age - 1) as usize];
1003                        wrap(
1004                            life as i64
1005                                - PALACES.iter().position(|p| *p == name).unwrap_or(0) as i64,
1006                        )
1007                    })
1008                });
1009            let small_limit = palaces
1010                .iter()
1011                .find(|p: &&ZiWeiPalace| {
1012                    u8::try_from(age).is_ok_and(|a| p.small_limit_ages.contains(&a))
1013                })
1014                .map(|p| p.branch);
1015            let cycle_index = year_cycle_index(target);
1016            let (y_stem, y_branch) = (cycle_index % 10, cycle_index % 12);
1017            let (suiqian, jiangqian) = yearly_gods(y_branch);
1018            // The month and day of the date read.
1019            let flows = target_date.as_ref().map(|(_, day, _, m, leap, d)| {
1020                let m = if *leap && *d > 15 { m % 12 + 1 } else { *m };
1021                let month_palace =
1022                    wrap(y_branch as i64 - i64::from(month) + hour as i64 + i64::from(m));
1023                let month_branch = wrap(YIN as i64 + i64::from(m) - 1);
1024                let month_stem = (y_stem % 5 * 2 + 2 + usize::from(m) - 1) % 10;
1025                let day_cycle = (day - 11).rem_euclid(60) as usize;
1026                let day_palace = wrap(month_palace as i64 + i64::from(*d) - 1);
1027                (
1028                    period(month_palace, month_stem, month_branch),
1029                    period(day_palace, day_cycle % 10, day_cycle % 12),
1030                )
1031            });
1032            let (monthly, daily) = flows.map_or((None, None), |(m, d)| (Some(m), Some(d)));
1033            ZiWeiHoroscope {
1034                year: target,
1035                nominal_age: age,
1036                decade: decade_at.map(|b| period(b, stem_of(b), b)),
1037                childhood: childhood && decade_branch.is_none(),
1038                small_limit,
1039                yearly: period(y_branch, y_stem, y_branch),
1040                suiqian,
1041                jiangqian,
1042                date: target_date.as_ref().map(|t| t.0.clone()),
1043                lunar_date: target_date.as_ref().map(|&(_, _, y, m, leap, d)| {
1044                    let cycle = year_cycle_index(y);
1045                    LunarDate {
1046                        year: y,
1047                        month: m,
1048                        leap,
1049                        day: d,
1050                        year_stem: STEMS[cycle % 10],
1051                        year_branch: BRANCHES[cycle % 12],
1052                        animal: super::ANIMALS[cycle % 12],
1053                    }
1054                }),
1055                monthly,
1056                daily,
1057            }
1058        });
1059
1060    Ok(ZiWei {
1061        time_basis: pillars.time_basis,
1062        zi_hour: pillars.zi_hour,
1063        lunar_date: lunar,
1064        month,
1065        hour_branch: BRANCHES[hour],
1066        life_palace: BRANCHES[life],
1067        body_palace: BRANCHES[body],
1068        bureau,
1069        life_master: LIFE_MASTERS[life],
1070        body_master: BODY_MASTERS[year_branch],
1071        palaces,
1072        transformations,
1073        decade_direction: forward.map(|f| if f { "forward" } else { "backward" }),
1074        horoscope,
1075    })
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081
1082    #[test]
1083    fn zi_wei_from_the_bureau_and_the_day() {
1084        // The examples of the placement rhyme: day 27, wood 3 → 戌; day 13, fire 6 → 亥;
1085        // day 6, earth 5 → 未; and the first two days of the water bureau.
1086        assert_eq!(BRANCHES[zi_wei_branch(3, 27)], "xu");
1087        assert_eq!(BRANCHES[zi_wei_branch(6, 13)], "hai");
1088        assert_eq!(BRANCHES[zi_wei_branch(5, 6)], "wei");
1089        assert_eq!(BRANCHES[zi_wei_branch(2, 1)], "chou");
1090        assert_eq!(BRANCHES[zi_wei_branch(2, 2)], "yin");
1091        assert_eq!(BRANCHES[zi_wei_branch(6, 1)], "you");
1092    }
1093
1094    #[test]
1095    fn the_bureau_is_the_nayin_of_the_life_palace() {
1096        // 癸未 is willow wood: the wood bureau, 3.
1097        assert_eq!(
1098            bureau_of(9, 7),
1099            Bureau {
1100                element: "wood",
1101                number: 3
1102            }
1103        );
1104        // 甲子 is sea metal: 4.
1105        assert_eq!(bureau_of(0, 0).number, 4);
1106        // 丙子 is stream water: 2.
1107        assert_eq!(bureau_of(2, 0).number, 2);
1108    }
1109}