Skip to main content

x_iztro/data/
types.rs

1use serde::{Deserialize, Serialize};
2
3/// 阴阳
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5pub enum YinYang {
6    /// 阳
7    Yang,
8    /// 阴
9    Yin,
10}
11
12impl YinYang {
13    /// 单字写法
14    ///
15    /// 阴阳在 iztro 中不参与国际化,六种语言下都是这两个汉字。
16    pub fn as_str(self) -> &'static str {
17        match self {
18            YinYang::Yang => "阳",
19            YinYang::Yin => "阴",
20        }
21    }
22}
23
24/// 五行
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub enum FiveElements {
27    /// 木
28    Wood,
29    /// 金
30    Metal,
31    /// 水
32    Water,
33    /// 火
34    Fire,
35    /// 土
36    Earth,
37}
38
39impl FiveElements {
40    /// 单字写法
41    ///
42    /// 五行在 iztro 中不参与国际化,六种语言下都是这五个汉字。
43    pub fn as_str(self) -> &'static str {
44        match self {
45            FiveElements::Wood => "木",
46            FiveElements::Metal => "金",
47            FiveElements::Water => "水",
48            FiveElements::Fire => "火",
49            FiveElements::Earth => "土",
50        }
51    }
52}
53
54/// 天干
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
56#[repr(usize)]
57pub enum HeavenlyStem {
58    /// 甲
59    Jia,
60    /// 乙
61    Yi,
62    /// 丙
63    Bing,
64    /// 丁
65    Ding,
66    /// 戊
67    Wu,
68    /// 己
69    Ji,
70    /// 庚
71    Geng,
72    /// 辛
73    Xin,
74    /// 壬
75    Ren,
76    /// 癸
77    Gui,
78}
79
80/// 地支
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
82#[repr(usize)]
83pub enum EarthlyBranch {
84    /// 子
85    Zi,
86    /// 丑
87    Chou,
88    /// 寅
89    Yin,
90    /// 卯
91    Mao,
92    /// 辰
93    Chen,
94    /// 巳
95    Si,
96    /// 午
97    Wu,
98    /// 未
99    Wei,
100    /// 申
101    Shen,
102    /// 酉
103    You,
104    /// 戌
105    Xu,
106    /// 亥
107    Hai,
108}
109
110/// 十二宫位
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[repr(usize)]
113pub enum Palace {
114    /// 命宫
115    Soul,
116    /// 父母
117    Parents,
118    /// 福德
119    Spirit,
120    /// 田宅
121    Property,
122    /// 官禄
123    Career,
124    /// 交友
125    Friends,
126    /// 迁移
127    Surface,
128    /// 疾厄
129    Health,
130    /// 财帛
131    Wealth,
132    /// 子女
133    Children,
134    /// 夫妻
135    Spouse,
136    /// 兄弟
137    Siblings,
138}
139
140/// 五行局
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[repr(u8)]
143pub enum FiveElementsClass {
144    /// 水二局
145    Water2nd = 2,
146    /// 木三局
147    Wood3rd = 3,
148    /// 金四局
149    Metal4th = 4,
150    /// 土五局
151    Earth5th = 5,
152    /// 火六局
153    Fire6th = 6,
154}
155
156/// 四化
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
158pub enum Mutagen {
159    /// 禄
160    Lu,
161    /// 权
162    Quan,
163    /// 科
164    Ke,
165    /// 忌
166    Ji,
167}
168
169/// 星曜亮度
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171pub enum Brightness {
172    /// 庙
173    Miao,
174    /// 旺
175    Wang,
176    /// 得
177    De,
178    /// 利
179    Li,
180    /// 平
181    Ping,
182    /// 不
183    Bu,
184    /// 陷
185    Xian,
186}
187
188/// 星曜类型
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
190#[non_exhaustive]
191pub enum StarType {
192    /// 主星
193    Major,
194    /// 吉星
195    Soft,
196    /// 煞星
197    Tough,
198    /// 杂耀
199    Adjective,
200    /// 桃花星
201    Flower,
202    /// 解神
203    Helper,
204    /// 禄存
205    Lucun,
206    /// 天马
207    Tianma,
208}
209
210impl StarType {
211    /// 语言无关标识(与 JS iztro 的 `type` 取值一致)
212    pub fn as_key(self) -> &'static str {
213        match self {
214            StarType::Major => "major",
215            StarType::Soft => "soft",
216            StarType::Tough => "tough",
217            StarType::Adjective => "adjective",
218            StarType::Flower => "flower",
219            StarType::Helper => "helper",
220            StarType::Lucun => "lucun",
221            StarType::Tianma => "tianma",
222        }
223    }
224}
225
226/// 运限范围
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
228#[non_exhaustive]
229pub enum Scope {
230    /// 本命
231    Origin,
232    /// 大限
233    Decadal,
234    /// 流年
235    Yearly,
236    /// 流月
237    Monthly,
238    /// 流日
239    Daily,
240    /// 流时
241    Hourly,
242}
243
244impl Scope {
245    /// 语言无关标识(与 JS iztro 的 `scope` 取值一致)
246    pub fn as_key(self) -> &'static str {
247        match self {
248            Scope::Origin => "origin",
249            Scope::Decadal => "decadal",
250            Scope::Yearly => "yearly",
251            Scope::Monthly => "monthly",
252            Scope::Daily => "daily",
253            Scope::Hourly => "hourly",
254        }
255    }
256
257    /// 由语言无关标识还原;未知标识返回 `None`
258    pub fn from_key(key: &str) -> Option<Self> {
259        match key {
260            "origin" => Some(Scope::Origin),
261            "decadal" => Some(Scope::Decadal),
262            "yearly" => Some(Scope::Yearly),
263            "monthly" => Some(Scope::Monthly),
264            "daily" => Some(Scope::Daily),
265            "hourly" => Some(Scope::Hourly),
266            _ => None,
267        }
268    }
269}
270
271/// 运限层级显示名
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
273pub enum HoroscopeName {
274    /// 大限
275    Decadal,
276    /// 童限(未起运时的大限位)
277    Childhood,
278    /// 小限
279    Age,
280    /// 流年
281    Yearly,
282    /// 流月
283    Monthly,
284    /// 流日
285    Daily,
286    /// 流时
287    Hourly,
288}
289
290/// 性别
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
292pub enum Gender {
293    /// 男
294    Male,
295    /// 女
296    Female,
297}
298
299impl Gender {
300    /// 性别的阴阳:男为阳、女为阴,决定大限与长生十二神的顺逆
301    pub fn yin_yang(self) -> YinYang {
302        match self {
303            Gender::Male => YinYang::Yang,
304            Gender::Female => YinYang::Yin,
305        }
306    }
307}
308
309/// 语言
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
311pub enum Language {
312    /// 简体中文
313    ZhCN,
314    /// 繁体中文
315    ZhTW,
316    /// 英文
317    EnUS,
318    /// 日文
319    JaJP,
320    /// 韩文
321    KoKR,
322    /// 越南文
323    ViVN,
324}
325
326impl Language {
327    /// 语言代码,取值与 iztro 的 `Language` 一致
328    pub fn as_code(self) -> &'static str {
329        match self {
330            Language::ZhCN => "zh-CN",
331            Language::ZhTW => "zh-TW",
332            Language::EnUS => "en-US",
333            Language::JaJP => "ja-JP",
334            Language::KoKR => "ko-KR",
335            Language::ViVN => "vi-VN",
336        }
337    }
338
339    /// 由语言代码还原,大小写不敏感,连字符与下划线等价(`zh-CN` / `zh_cn` 都可);
340    /// 未知代码返回 `None`
341    pub fn from_code(code: &str) -> Option<Self> {
342        match code.to_ascii_lowercase().replace('_', "-").as_str() {
343            "zh-cn" => Some(Language::ZhCN),
344            "zh-tw" => Some(Language::ZhTW),
345            "en-us" => Some(Language::EnUS),
346            "ja-jp" => Some(Language::JaJP),
347            "ko-kr" => Some(Language::KoKR),
348            "vi-vn" => Some(Language::ViVN),
349            _ => None,
350        }
351    }
352}
353
354/// 算法
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
356#[non_exhaustive]
357pub enum Algorithm {
358    /// 默认
359    Default,
360    /// 中州派
361    Zhongzhou,
362}
363
364/// 排盘视角:中州派把同一组出生数据看作三张盘,差别在于用哪一宫的干支起五行局。
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
366#[non_exhaustive]
367pub enum AstroType {
368    /// 天盘:以命宫干支起五行局,即常规排盘结果
369    Heaven,
370    /// 地盘:以身宫干支起五行局,身宫即为新盘的命宫
371    Earth,
372    /// 人盘:以福德宫干支起五行局,福德宫即为新盘的命宫
373    Human,
374}
375
376/// 农历输入的闰月处理方式(`by_lunar` 专用)。
377///
378/// 把 iztro `byLunar` 的 `isLeapMonth` 与 `fixLeap` 两个布尔合成一个三态值:
379/// 两个布尔相邻传参极易写反且不报错,而 `fixLeap` 只在输入是闰月时才有意义,
380/// 三态恰好覆盖全部有效组合。阳历排盘的 `fix_leap` 仍是单个布尔(见 [`by_solar`])。
381///
382/// [`by_solar`]: crate::by_solar
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
384pub enum LeapMonth {
385    /// 输入的农历月不是闰月
386    NotLeap,
387    /// 输入的农历月是闰月,按闰月本身排盘
388    Leap,
389    /// 输入的农历月是闰月,且十五之后视作次月(iztro `fixLeap`)
390    LeapFixed,
391}
392
393impl LeapMonth {
394    /// 由 iztro 风格的两个布尔(`isLeapMonth`、`fixLeap`)合成;非闰月时 `fix_leap` 被忽略
395    pub fn from_flags(is_leap_month: bool, fix_leap: bool) -> Self {
396        match (is_leap_month, fix_leap) {
397            (false, _) => LeapMonth::NotLeap,
398            (true, false) => LeapMonth::Leap,
399            (true, true) => LeapMonth::LeapFixed,
400        }
401    }
402
403    /// 输入月是否闰月
404    pub fn is_leap_month(self) -> bool {
405        !matches!(self, LeapMonth::NotLeap)
406    }
407
408    /// 是否按 iztro `fixLeap` 规则把闰月十五之后视作次月
409    pub fn fix_leap(self) -> bool {
410        matches!(self, LeapMonth::LeapFixed)
411    }
412}
413
414/// 年分界点:排盘年干支(及其驱动的四化、命主身主等)按哪一天换年
415#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
416pub enum YearDivide {
417    /// 正月初一分界
418    Normal,
419    /// 立春分界
420    Exact,
421}
422
423/// 运限分界点:运限干支与干支纪日的月柱按初一还是节气推算
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
425pub enum HoroscopeDivide {
426    /// 年按正月初一分界,月按初一以五虎遁推算
427    Normal,
428    /// 年按立春分界,月按节气分界
429    Exact,
430}
431
432/// 虚岁分界点
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
434pub enum AgeDivide {
435    /// 以自然农历年为界,跨年即加一岁
436    Normal,
437    /// 以生日为界,过了生日才加一岁
438    Birthday,
439}
440
441/// 晚子时(23:00-00:00)归属
442#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
443pub enum DayDivide {
444    /// 晚子时归次日
445    Forward,
446    /// 晚子时归当天(按当日早子时排盘)
447    Current,
448}
449
450/// 为配置开关枚举生成语言无关标识的双向映射。
451///
452/// 绑定层 config JSON 的取值与这里一一对应,是该映射的唯一来源——
453/// 解析入参与回写 `ConfigDto` 都走它,两处不会各抄一份而走样。
454macro_rules! config_switch_keys {
455    ($ty:ident { $($variant:ident => $key:literal),+ $(,)? }) => {
456        impl $ty {
457            /// 语言无关标识(与 JS iztro 同名配置项的取值一致)
458            pub fn as_key(self) -> &'static str {
459                match self {
460                    $($ty::$variant => $key),+
461                }
462            }
463
464            /// 由语言无关标识还原;未知标识返回 `None`
465            pub fn from_key(key: &str) -> Option<Self> {
466                match key {
467                    $($key => Some($ty::$variant),)+
468                    _ => None,
469                }
470            }
471        }
472    };
473}
474
475config_switch_keys!(YearDivide { Normal => "normal", Exact => "exact" });
476config_switch_keys!(HoroscopeDivide { Normal => "normal", Exact => "exact" });
477config_switch_keys!(AgeDivide { Normal => "normal", Birthday => "birthday" });
478config_switch_keys!(DayDivide { Forward => "forward", Current => "current" });
479config_switch_keys!(Algorithm { Default => "default", Zhongzhou => "zhongzhou" });
480config_switch_keys!(AstroType { Heaven => "heaven", Earth => "earth", Human => "human" });
481config_switch_keys!(LeapMonth { NotLeap => "notLeap", Leap => "leap", LeapFixed => "leapFixed" });
482
483/// 自定义四化与亮度表。
484///
485/// 紫微斗数流派众多,四化与星耀亮度是分歧最集中的两处。这里按 key **整表替换**
486/// 默认值:给出某个天干的四化就只改那个天干,未给出的天干仍用默认表;亮度同理。
487///
488/// 通过 [`Config::with_mutagens`] / [`Config::with_brightness`] 构造。
489#[derive(Debug, Clone, PartialEq, Eq, Default)]
490pub struct TableOverrides {
491    /// 天干 → 该干化出的四颗星,顺序为禄、权、科、忌
492    mutagens: std::collections::HashMap<HeavenlyStem, [crate::data::stars::StarKey; 4]>,
493    /// 星耀 → 它在十二宫(寅宫为 0)各自的亮度,无亮度的位置为 None
494    brightness: std::collections::HashMap<crate::data::stars::StarKey, [Option<Brightness>; 12]>,
495}
496
497impl TableOverrides {
498    /// 覆盖某个天干的四化表。
499    pub fn set_mutagens(&mut self, stem: HeavenlyStem, stars: [crate::data::stars::StarKey; 4]) {
500        self.mutagens.insert(stem, stars);
501    }
502
503    /// 覆盖某颗星的十二宫亮度表。
504    pub fn set_brightness(
505        &mut self,
506        star: crate::data::stars::StarKey,
507        table: [Option<Brightness>; 12],
508    ) {
509        self.brightness.insert(star, table);
510    }
511
512    /// 取该天干被覆盖的四化表;未覆盖时返回 `None`。
513    pub fn mutagens_of(&self, stem: HeavenlyStem) -> Option<&[crate::data::stars::StarKey; 4]> {
514        self.mutagens.get(&stem)
515    }
516
517    /// 取该星被覆盖的亮度表;未覆盖时返回 `None`。
518    pub fn brightness_of(
519        &self,
520        star: crate::data::stars::StarKey,
521    ) -> Option<&[Option<Brightness>; 12]> {
522        self.brightness.get(&star)
523    }
524
525    /// 是否没有任何覆盖。
526    pub fn is_empty(&self) -> bool {
527        self.mutagens.is_empty() && self.brightness.is_empty()
528    }
529}
530
531/// 排盘配置:控制分界点、算法派别与自定义表的全部开关
532#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
533pub struct Config {
534    /// 年分界点
535    pub year_divide: YearDivide,
536    /// 运限分界点
537    pub horoscope_divide: HoroscopeDivide,
538    /// 虚岁分界点
539    pub age_divide: AgeDivide,
540    /// 晚子时归属
541    pub day_divide: DayDivide,
542    /// 算法派别
543    pub algorithm: Algorithm,
544    /// 排盘视角(天盘/地盘/人盘)
545    pub astro_type: AstroType,
546    /// 自定义四化与亮度表;`None` 表示全部使用默认表。
547    ///
548    /// 不参与序列化:它是排盘的输入配置,不属于排盘结果,
549    /// 加入 DTO 会破坏与 JS iztro 的字段契约。
550    #[serde(skip)]
551    pub overrides: Option<std::sync::Arc<TableOverrides>>,
552}
553
554impl Default for Config {
555    /// 与 JS iztro 的默认配置一致
556    fn default() -> Self {
557        Config {
558            year_divide: YearDivide::Normal,
559            horoscope_divide: HoroscopeDivide::Normal,
560            age_divide: AgeDivide::Normal,
561            day_divide: DayDivide::Forward,
562            algorithm: Algorithm::Default,
563            astro_type: AstroType::Heaven,
564            overrides: None,
565        }
566    }
567}
568
569impl Config {
570    /// 在当前配置上指定排盘视角。
571    pub fn with_astro_type(mut self, astro_type: AstroType) -> Self {
572        self.astro_type = astro_type;
573        self
574    }
575
576    /// 在当前配置上覆盖某个天干的四化表。
577    pub fn with_mutagens(
578        mut self,
579        stem: HeavenlyStem,
580        stars: [crate::data::stars::StarKey; 4],
581    ) -> Self {
582        let mut tables = self
583            .overrides
584            .take()
585            .map_or_else(TableOverrides::default, |arc| {
586                std::sync::Arc::try_unwrap(arc).unwrap_or_else(|shared| (*shared).clone())
587            });
588        tables.set_mutagens(stem, stars);
589        self.overrides = Some(std::sync::Arc::new(tables));
590        self
591    }
592
593    /// 在当前配置上覆盖某颗星的十二宫亮度表。
594    pub fn with_brightness(
595        mut self,
596        star: crate::data::stars::StarKey,
597        table: [Option<Brightness>; 12],
598    ) -> Self {
599        let mut tables = self
600            .overrides
601            .take()
602            .map_or_else(TableOverrides::default, |arc| {
603                std::sync::Arc::try_unwrap(arc).unwrap_or_else(|shared| (*shared).clone())
604            });
605        tables.set_brightness(star, table);
606        self.overrides = Some(std::sync::Arc::new(tables));
607        self
608    }
609
610    /// 该天干实际生效的四化表:有覆盖用覆盖,否则用默认表。
611    pub fn mutagens_of(&self, stem: HeavenlyStem) -> [crate::data::stars::StarKey; 4] {
612        self.overrides
613            .as_ref()
614            .and_then(|t| t.mutagens_of(stem))
615            .copied()
616            .unwrap_or_else(|| crate::data::heavenly_stems::get_heavenly_stem_info(stem).mutagen)
617    }
618
619    /// 该星在指定宫位实际生效的亮度:有覆盖用覆盖,否则用默认表。
620    ///
621    /// `palace_index` 为盘上位置(寅宫为 0),越界会对 12 取模。
622    pub fn brightness_of(
623        &self,
624        star: crate::data::stars::StarKey,
625        palace_index: usize,
626    ) -> Option<Brightness> {
627        let index = palace_index % 12;
628        if let Some(table) = self.overrides.as_ref().and_then(|t| t.brightness_of(star)) {
629            return table[index];
630        }
631        crate::data::stars::get_brightness_table(star)?[index]
632    }
633}
634
635/// 时辰索引
636pub type TimeIndex = u8;
637
638impl HeavenlyStem {
639    /// 天干序号(甲=0 … 癸=9)
640    pub fn index(&self) -> usize {
641        *self as usize
642    }
643    /// 由序号取天干(对 10 取模)
644    pub fn from_index(i: usize) -> Self {
645        crate::data::constants::HEAVENLY_STEMS[i % 10]
646    }
647}
648
649impl EarthlyBranch {
650    /// 地支序号(子=0 … 亥=11)
651    pub fn index(&self) -> usize {
652        *self as usize
653    }
654    /// 由序号取地支(对 12 取模)
655    pub fn from_index(i: usize) -> Self {
656        crate::data::constants::EARTHLY_BRANCHES[i % 12]
657    }
658}
659
660impl Palace {
661    /// 宫位在 [`crate::data::constants::PALACES`] 中的序号
662    pub fn index(&self) -> usize {
663        *self as usize
664    }
665    /// 由序号取宫位名(对 12 取模)
666    pub fn from_index(i: usize) -> Self {
667        crate::data::constants::PALACES[i % 12]
668    }
669}
670
671impl FiveElementsClass {
672    /// 五行局数值(水二局=2 … 火六局=6)
673    pub fn value(&self) -> usize {
674        *self as usize
675    }
676}
677
678impl Palace {
679    /// 语言无关的宫位标识(iztro i18n key,如 "soulPalace")。
680    pub fn as_key(&self) -> &'static str {
681        match self {
682            Palace::Soul => "soulPalace",
683            Palace::Parents => "parentsPalace",
684            Palace::Spirit => "spiritPalace",
685            Palace::Property => "propertyPalace",
686            Palace::Career => "careerPalace",
687            Palace::Friends => "friendsPalace",
688            Palace::Surface => "surfacePalace",
689            Palace::Health => "healthPalace",
690            Palace::Wealth => "wealthPalace",
691            Palace::Children => "childrenPalace",
692            Palace::Spouse => "spousePalace",
693            Palace::Siblings => "siblingsPalace",
694        }
695    }
696}
697
698impl HeavenlyStem {
699    /// 语言无关的天干标识(iztro i18n key,如 "jiaHeavenly")。
700    pub fn as_key(&self) -> &'static str {
701        match self {
702            HeavenlyStem::Jia => "jiaHeavenly",
703            HeavenlyStem::Yi => "yiHeavenly",
704            HeavenlyStem::Bing => "bingHeavenly",
705            HeavenlyStem::Ding => "dingHeavenly",
706            HeavenlyStem::Wu => "wuHeavenly",
707            HeavenlyStem::Ji => "jiHeavenly",
708            HeavenlyStem::Geng => "gengHeavenly",
709            HeavenlyStem::Xin => "xinHeavenly",
710            HeavenlyStem::Ren => "renHeavenly",
711            HeavenlyStem::Gui => "guiHeavenly",
712        }
713    }
714}
715
716impl EarthlyBranch {
717    /// 语言无关的地支标识(iztro i18n key,如 "ziEarthly")。
718    pub fn as_key(&self) -> &'static str {
719        match self {
720            EarthlyBranch::Zi => "ziEarthly",
721            EarthlyBranch::Chou => "chouEarthly",
722            EarthlyBranch::Yin => "yinEarthly",
723            EarthlyBranch::Mao => "maoEarthly",
724            EarthlyBranch::Chen => "chenEarthly",
725            EarthlyBranch::Si => "siEarthly",
726            EarthlyBranch::Wu => "wuEarthly",
727            EarthlyBranch::Wei => "weiEarthly",
728            EarthlyBranch::Shen => "shenEarthly",
729            EarthlyBranch::You => "youEarthly",
730            EarthlyBranch::Xu => "xuEarthly",
731            EarthlyBranch::Hai => "haiEarthly",
732        }
733    }
734}
735
736impl Mutagen {
737    /// 语言无关的四化标识(iztro i18n key,如 "sihuaLu")。
738    pub fn as_key(&self) -> &'static str {
739        match self {
740            Mutagen::Lu => "sihuaLu",
741            Mutagen::Quan => "sihuaQuan",
742            Mutagen::Ke => "sihuaKe",
743            Mutagen::Ji => "sihuaJi",
744        }
745    }
746}
747
748impl Brightness {
749    /// 语言无关的亮度标识(iztro i18n key,如 "miao")。
750    pub fn as_key(&self) -> &'static str {
751        match self {
752            Brightness::Miao => "miao",
753            Brightness::Wang => "wang",
754            Brightness::De => "de",
755            Brightness::Li => "li",
756            Brightness::Ping => "ping",
757            Brightness::Bu => "bu",
758            Brightness::Xian => "xian",
759        }
760    }
761}
762
763impl FiveElementsClass {
764    /// 语言无关的五行局标识("water2nd" 等)。
765    pub fn as_key(&self) -> &'static str {
766        match self {
767            FiveElementsClass::Water2nd => "water2nd",
768            FiveElementsClass::Wood3rd => "wood3rd",
769            FiveElementsClass::Metal4th => "metal4th",
770            FiveElementsClass::Earth5th => "earth5th",
771            FiveElementsClass::Fire6th => "fire6th",
772        }
773    }
774}
775
776impl Palace {
777    /// 由语言无关标识反查宫位名;标识未知时返回 `None`。
778    pub fn from_key(key: &str) -> Option<Palace> {
779        match key {
780            "soulPalace" => Some(Palace::Soul),
781            "parentsPalace" => Some(Palace::Parents),
782            "spiritPalace" => Some(Palace::Spirit),
783            "propertyPalace" => Some(Palace::Property),
784            "careerPalace" => Some(Palace::Career),
785            "friendsPalace" => Some(Palace::Friends),
786            "surfacePalace" => Some(Palace::Surface),
787            "healthPalace" => Some(Palace::Health),
788            "wealthPalace" => Some(Palace::Wealth),
789            "childrenPalace" => Some(Palace::Children),
790            "spousePalace" => Some(Palace::Spouse),
791            "siblingsPalace" => Some(Palace::Siblings),
792            _ => None,
793        }
794    }
795}
796
797impl HeavenlyStem {
798    /// 由语言无关标识反查天干;标识未知时返回 `None`。
799    pub fn from_key(key: &str) -> Option<HeavenlyStem> {
800        match key {
801            "jiaHeavenly" => Some(HeavenlyStem::Jia),
802            "yiHeavenly" => Some(HeavenlyStem::Yi),
803            "bingHeavenly" => Some(HeavenlyStem::Bing),
804            "dingHeavenly" => Some(HeavenlyStem::Ding),
805            "wuHeavenly" => Some(HeavenlyStem::Wu),
806            "jiHeavenly" => Some(HeavenlyStem::Ji),
807            "gengHeavenly" => Some(HeavenlyStem::Geng),
808            "xinHeavenly" => Some(HeavenlyStem::Xin),
809            "renHeavenly" => Some(HeavenlyStem::Ren),
810            "guiHeavenly" => Some(HeavenlyStem::Gui),
811            _ => None,
812        }
813    }
814}
815
816impl EarthlyBranch {
817    /// 由语言无关标识反查地支;标识未知时返回 `None`。
818    pub fn from_key(key: &str) -> Option<EarthlyBranch> {
819        match key {
820            "ziEarthly" => Some(EarthlyBranch::Zi),
821            "chouEarthly" => Some(EarthlyBranch::Chou),
822            "yinEarthly" => Some(EarthlyBranch::Yin),
823            "maoEarthly" => Some(EarthlyBranch::Mao),
824            "chenEarthly" => Some(EarthlyBranch::Chen),
825            "siEarthly" => Some(EarthlyBranch::Si),
826            "wuEarthly" => Some(EarthlyBranch::Wu),
827            "weiEarthly" => Some(EarthlyBranch::Wei),
828            "shenEarthly" => Some(EarthlyBranch::Shen),
829            "youEarthly" => Some(EarthlyBranch::You),
830            "xuEarthly" => Some(EarthlyBranch::Xu),
831            "haiEarthly" => Some(EarthlyBranch::Hai),
832            _ => None,
833        }
834    }
835}
836
837impl Mutagen {
838    /// 由语言无关标识反查四化;标识未知时返回 `None`。
839    pub fn from_key(key: &str) -> Option<Mutagen> {
840        match key {
841            "sihuaLu" => Some(Mutagen::Lu),
842            "sihuaQuan" => Some(Mutagen::Quan),
843            "sihuaKe" => Some(Mutagen::Ke),
844            "sihuaJi" => Some(Mutagen::Ji),
845            _ => None,
846        }
847    }
848}
849
850impl Brightness {
851    /// 由语言无关标识反查亮度;标识未知时返回 `None`。
852    pub fn from_key(key: &str) -> Option<Brightness> {
853        match key {
854            "miao" => Some(Brightness::Miao),
855            "wang" => Some(Brightness::Wang),
856            "de" => Some(Brightness::De),
857            "li" => Some(Brightness::Li),
858            "ping" => Some(Brightness::Ping),
859            "bu" => Some(Brightness::Bu),
860            "xian" => Some(Brightness::Xian),
861            _ => None,
862        }
863    }
864}
865
866impl FiveElementsClass {
867    /// 由语言无关标识反查五行局;标识未知时返回 `None`。
868    pub fn from_key(key: &str) -> Option<FiveElementsClass> {
869        match key {
870            "water2nd" => Some(FiveElementsClass::Water2nd),
871            "wood3rd" => Some(FiveElementsClass::Wood3rd),
872            "metal4th" => Some(FiveElementsClass::Metal4th),
873            "earth5th" => Some(FiveElementsClass::Earth5th),
874            "fire6th" => Some(FiveElementsClass::Fire6th),
875            _ => None,
876        }
877    }
878}
879
880#[cfg(test)]
881mod key_roundtrip_tests {
882    use super::*;
883    use crate::astro::builder::by_solar;
884    use crate::data::constants::{EARTHLY_BRANCHES, HEAVENLY_STEMS, PALACES};
885    use crate::data::stars::StarKey;
886
887    /// 语言代码大小写与连字符/下划线写法都能还原,`as_code` 与 `from_code` 互逆。
888    #[test]
889    fn test_language_code_aliases() {
890        for lang in [
891            Language::ZhCN,
892            Language::ZhTW,
893            Language::EnUS,
894            Language::JaJP,
895            Language::KoKR,
896            Language::ViVN,
897        ] {
898            let code = lang.as_code();
899            assert_eq!(Language::from_code(code), Some(lang));
900            assert_eq!(Language::from_code(&code.to_lowercase()), Some(lang));
901            assert_eq!(Language::from_code(&code.replace('-', "_")), Some(lang));
902        }
903        assert_eq!(Language::from_code("zh_cn"), Some(Language::ZhCN));
904        assert_eq!(Language::from_code("klingon"), None);
905    }
906
907    /// `as_key` 与 `from_key` 必须互为逆运算,否则绑定层的 key 往返会失真。
908    #[test]
909    fn test_enum_key_roundtrip() {
910        for p in PALACES {
911            assert_eq!(Palace::from_key(p.as_key()), Some(p), "{p:?}");
912        }
913        for s in HEAVENLY_STEMS {
914            assert_eq!(HeavenlyStem::from_key(s.as_key()), Some(s), "{s:?}");
915        }
916        for b in EARTHLY_BRANCHES {
917            assert_eq!(EarthlyBranch::from_key(b.as_key()), Some(b), "{b:?}");
918        }
919        for m in [Mutagen::Lu, Mutagen::Quan, Mutagen::Ke, Mutagen::Ji] {
920            assert_eq!(Mutagen::from_key(m.as_key()), Some(m), "{m:?}");
921        }
922        for b in [
923            Brightness::Miao,
924            Brightness::Wang,
925            Brightness::De,
926            Brightness::Li,
927            Brightness::Ping,
928            Brightness::Bu,
929            Brightness::Xian,
930        ] {
931            assert_eq!(Brightness::from_key(b.as_key()), Some(b), "{b:?}");
932        }
933        for c in [
934            FiveElementsClass::Water2nd,
935            FiveElementsClass::Wood3rd,
936            FiveElementsClass::Metal4th,
937            FiveElementsClass::Earth5th,
938            FiveElementsClass::Fire6th,
939        ] {
940            assert_eq!(FiveElementsClass::from_key(c.as_key()), Some(c), "{c:?}");
941        }
942
943        for v in [YearDivide::Normal, YearDivide::Exact] {
944            assert_eq!(YearDivide::from_key(v.as_key()), Some(v), "{v:?}");
945        }
946        for v in [HoroscopeDivide::Normal, HoroscopeDivide::Exact] {
947            assert_eq!(HoroscopeDivide::from_key(v.as_key()), Some(v), "{v:?}");
948        }
949        for v in [AgeDivide::Normal, AgeDivide::Birthday] {
950            assert_eq!(AgeDivide::from_key(v.as_key()), Some(v), "{v:?}");
951        }
952        for v in [DayDivide::Forward, DayDivide::Current] {
953            assert_eq!(DayDivide::from_key(v.as_key()), Some(v), "{v:?}");
954        }
955        for v in [Algorithm::Default, Algorithm::Zhongzhou] {
956            assert_eq!(Algorithm::from_key(v.as_key()), Some(v), "{v:?}");
957        }
958        for v in [AstroType::Heaven, AstroType::Earth, AstroType::Human] {
959            assert_eq!(AstroType::from_key(v.as_key()), Some(v), "{v:?}");
960        }
961        for v in [LeapMonth::NotLeap, LeapMonth::Leap, LeapMonth::LeapFixed] {
962            assert_eq!(LeapMonth::from_key(v.as_key()), Some(v), "{v:?}");
963            assert_eq!(
964                LeapMonth::from_flags(v.is_leap_month(), v.fix_leap()),
965                v,
966                "{v:?}"
967            );
968        }
969        assert_eq!(LeapMonth::from_flags(false, true), LeapMonth::NotLeap);
970
971        assert_eq!(Palace::from_key("bodyPalace"), None);
972        assert_eq!(StarKey::from_key("nope"), None);
973        assert_eq!(Algorithm::from_key("normal"), None);
974    }
975
976    /// 盘上出现的每一颗星(含运限流耀)都要能由 key 还原。
977    #[test]
978    fn test_star_key_roundtrip_over_chart() {
979        let chart = by_solar(
980            "2000-8-16",
981            2,
982            Gender::Female,
983            true,
984            Language::ZhCN,
985            Config::default(),
986        )
987        .unwrap();
988
989        let mut checked = 0;
990        for palace in &chart.palaces {
991            for star in palace
992                .major_stars
993                .iter()
994                .chain(palace.minor_stars.iter())
995                .chain(palace.adjective_stars.iter())
996            {
997                assert_eq!(
998                    StarKey::from_key(star.key.as_key()),
999                    Some(star.key),
1000                    "{:?} 的 key 往返失败",
1001                    star.key
1002                );
1003                checked += 1;
1004            }
1005            assert_eq!(
1006                StarKey::from_key(palace.changsheng12.as_key()),
1007                Some(palace.changsheng12)
1008            );
1009            assert_eq!(
1010                StarKey::from_key(palace.suiqian12.as_key()),
1011                Some(palace.suiqian12)
1012            );
1013        }
1014        assert!(checked > 60, "覆盖的星耀太少:{checked}");
1015    }
1016}