Skip to main content

mingli_contract/
query.rs

1//! 一次排盘的输入:时刻与它周围那些可缺省的原子。
2//!
3//! [`Query`] 是全叶共享的那一份;[`AskTime`] 是只带时间维的简化版;
4//! [`QueryKind`] 把输入按问局归类,携带各类各自需要的原子。
5//! 取机种子与主体类型也在这里——它们是输入,不是某一层的私产。
6
7use crate::{Intent, Moment};
8use serde::Serialize;
9use std::collections::BTreeMap;
10
11/// 性别(用于需要它的叶,如八字大运)。
12///
13/// 线上一律小写。这个枚举原本按 Rust 的拼法收发,于是凡是直接把 [`Query`] 从 JSON 解出来的
14/// 地方只认 `"Male"`,而各叶盘里回声出去的 `input.gender` 写的是 `"male"`——同一个词在
15/// 同一套契约里有两种拼法,写错的那一头会被拒。`Male` / `Female` 与 `男` / `女`
16/// 都以别名接受,旧调用不破。
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum Gender {
20    /// 男。
21    #[serde(alias = "Male", alias = "男")]
22    Male,
23    /// 女。
24    #[serde(alias = "Female", alias = "女")]
25    Female,
26}
27
28/// 排盘查询(共享输入)。
29
30#[derive(Debug, Clone, Serialize, serde::Deserialize)]
31pub struct Query {
32    /// 公历年。
33    pub year: i32,
34    /// 公历月 1..12。
35    pub month: u32,
36    /// 公历日 1..31。
37    pub day: u32,
38    /// 时 0..23。
39    pub hour: u32,
40    /// 分 0..59。
41    pub minute: u32,
42    /// 时区偏移小时(中国 +8,日本 +9)。
43    pub tz: f64,
44    /// 性别(可选)。
45    pub gender: Option<Gender>,
46    /// 出生地纬度(度,北纬为正;占星 Asc/MC 需要,可选)。
47    pub latitude: Option<f64>,
48    /// 出生地经度(度,东经为正;占星 Asc/MC 需要,可选)。
49    pub longitude: Option<f64>,
50    /// C 族(抽样/起卦)的可复现种子;`None` 时由共享时刻派生(见 [`effective_seed`])。
51    pub seed: Option<u64>,
52    /// 姓名(D 族数字学用,可选)。拉丁字母按 Pythagorean/Chaldean 取值。
53    pub name: Option<String>,
54    /// **流派选择**:key=叶 `id`,value=该叶选定的流派 id(见各叶 `schools()`)。
55    /// 缺省即按各叶 `default = true` 的流派算。同一次 `cast_all` 可为不同叶分别指定。
56    #[serde(default)]
57    pub schools: BTreeMap<String, String>,
58}
59
60impl Query {
61    /// 只带时刻的最小查询:性别 / 坐标 / 种子 / 姓名 / 流派全部缺省。
62    ///
63    /// 需要这些原子的叶自会在缺省下走它的降级路径(如八字不排大运、占星不出 Asc)。
64    #[must_use]
65    pub fn at(year: i32, month: u32, day: u32, hour: u32, minute: u32, tz: f64) -> Self {
66        Self {
67            year,
68            month,
69            day,
70            hour,
71            minute,
72            tz,
73            gender: None,
74            latitude: None,
75            longitude: None,
76            seed: None,
77            name: None,
78            schools: BTreeMap::new(),
79        }
80    }
81
82    /// 取叶 `engine_id` 的流派 id。未指定时返回 `default_id`。
83    #[must_use]
84    pub fn school_of<'a>(&'a self, engine_id: &str, default_id: &'a str) -> &'a str {
85        self.schools.get(engine_id).map_or(default_id, |s| s.as_str())
86    }
87}
88
89/// C 族叶的有效种子:显式 `q.seed` 优先,否则由共享时刻的儒略日比特派生(同一时刻可复现)。
90#[must_use]
91pub fn effective_seed(m: &Moment, q: &Query) -> u64 {
92    q.seed.unwrap_or_else(|| m.jd_ut.to_bits())
93}
94
95// 以下是按问局归类的输入。与 `Query`(本命载荷)平行:`Query` 是「一个时刻加它周围的原子」,
96// `QueryKind` 是「这次问的是哪一类,因而要哪些原子」。哪几片叶答某一类不在这里,见
97// `crate::ports::CastingEngine::answers`。
98
99/// 占测时刻(用于 Event/Election/Locative/Fortune 等问局的「问的此刻」或时窗端点)。
100///
101/// 比 [`Query`] 简化:只携时间维原子,不带性别/坐标/姓名/种子;后者由各意图按需另带。
102#[derive(Debug, Clone, Serialize, serde::Deserialize)]
103pub struct AskTime {
104    /// 公历年。
105    pub year: i32,
106    /// 公历月 1..12。
107    pub month: u32,
108    /// 公历日 1..31。
109    pub day: u32,
110    /// 时 0..23。
111    pub hour: u32,
112    /// 分 0..59。
113    pub minute: u32,
114    /// 时区偏移小时。
115    pub tz: f64,
116}
117
118/// 问局(需求侧)分类,按「时间轴与切面」模型组织。
119///
120/// 每变体携带其所需的**输入原子**;一切意图最终都映射到一组叶(由编排层的 `route` 在运行时定夺)。
121/// `Natal` 直接复用 [`Query`] 作载荷——「一个时刻的切片」要的原子与共享输入恰好相同;
122/// 其余变体各携该类问局的最小输入原子。
123#[derive(Debug, Clone, Serialize, serde::Deserialize)]
124#[serde(tag = "kind", rename_all = "snake_case")]
125pub enum QueryKind {
126    /// **命**:本命盘——一个时刻的静态切片。
127    Natal(Query),
128    /// **运**:本命 + 目标时刻 → 运势/流年/dasha 定位。
129    Fortune {
130        /// 出生切片。
131        natal: Query,
132        /// 目标时刻(流年/大运扫描端点)。
133        t_target: AskTime,
134    },
135    /// **事**:占事(问事此刻 + 取机动作)。
136    Event {
137        /// 问事此刻。
138        t_ask: AskTime,
139        /// 取机种子(摇钱/抽牌/数蓍/random 派生)。
140        seed: u64,
141        /// 问句(只入释义不入算)。
142        q_text: Option<String>,
143    },
144    /// **择**:时窗扫描 + 排序(择吉)。
145    Election {
146        /// 时窗起。
147        window_start: AskTime,
148        /// 时窗止。
149        window_end: AskTime,
150        /// 事类(婚/葬/动土/行/开业…)。
151        category: String,
152    },
153    /// **合**:合盘(N=2 起,合婚/合伙)。
154    Synastry {
155        /// 甲方本命。
156        a: Query,
157        /// 乙方本命。
158        b: Query,
159    },
160    /// **群/国**:政体奠基时刻 → 国运盘。
161    Mundane {
162        /// 政体奠基时刻(立国/开国大典/政权更替)。
163        p_polity: Query,
164    },
165    /// **寻**:取方位(占课为主)。
166    Locative {
167        /// 问事此刻。
168        t_ask: AskTime,
169        /// 取机种子。
170        seed: u64,
171        /// 事类(寻人/寻物/寻方向)。
172        category: String,
173    },
174    /// **号**:字/词模态(姓名笔画/字母值,与时刻无关)。
175    Onomancy {
176        /// 姓名(数字学/gematria/abjad 字母值)。
177        name: String,
178        /// 姓笔画(五格用,可选)。
179        surname_strokes: Option<u32>,
180        /// 名笔画(五格用,可选)。
181        given_strokes: Option<u32>,
182    },
183}
184
185impl QueryKind {
186    /// 取本问局属于哪一类意图。
187    #[must_use]
188    pub fn intent(&self) -> Intent {
189        match self {
190            Self::Natal(_) => Intent::Natal,
191            Self::Fortune { .. } => Intent::Fortune,
192            Self::Event { .. } => Intent::Event,
193            Self::Election { .. } => Intent::Election,
194            Self::Synastry { .. } => Intent::Synastry,
195            Self::Mundane { .. } => Intent::Mundane,
196            Self::Locative { .. } => Intent::Locative,
197            Self::Onomancy { .. } => Intent::Onomancy,
198        }
199    }
200
201    /// 取意图稳定 id。
202    #[must_use]
203    pub fn id(&self) -> &'static str {
204        self.intent().id()
205    }
206}
207
208/// 主体类型:同一套四柱计算给不同主体读出不同象义。
209///
210/// **计算层完全 DET 同源**(干支/五行/十神/旺衰对任何主体一致);
211/// **只解读层换映射**。person 是默认;company/product/event 适配「物有时刻 → 八字」(择日的逆运算)。
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum Subject {
215    /// 人(默认):传统人盘。年=祖根、月=父母青年、日=自身/配偶、时=子女晚年。
216    Person,
217    /// 公司/组织:年=创立根基/行业属性、月=成长环境/团队、日=主体/核心、时=前景/产出。
218    Company,
219    /// 物(有时刻发布的产品/建筑/开张):同公司盘(择日的镜像)。
220    Product,
221    /// 事(已发生事件):用于复盘事的性质与走向。
222    Event,
223}
224
225impl Subject {
226    /// 从字符串解析(`"person"/"company"/"product"/"event"`)。
227    ///
228    /// 首字母大写与中文都收,与 [`Gender`] 的别名一致——同一份请求里
229    /// `gender` 收 `"Male"` 而 `subject` 不收 `"Person"`,宽严不一只会让人踩坑。
230    /// 认不出的一律返回 `None`,由调用方拒绝,**不当成没写**。
231    #[must_use]
232    pub fn from_str_opt(s: &str) -> Option<Self> {
233        match s {
234            "person" | "Person" | "人" => Some(Self::Person),
235            "company" | "Company" | "公司" => Some(Self::Company),
236            "product" | "Product" | "object" | "Object" | "物" | "产品" => Some(Self::Product),
237            "event" | "Event" | "事" => Some(Self::Event),
238            _ => None,
239        }
240    }
241    /// 中文展示名。
242    #[must_use]
243    pub fn cn(self) -> &'static str {
244        match self {
245            Self::Person => "人",
246            Self::Company => "公司/组织",
247            Self::Product => "物/产品",
248            Self::Event => "事/事件",
249        }
250    }
251}