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
//! House system types: [`HouseSystem`] and [`CustomHouseSystem`].
use core::fmt;
use crate::custom_bodies::{validate_canonical_text, CustomDefinitionValidationError};
/// A built-in or custom house system selection.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum HouseSystem {
/// Placidus.
Placidus,
/// Koch.
Koch,
/// Porphyry.
Porphyry,
/// Regiomontanus.
Regiomontanus,
/// Campanus.
Campanus,
/// Carter (poli-equatorial) houses.
Carter,
/// Horizon/Azimuth houses.
Horizon,
/// APC houses.
Apc,
/// Krusinski-Pisa-Goelzer houses.
KrusinskiPisaGoelzer,
/// Equal houses.
Equal,
/// Equal houses with the Midheaven on cusp 10.
EqualMidheaven,
/// Equal houses with the first house anchored at 0° Aries.
EqualAries,
/// Vehlow equal houses, with the Ascendant centered in house 1.
Vehlow,
/// Sripati houses.
Sripati,
/// Whole sign houses.
WholeSign,
/// Alcabitius.
Alcabitius,
/// Albategnius / Savard-A.
Albategnius,
/// Pullen sinusoidal delta (Neo-Porphyry).
PullenSd,
/// Pullen sinusoidal ratio.
PullenSr,
/// Meridian-style systems.
Meridian,
/// Axial variants documented by specific software.
Axial,
/// Topocentric (Polich-Page).
Topocentric,
/// Morinus.
Morinus,
/// Sunshine (Bob Makransky / Dieter Treindl family).
Sunshine,
/// Gauquelin sectors.
Gauquelin,
/// A custom house system definition.
Custom(CustomHouseSystem),
}
impl fmt::Display for HouseSystem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Placidus => f.write_str("Placidus"),
Self::Koch => f.write_str("Koch"),
Self::Porphyry => f.write_str("Porphyry"),
Self::Regiomontanus => f.write_str("Regiomontanus"),
Self::Campanus => f.write_str("Campanus"),
Self::Carter => f.write_str("Carter (poli-equatorial)"),
Self::Horizon => f.write_str("Horizon/Azimuth"),
Self::Apc => f.write_str("APC"),
Self::KrusinskiPisaGoelzer => f.write_str("Krusinski-Pisa-Goelzer"),
Self::Equal => f.write_str("Equal"),
Self::EqualMidheaven => f.write_str("Equal (MC)"),
Self::EqualAries => f.write_str("Equal (1=Aries)"),
Self::Vehlow => f.write_str("Vehlow Equal"),
Self::Sripati => f.write_str("Sripati"),
Self::WholeSign => f.write_str("Whole Sign"),
Self::Alcabitius => f.write_str("Alcabitius"),
Self::Albategnius => f.write_str("Albategnius"),
Self::PullenSd => f.write_str("Pullen SD"),
Self::PullenSr => f.write_str("Pullen SR"),
Self::Meridian => f.write_str("Meridian"),
Self::Axial => f.write_str("Axial"),
Self::Topocentric => f.write_str("Topocentric"),
Self::Morinus => f.write_str("Morinus"),
Self::Sunshine => f.write_str("Sunshine"),
Self::Gauquelin => f.write_str("Gauquelin sectors"),
Self::Custom(custom) => fmt::Display::fmt(custom, f),
}
}
}
impl HouseSystem {
/// Validates the custom house-system definition when this is a custom variant.
///
/// Built-in house systems are always valid; only structured custom labels,
/// aliases, and notes are checked. This keeps malformed catalog entries
/// from leaking into request summaries or release profiles.
pub fn validate(&self) -> Result<(), CustomDefinitionValidationError> {
match self {
Self::Custom(custom) => custom.validate(),
_ => Ok(()),
}
}
/// Validates the custom house-system definition against reserved built-in labels.
///
/// Built-in house systems are always valid; custom entries are checked with
/// the same structural rules as [`validate`](Self::validate) and are then
/// compared against a caller-supplied built-in label resolver.
pub fn validate_against_reserved_labels(
&self,
is_reserved_label: impl Fn(&str) -> bool,
) -> Result<(), CustomDefinitionValidationError> {
match self {
Self::Custom(custom) => custom.validate_against_reserved_labels(is_reserved_label),
_ => Ok(()),
}
}
}
/// A structured custom house-system definition.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CustomHouseSystem {
/// Stable human-readable name.
pub name: String,
/// Optional alternative names or aliases.
pub aliases: Vec<String>,
/// Optional notes about formula, assumptions, or limits.
pub notes: Option<String>,
}
impl CustomHouseSystem {
/// Creates a custom house-system definition.
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
aliases: Vec::new(),
notes: None,
}
}
/// Validates the custom house-system definition.
///
/// The canonical name must be non-empty and free of leading or trailing
/// whitespace. Aliases and notes, when present, must follow the same
/// canonical-text rule, and aliases must be unique after ASCII
/// case-normalization, including against the canonical name.
pub fn validate(&self) -> Result<(), CustomDefinitionValidationError> {
validate_canonical_text("custom house system", "name", &self.name)?;
let mut aliases: Vec<&str> = Vec::with_capacity(self.aliases.len());
for (index, alias) in self.aliases.iter().enumerate() {
let field = format!("alias[{index}]");
validate_canonical_text("custom house system", field, alias)?;
if self.name.eq_ignore_ascii_case(alias)
|| aliases
.iter()
.any(|existing| existing.eq_ignore_ascii_case(alias))
{
return Err(CustomDefinitionValidationError::duplicate_alias(
"custom house system",
alias,
));
}
aliases.push(alias);
}
if let Some(notes) = &self.notes {
validate_canonical_text("custom house system", "notes", notes)?;
}
Ok(())
}
/// Validates the custom house-system definition against reserved built-in labels.
///
/// This is a stricter form of [`validate`](Self::validate) that also rejects
/// a canonical name or alias that collides with a published built-in house-system
/// label. That keeps user-defined catalog entries distinguishable from the
/// built-in compatibility catalog when request validation or release profiles
/// surface them.
pub fn validate_against_reserved_labels(
&self,
is_reserved_label: impl Fn(&str) -> bool,
) -> Result<(), CustomDefinitionValidationError> {
self.validate()?;
if is_reserved_label(&self.name) {
return Err(CustomDefinitionValidationError::reserved_label(
"custom house system",
"name",
&self.name,
));
}
for (index, alias) in self.aliases.iter().enumerate() {
if is_reserved_label(alias) {
return Err(CustomDefinitionValidationError::reserved_label(
"custom house system",
format!("alias[{index}]"),
alias,
));
}
}
Ok(())
}
}
impl fmt::Display for CustomHouseSystem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.name)?;
if !self.aliases.is_empty() {
write!(f, " [aliases: {}]", self.aliases.join(", "))?;
}
if let Some(notes) = &self.notes {
write!(f, " ({notes})")?;
}
Ok(())
}
}