Skip to main content

pleiades_types/
house_systems.rs

1//! House system types: [`HouseSystem`] and [`CustomHouseSystem`].
2
3use core::fmt;
4
5use crate::custom_bodies::{validate_canonical_text, CustomDefinitionValidationError};
6
7/// A built-in or custom house system selection.
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[derive(Clone, Debug, PartialEq, Eq, Hash)]
10#[non_exhaustive]
11pub enum HouseSystem {
12    /// Placidus.
13    Placidus,
14    /// Koch.
15    Koch,
16    /// Porphyry.
17    Porphyry,
18    /// Regiomontanus.
19    Regiomontanus,
20    /// Campanus.
21    Campanus,
22    /// Carter (poli-equatorial) houses.
23    Carter,
24    /// Horizon/Azimuth houses.
25    Horizon,
26    /// APC houses.
27    Apc,
28    /// Krusinski-Pisa-Goelzer houses.
29    KrusinskiPisaGoelzer,
30    /// Equal houses.
31    Equal,
32    /// Equal houses with the Midheaven on cusp 10.
33    EqualMidheaven,
34    /// Equal houses with the first house anchored at 0° Aries.
35    EqualAries,
36    /// Vehlow equal houses, with the Ascendant centered in house 1.
37    Vehlow,
38    /// Sripati houses.
39    Sripati,
40    /// Whole sign houses.
41    WholeSign,
42    /// Alcabitius.
43    Alcabitius,
44    /// Albategnius / Savard-A.
45    Albategnius,
46    /// Pullen sinusoidal delta (Neo-Porphyry).
47    PullenSd,
48    /// Pullen sinusoidal ratio.
49    PullenSr,
50    /// Meridian-style systems.
51    Meridian,
52    /// Axial variants documented by specific software.
53    Axial,
54    /// Topocentric (Polich-Page).
55    Topocentric,
56    /// Morinus.
57    Morinus,
58    /// Sunshine (Bob Makransky / Dieter Treindl family).
59    Sunshine,
60    /// Gauquelin sectors.
61    Gauquelin,
62    /// A custom house system definition.
63    Custom(CustomHouseSystem),
64}
65
66impl fmt::Display for HouseSystem {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            Self::Placidus => f.write_str("Placidus"),
70            Self::Koch => f.write_str("Koch"),
71            Self::Porphyry => f.write_str("Porphyry"),
72            Self::Regiomontanus => f.write_str("Regiomontanus"),
73            Self::Campanus => f.write_str("Campanus"),
74            Self::Carter => f.write_str("Carter (poli-equatorial)"),
75            Self::Horizon => f.write_str("Horizon/Azimuth"),
76            Self::Apc => f.write_str("APC"),
77            Self::KrusinskiPisaGoelzer => f.write_str("Krusinski-Pisa-Goelzer"),
78            Self::Equal => f.write_str("Equal"),
79            Self::EqualMidheaven => f.write_str("Equal (MC)"),
80            Self::EqualAries => f.write_str("Equal (1=Aries)"),
81            Self::Vehlow => f.write_str("Vehlow Equal"),
82            Self::Sripati => f.write_str("Sripati"),
83            Self::WholeSign => f.write_str("Whole Sign"),
84            Self::Alcabitius => f.write_str("Alcabitius"),
85            Self::Albategnius => f.write_str("Albategnius"),
86            Self::PullenSd => f.write_str("Pullen SD"),
87            Self::PullenSr => f.write_str("Pullen SR"),
88            Self::Meridian => f.write_str("Meridian"),
89            Self::Axial => f.write_str("Axial"),
90            Self::Topocentric => f.write_str("Topocentric"),
91            Self::Morinus => f.write_str("Morinus"),
92            Self::Sunshine => f.write_str("Sunshine"),
93            Self::Gauquelin => f.write_str("Gauquelin sectors"),
94            Self::Custom(custom) => fmt::Display::fmt(custom, f),
95        }
96    }
97}
98
99impl HouseSystem {
100    /// Validates the custom house-system definition when this is a custom variant.
101    ///
102    /// Built-in house systems are always valid; only structured custom labels,
103    /// aliases, and notes are checked. This keeps malformed catalog entries
104    /// from leaking into request summaries or release profiles.
105    pub fn validate(&self) -> Result<(), CustomDefinitionValidationError> {
106        match self {
107            Self::Custom(custom) => custom.validate(),
108            _ => Ok(()),
109        }
110    }
111
112    /// Validates the custom house-system definition against reserved built-in labels.
113    ///
114    /// Built-in house systems are always valid; custom entries are checked with
115    /// the same structural rules as [`validate`](Self::validate) and are then
116    /// compared against a caller-supplied built-in label resolver.
117    pub fn validate_against_reserved_labels(
118        &self,
119        is_reserved_label: impl Fn(&str) -> bool,
120    ) -> Result<(), CustomDefinitionValidationError> {
121        match self {
122            Self::Custom(custom) => custom.validate_against_reserved_labels(is_reserved_label),
123            _ => Ok(()),
124        }
125    }
126}
127
128/// A structured custom house-system definition.
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130#[derive(Clone, Debug, PartialEq, Eq, Hash)]
131pub struct CustomHouseSystem {
132    /// Stable human-readable name.
133    pub name: String,
134    /// Optional alternative names or aliases.
135    pub aliases: Vec<String>,
136    /// Optional notes about formula, assumptions, or limits.
137    pub notes: Option<String>,
138}
139
140impl CustomHouseSystem {
141    /// Creates a custom house-system definition.
142    pub fn new(name: impl Into<String>) -> Self {
143        Self {
144            name: name.into(),
145            aliases: Vec::new(),
146            notes: None,
147        }
148    }
149
150    /// Validates the custom house-system definition.
151    ///
152    /// The canonical name must be non-empty and free of leading or trailing
153    /// whitespace. Aliases and notes, when present, must follow the same
154    /// canonical-text rule, and aliases must be unique after ASCII
155    /// case-normalization, including against the canonical name.
156    pub fn validate(&self) -> Result<(), CustomDefinitionValidationError> {
157        validate_canonical_text("custom house system", "name", &self.name)?;
158
159        let mut aliases: Vec<&str> = Vec::with_capacity(self.aliases.len());
160        for (index, alias) in self.aliases.iter().enumerate() {
161            let field = format!("alias[{index}]");
162            validate_canonical_text("custom house system", field, alias)?;
163
164            if self.name.eq_ignore_ascii_case(alias)
165                || aliases
166                    .iter()
167                    .any(|existing| existing.eq_ignore_ascii_case(alias))
168            {
169                return Err(CustomDefinitionValidationError::duplicate_alias(
170                    "custom house system",
171                    alias,
172                ));
173            }
174
175            aliases.push(alias);
176        }
177
178        if let Some(notes) = &self.notes {
179            validate_canonical_text("custom house system", "notes", notes)?;
180        }
181
182        Ok(())
183    }
184
185    /// Validates the custom house-system definition against reserved built-in labels.
186    ///
187    /// This is a stricter form of [`validate`](Self::validate) that also rejects
188    /// a canonical name or alias that collides with a published built-in house-system
189    /// label. That keeps user-defined catalog entries distinguishable from the
190    /// built-in compatibility catalog when request validation or release profiles
191    /// surface them.
192    pub fn validate_against_reserved_labels(
193        &self,
194        is_reserved_label: impl Fn(&str) -> bool,
195    ) -> Result<(), CustomDefinitionValidationError> {
196        self.validate()?;
197
198        if is_reserved_label(&self.name) {
199            return Err(CustomDefinitionValidationError::reserved_label(
200                "custom house system",
201                "name",
202                &self.name,
203            ));
204        }
205
206        for (index, alias) in self.aliases.iter().enumerate() {
207            if is_reserved_label(alias) {
208                return Err(CustomDefinitionValidationError::reserved_label(
209                    "custom house system",
210                    format!("alias[{index}]"),
211                    alias,
212                ));
213            }
214        }
215
216        Ok(())
217    }
218}
219
220impl fmt::Display for CustomHouseSystem {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        f.write_str(&self.name)?;
223
224        if !self.aliases.is_empty() {
225            write!(f, " [aliases: {}]", self.aliases.join(", "))?;
226        }
227
228        if let Some(notes) = &self.notes {
229            write!(f, " ({notes})")?;
230        }
231
232        Ok(())
233    }
234}