iztro 0.6.0

Strongly typed Zi Wei Dou Shu chart generation aligned with iztro.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
use crate::core::{
    error::ChartError,
    model::{
        bureau::FiveElementBureau,
        calendar::BirthContext,
        chart::palace::PalaceName,
        chart::snapshot::ChartStackSnapshot,
        profile::MethodProfile,
        star::mutagen::{Mutagen, Scope},
        star::{
            Brightness, KnownStarFamily, StarCategory, StarKind, StarName, try_known_star_metadata,
        },
    },
};
use lunar_lite::{EarthlyBranch, HeavenlyStem, StemBranch};
use serde::{Deserialize, Deserializer, Serialize};

/// Number of palaces required for a complete chart.
pub const PALACE_COUNT: usize = 12;

/// A complete chart placeholder composed of deterministic chart facts.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Chart {
    birth_context: BirthContext,
    birth_year: StemBranch,
    method_profile: MethodProfile,
    palaces: Vec<Palace>,
    body_palace_branch: Option<EarthlyBranch>,
    five_element_bureau: Option<FiveElementBureau>,
}

impl Chart {
    /// Creates a chart from typed chart facts after checking core invariants.
    pub fn try_new(
        birth_context: BirthContext,
        birth_year: StemBranch,
        method_profile: MethodProfile,
        palaces: Vec<Palace>,
        body_palace_branch: Option<EarthlyBranch>,
        five_element_bureau: Option<FiveElementBureau>,
    ) -> Result<Self, ChartError> {
        if palaces.len() != PALACE_COUNT {
            return Err(ChartError::InvalidPalaceCount {
                expected: PALACE_COUNT,
                actual: palaces.len(),
            });
        }

        Ok(Self {
            birth_context,
            birth_year,
            method_profile,
            palaces,
            body_palace_branch,
            five_element_bureau,
        })
    }

    /// Returns the birth context used by this chart.
    pub const fn birth_context(&self) -> &BirthContext {
        &self.birth_context
    }

    /// Returns the birth-year stem-branch used for natal chart derivation.
    pub const fn birth_year(&self) -> StemBranch {
        self.birth_year
    }

    /// Returns the method profile metadata.
    pub const fn method_profile(&self) -> &MethodProfile {
        &self.method_profile
    }

    /// Returns the palaces in this chart.
    pub fn palaces(&self) -> &[Palace] {
        &self.palaces
    }

    /// Returns an owned renderer-neutral stack snapshot of this natal chart.
    pub fn stack_snapshot(&self) -> ChartStackSnapshot {
        ChartStackSnapshot::from_natal_chart(self)
    }

    /// Returns the branch containing the Body Palace, if known.
    pub const fn body_palace_branch(&self) -> Option<EarthlyBranch> {
        self.body_palace_branch
    }

    /// Returns whether the given branch is the Body Palace branch.
    pub fn is_body_palace_branch(&self, branch: EarthlyBranch) -> bool {
        self.body_palace_branch == Some(branch)
    }

    /// Returns the palace containing the Body Palace, if known.
    pub fn body_palace(&self) -> Option<&Palace> {
        let body_branch = self.body_palace_branch?;

        self.palaces
            .iter()
            .find(|palace| palace.branch() == body_branch)
    }

    /// Returns the five-element bureau (五行局), if calculated.
    pub const fn five_element_bureau(&self) -> Option<FiveElementBureau> {
        self.five_element_bureau
    }

    /// Returns the Life Palace, identified by [`PalaceName::Life`], if present.
    pub fn life_palace(&self) -> Option<&Palace> {
        self.palaces
            .iter()
            .find(|palace| palace.name() == PalaceName::Life)
    }

    /// Returns all major-star placements with their palace context.
    pub fn major_stars(&self) -> Vec<MajorStarPlacementRef<'_>> {
        self.stars_by_category(StarCategory::Major)
    }

    /// Returns one major-star placement with palace context.
    pub fn major_star(&self, name: StarName) -> Option<MajorStarPlacementRef<'_>> {
        self.star(name)
            .filter(|fact| fact.placement().category() == StarCategory::Major)
    }

    /// Returns the palace containing a major star, if present.
    pub fn palace_by_major_star(&self, name: StarName) -> Option<&Palace> {
        self.major_star(name).map(|fact| fact.palace())
    }

    /// Returns major-star placements in a palace name.
    pub fn major_stars_in_palace(&self, name: PalaceName) -> Vec<MajorStarPlacementRef<'_>> {
        self.stars_in_palace(name)
            .into_iter()
            .filter(|fact| fact.placement().category() == StarCategory::Major)
            .collect()
    }

    /// Returns major-star placements in an Earthly Branch.
    pub fn major_stars_in_branch(&self, branch: EarthlyBranch) -> Vec<MajorStarPlacementRef<'_>> {
        self.stars_in_branch(branch)
            .into_iter()
            .filter(|fact| fact.placement().category() == StarCategory::Major)
            .collect()
    }

    /// Returns all star placements with their palace context.
    pub fn stars(&self) -> Vec<StarPlacementRef<'_>> {
        self.palaces.iter().flat_map(stars_in).collect()
    }

    /// Returns one star placement with palace context.
    pub fn star(&self, name: StarName) -> Option<StarPlacementRef<'_>> {
        self.palaces.iter().find_map(|palace| {
            palace
                .stars()
                .iter()
                .find(|star| star.name() == name)
                .map(|placement| StarPlacementRef::new(palace, placement))
        })
    }

    /// Returns the palace containing a star, if present.
    pub fn palace_containing_star(&self, name: StarName) -> Option<&Palace> {
        self.star(name).map(|fact| fact.palace())
    }

    /// Returns star placements in a palace name.
    pub fn stars_in_palace(&self, name: PalaceName) -> Vec<StarPlacementRef<'_>> {
        self.palaces
            .iter()
            .filter(|palace| palace.name() == name)
            .flat_map(stars_in)
            .collect()
    }

    /// Returns star placements in an Earthly Branch.
    pub fn stars_in_branch(&self, branch: EarthlyBranch) -> Vec<StarPlacementRef<'_>> {
        self.palaces
            .iter()
            .filter(|palace| palace.branch() == branch)
            .flat_map(stars_in)
            .collect()
    }

    /// Returns star placements in a coarse category.
    pub fn stars_by_category(&self, category: StarCategory) -> Vec<StarPlacementRef<'_>> {
        self.stars()
            .into_iter()
            .filter(|fact| fact.placement().category() == category)
            .collect()
    }

    /// Returns star placements in an iztro-compatible fine kind.
    pub fn stars_by_kind(&self, kind: StarKind) -> Vec<StarPlacementRef<'_>> {
        self.stars()
            .into_iter()
            .filter(|fact| fact.placement().kind() == kind)
            .collect()
    }

    /// Returns all decorative (untyped) star placements with palace context.
    ///
    /// Decorative entries are a separate fact surface from typed
    /// [`StarPlacement`]s: they never appear in [`Chart::stars`].
    pub fn decorative_stars(&self) -> Vec<DecorativeStarPlacementRef<'_>> {
        self.palaces.iter().flat_map(decorative_stars_in).collect()
    }

    /// Returns one decorative star placement with palace context.
    pub fn decorative_star(&self, name: StarName) -> Option<DecorativeStarPlacementRef<'_>> {
        self.palaces.iter().find_map(|palace| {
            palace
                .decorative_stars()
                .iter()
                .find(|star| star.name() == name)
                .map(|placement| DecorativeStarPlacementRef::new(palace, placement))
        })
    }
}

fn stars_in(palace: &Palace) -> impl Iterator<Item = StarPlacementRef<'_>> {
    palace
        .stars()
        .iter()
        .map(|placement| StarPlacementRef::new(palace, placement))
}

fn decorative_stars_in(palace: &Palace) -> impl Iterator<Item = DecorativeStarPlacementRef<'_>> {
    palace
        .decorative_stars()
        .iter()
        .map(|placement| DecorativeStarPlacementRef::new(palace, placement))
}

/// A borrowed star placement together with the palace containing it.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StarPlacementRef<'a> {
    palace: &'a Palace,
    placement: &'a StarPlacement,
}

impl<'a> StarPlacementRef<'a> {
    /// Creates a borrowed star placement fact with palace context.
    pub const fn new(palace: &'a Palace, placement: &'a StarPlacement) -> Self {
        Self { palace, placement }
    }

    /// Returns the palace containing this star.
    pub const fn palace(&self) -> &'a Palace {
        self.palace
    }

    /// Returns the star placement.
    pub const fn placement(&self) -> &'a StarPlacement {
        self.placement
    }
}

/// A borrowed major-star placement together with the palace containing it.
pub type MajorStarPlacementRef<'a> = StarPlacementRef<'a>;

/// A borrowed decorative star placement together with the palace containing it.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DecorativeStarPlacementRef<'a> {
    palace: &'a Palace,
    placement: &'a DecorativeStarPlacement,
}

impl<'a> DecorativeStarPlacementRef<'a> {
    /// Creates a borrowed decorative star placement fact with palace context.
    pub const fn new(palace: &'a Palace, placement: &'a DecorativeStarPlacement) -> Self {
        Self { palace, placement }
    }

    /// Returns the palace containing this decorative star.
    pub const fn palace(&self) -> &'a Palace {
        self.palace
    }

    /// Returns the decorative star placement.
    pub const fn placement(&self) -> &'a DecorativeStarPlacement {
        self.placement
    }

    /// Returns the branch of the palace containing this decorative star.
    pub const fn branch(&self) -> EarthlyBranch {
        self.palace.branch()
    }

    /// Returns the decorative star name.
    pub const fn name(&self) -> StarName {
        self.placement.name()
    }
}

/// A palace with its branch, stem, and star placements.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Palace {
    name: PalaceName,
    branch: EarthlyBranch,
    stem: HeavenlyStem,
    stars: Vec<StarPlacement>,
    /// Untyped decorative runtime entries (长生/博士/岁前/将前十二神). Skipped when
    /// empty so charts without decorative placement serialize unchanged.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    decorative_stars: Vec<DecorativeStarPlacement>,
}

impl Palace {
    /// Creates a palace fact container with no decorative entries.
    pub fn new(
        name: PalaceName,
        branch: EarthlyBranch,
        stem: HeavenlyStem,
        stars: Vec<StarPlacement>,
    ) -> Self {
        Self {
            name,
            branch,
            stem,
            stars,
            decorative_stars: Vec::new(),
        }
    }

    /// Returns this palace with its decorative star placements replaced.
    ///
    /// Decorative entries are a separate fact surface from typed [`StarPlacement`]s
    /// and never alter [`Palace::stars`].
    pub fn with_decorative_stars(mut self, decorative_stars: Vec<DecorativeStarPlacement>) -> Self {
        self.decorative_stars = decorative_stars;
        self
    }

    /// Returns the palace name.
    pub const fn name(&self) -> PalaceName {
        self.name
    }

    /// Returns the palace branch.
    pub const fn branch(&self) -> EarthlyBranch {
        self.branch
    }

    /// Returns the palace stem.
    pub const fn stem(&self) -> HeavenlyStem {
        self.stem
    }

    /// Returns typed stars placed in this palace.
    pub fn stars(&self) -> &[StarPlacement] {
        &self.stars
    }

    /// Returns decorative (untyped) star placements in this palace.
    pub fn decorative_stars(&self) -> &[DecorativeStarPlacement] {
        &self.decorative_stars
    }
}

/// A star placement within a palace.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct StarPlacement {
    name: StarName,
    kind: StarKind,
    brightness: Brightness,
    mutagen: Option<Mutagen>,
    scope: Scope,
}

impl StarPlacement {
    /// Creates a typed star placement fact.
    pub const fn new(
        name: StarName,
        kind: StarKind,
        brightness: Brightness,
        mutagen: Option<Mutagen>,
        scope: Scope,
    ) -> Self {
        Self {
            name,
            kind,
            brightness,
            mutagen,
            scope,
        }
    }

    /// Returns the star name.
    pub const fn name(&self) -> StarName {
        self.name
    }

    /// Returns the iztro-compatible fine star type.
    pub const fn kind(&self) -> StarKind {
        self.kind
    }

    /// Returns the coarse palace grouping.
    pub const fn category(&self) -> StarCategory {
        self.kind.category()
    }

    /// Returns the star brightness.
    pub const fn brightness(&self) -> Brightness {
        self.brightness
    }

    /// Returns the optional mutagen attached to this placement.
    pub const fn mutagen(&self) -> Option<Mutagen> {
        self.mutagen
    }

    /// Returns the scope of this placement.
    pub const fn scope(&self) -> Scope {
        self.scope
    }
}

/// One of the four untyped "twelve gods" runtime star families.
///
/// These families have no concrete [`StarKind`] upstream, so their entries are
/// modelled as [`DecorativeStarPlacement`]s rather than typed [`StarPlacement`]s.
///
/// The derived [`Ord`]/[`PartialOrd`] follow the variant declaration order and
/// exist only to give facade/export snapshots a stable, deterministic
/// decorative-star ordering key (see
/// [`crate::core::model::chart::facade_snapshot`]). They do not affect placement.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecorativeStarFamily {
    /// 长生十二神 (Changsheng twelve phases).
    Changsheng12,
    /// 博士十二神 (Boshi twelve gods).
    Boshi12,
    /// 岁前十二神 (Suiqian twelve gods).
    Suiqian12,
    /// 将前十二神 (Jiangqian twelve gods).
    Jiangqian12,
}

impl DecorativeStarFamily {
    /// Returns the broad runtime-inventory family for this decorative family.
    pub const fn known_family(self) -> KnownStarFamily {
        match self {
            Self::Changsheng12 => KnownStarFamily::Changsheng12,
            Self::Boshi12 => KnownStarFamily::Boshi12,
            Self::Suiqian12 => KnownStarFamily::Suiqian12,
            Self::Jiangqian12 => KnownStarFamily::Jiangqian12,
        }
    }

    /// Returns the decorative family for a runtime-inventory family, if it is one
    /// of the four decorative "twelve gods" families.
    pub const fn from_known_family(family: KnownStarFamily) -> Option<Self> {
        match family {
            KnownStarFamily::Changsheng12 => Some(Self::Changsheng12),
            KnownStarFamily::Boshi12 => Some(Self::Boshi12),
            KnownStarFamily::Suiqian12 => Some(Self::Suiqian12),
            KnownStarFamily::Jiangqian12 => Some(Self::Jiangqian12),
            _ => None,
        }
    }
}

/// An untyped decorative star placement within a palace.
///
/// Unlike [`StarPlacement`], decorative entries carry no [`StarKind`]: upstream
/// iztro emits them as bare names. The [`DecorativeStarPlacement::try_new`]
/// constructor validates that the name is a known decorative star whose family
/// matches and whose known metadata has no [`StarKind`].
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DecorativeStarPlacement {
    name: StarName,
    family: DecorativeStarFamily,
    scope: Scope,
}

impl DecorativeStarPlacement {
    /// Creates a decorative star placement after validating it against the
    /// known-star inventory.
    ///
    /// Returns [`ChartError::InvalidDecorativeStarPlacement`] when `name` is not a
    /// known star, when its known family differs from `family`, or when its known
    /// metadata carries a [`StarKind`] (i.e. it is a typed star, not decorative).
    pub fn try_new(
        name: StarName,
        family: DecorativeStarFamily,
        scope: Scope,
    ) -> Result<Self, ChartError> {
        let metadata = try_known_star_metadata(name)
            .ok_or(ChartError::InvalidDecorativeStarPlacement { star: name })?;
        if metadata.family() != family.known_family() || metadata.kind().is_some() {
            return Err(ChartError::InvalidDecorativeStarPlacement { star: name });
        }

        Ok(Self {
            name,
            family,
            scope,
        })
    }

    /// Returns the decorative star name.
    pub const fn name(&self) -> StarName {
        self.name
    }

    /// Returns the decorative star family.
    pub const fn family(&self) -> DecorativeStarFamily {
        self.family
    }

    /// Returns the scope of this decorative placement.
    pub const fn scope(&self) -> Scope {
        self.scope
    }
}

impl<'de> Deserialize<'de> for DecorativeStarPlacement {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct DecorativeStarPlacementData {
            name: StarName,
            family: DecorativeStarFamily,
            scope: Scope,
        }

        let data = DecorativeStarPlacementData::deserialize(deserializer)?;
        Self::try_new(data.name, data.family, data.scope).map_err(serde::de::Error::custom)
    }
}