Skip to main content

use_astronomical_orbit/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn normalized_key(value: &str) -> String {
8    value
9        .trim()
10        .chars()
11        .map(|character| match character {
12            '_' | ' ' => '-',
13            other => other.to_ascii_lowercase(),
14        })
15        .collect()
16}
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum OrbitTextError {
20    EmptyName,
21}
22
23impl fmt::Display for OrbitTextError {
24    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            Self::EmptyName => formatter.write_str("orbit name cannot be empty"),
27        }
28    }
29}
30
31impl Error for OrbitTextError {}
32
33#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub struct OrbitName(String);
35
36impl OrbitName {
37    /// Creates an orbit name from non-empty text.
38    ///
39    /// # Errors
40    ///
41    /// Returns [`OrbitTextError::EmptyName`] when the trimmed input is empty.
42    pub fn new(value: impl AsRef<str>) -> Result<Self, OrbitTextError> {
43        let trimmed = value.as_ref().trim();
44
45        if trimmed.is_empty() {
46            Err(OrbitTextError::EmptyName)
47        } else {
48            Ok(Self(trimmed.to_string()))
49        }
50    }
51
52    #[must_use]
53    pub fn as_str(&self) -> &str {
54        &self.0
55    }
56}
57
58impl AsRef<str> for OrbitName {
59    fn as_ref(&self) -> &str {
60        self.as_str()
61    }
62}
63
64impl fmt::Display for OrbitName {
65    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66        formatter.write_str(self.as_str())
67    }
68}
69
70impl FromStr for OrbitName {
71    type Err = OrbitTextError;
72
73    fn from_str(value: &str) -> Result<Self, Self::Err> {
74        Self::new(value)
75    }
76}
77
78#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
79pub enum OrbitKind {
80    Circular,
81    Elliptical,
82    Parabolic,
83    Hyperbolic,
84    Geocentric,
85    Heliocentric,
86    Areocentric,
87    Selenocentric,
88    Barycentric,
89    Unknown,
90    Custom(String),
91}
92
93impl fmt::Display for OrbitKind {
94    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self {
96            Self::Circular => formatter.write_str("circular"),
97            Self::Elliptical => formatter.write_str("elliptical"),
98            Self::Parabolic => formatter.write_str("parabolic"),
99            Self::Hyperbolic => formatter.write_str("hyperbolic"),
100            Self::Geocentric => formatter.write_str("geocentric"),
101            Self::Heliocentric => formatter.write_str("heliocentric"),
102            Self::Areocentric => formatter.write_str("areocentric"),
103            Self::Selenocentric => formatter.write_str("selenocentric"),
104            Self::Barycentric => formatter.write_str("barycentric"),
105            Self::Unknown => formatter.write_str("unknown"),
106            Self::Custom(value) => formatter.write_str(value),
107        }
108    }
109}
110
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub enum OrbitKindParseError {
113    Empty,
114}
115
116impl fmt::Display for OrbitKindParseError {
117    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
118        match self {
119            Self::Empty => formatter.write_str("orbit kind cannot be empty"),
120        }
121    }
122}
123
124impl Error for OrbitKindParseError {}
125
126impl FromStr for OrbitKind {
127    type Err = OrbitKindParseError;
128
129    fn from_str(value: &str) -> Result<Self, Self::Err> {
130        let trimmed = value.trim();
131
132        if trimmed.is_empty() {
133            return Err(OrbitKindParseError::Empty);
134        }
135
136        match normalized_key(trimmed).as_str() {
137            "circular" => Ok(Self::Circular),
138            "elliptical" => Ok(Self::Elliptical),
139            "parabolic" => Ok(Self::Parabolic),
140            "hyperbolic" => Ok(Self::Hyperbolic),
141            "geocentric" => Ok(Self::Geocentric),
142            "heliocentric" => Ok(Self::Heliocentric),
143            "areocentric" => Ok(Self::Areocentric),
144            "selenocentric" => Ok(Self::Selenocentric),
145            "barycentric" => Ok(Self::Barycentric),
146            "unknown" => Ok(Self::Unknown),
147            _ => Ok(Self::Custom(trimmed.to_string())),
148        }
149    }
150}
151
152#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
153pub enum OrbitDirection {
154    Prograde,
155    Retrograde,
156    Polar,
157    Unknown,
158    Custom(String),
159}
160
161impl fmt::Display for OrbitDirection {
162    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            Self::Prograde => formatter.write_str("prograde"),
165            Self::Retrograde => formatter.write_str("retrograde"),
166            Self::Polar => formatter.write_str("polar"),
167            Self::Unknown => formatter.write_str("unknown"),
168            Self::Custom(value) => formatter.write_str(value),
169        }
170    }
171}
172
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub enum OrbitDirectionParseError {
175    Empty,
176}
177
178impl fmt::Display for OrbitDirectionParseError {
179    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
180        match self {
181            Self::Empty => formatter.write_str("orbit direction cannot be empty"),
182        }
183    }
184}
185
186impl Error for OrbitDirectionParseError {}
187
188impl FromStr for OrbitDirection {
189    type Err = OrbitDirectionParseError;
190
191    fn from_str(value: &str) -> Result<Self, Self::Err> {
192        let trimmed = value.trim();
193
194        if trimmed.is_empty() {
195            return Err(OrbitDirectionParseError::Empty);
196        }
197
198        match normalized_key(trimmed).as_str() {
199            "prograde" => Ok(Self::Prograde),
200            "retrograde" => Ok(Self::Retrograde),
201            "polar" => Ok(Self::Polar),
202            "unknown" => Ok(Self::Unknown),
203            _ => Ok(Self::Custom(trimmed.to_string())),
204        }
205    }
206}
207
208#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
209pub enum OrbitState {
210    Bound,
211    Escape,
212    Transfer,
213    Decaying,
214    Unknown,
215    Custom(String),
216}
217
218impl fmt::Display for OrbitState {
219    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
220        match self {
221            Self::Bound => formatter.write_str("bound"),
222            Self::Escape => formatter.write_str("escape"),
223            Self::Transfer => formatter.write_str("transfer"),
224            Self::Decaying => formatter.write_str("decaying"),
225            Self::Unknown => formatter.write_str("unknown"),
226            Self::Custom(value) => formatter.write_str(value),
227        }
228    }
229}
230
231#[derive(Clone, Copy, Debug, Eq, PartialEq)]
232pub enum OrbitStateParseError {
233    Empty,
234}
235
236impl fmt::Display for OrbitStateParseError {
237    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
238        match self {
239            Self::Empty => formatter.write_str("orbit state cannot be empty"),
240        }
241    }
242}
243
244impl Error for OrbitStateParseError {}
245
246impl FromStr for OrbitState {
247    type Err = OrbitStateParseError;
248
249    fn from_str(value: &str) -> Result<Self, Self::Err> {
250        let trimmed = value.trim();
251
252        if trimmed.is_empty() {
253            return Err(OrbitStateParseError::Empty);
254        }
255
256        match normalized_key(trimmed).as_str() {
257            "bound" => Ok(Self::Bound),
258            "escape" => Ok(Self::Escape),
259            "transfer" => Ok(Self::Transfer),
260            "decaying" => Ok(Self::Decaying),
261            "unknown" => Ok(Self::Unknown),
262            _ => Ok(Self::Custom(trimmed.to_string())),
263        }
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::{OrbitDirection, OrbitKind, OrbitName, OrbitState, OrbitTextError};
270
271    #[test]
272    fn valid_orbit_name() {
273        let name = OrbitName::new("Earth heliocentric orbit").unwrap();
274
275        assert_eq!(name.as_str(), "Earth heliocentric orbit");
276    }
277
278    #[test]
279    fn empty_orbit_name_rejected() {
280        assert_eq!(OrbitName::new("  "), Err(OrbitTextError::EmptyName));
281    }
282
283    #[test]
284    fn orbit_kind_display_and_parse() {
285        assert_eq!(OrbitKind::Heliocentric.to_string(), "heliocentric");
286        assert_eq!(
287            "barycentric".parse::<OrbitKind>().unwrap(),
288            OrbitKind::Barycentric
289        );
290    }
291
292    #[test]
293    fn orbit_direction_display_and_parse() {
294        assert_eq!(OrbitDirection::Prograde.to_string(), "prograde");
295        assert_eq!(
296            "polar".parse::<OrbitDirection>().unwrap(),
297            OrbitDirection::Polar
298        );
299    }
300
301    #[test]
302    fn orbit_state_display_and_parse() {
303        assert_eq!(OrbitState::Bound.to_string(), "bound");
304        assert_eq!(
305            "transfer".parse::<OrbitState>().unwrap(),
306            OrbitState::Transfer
307        );
308    }
309
310    #[test]
311    fn custom_orbit_kind() {
312        assert_eq!(
313            "graveyard".parse::<OrbitKind>().unwrap(),
314            OrbitKind::Custom("graveyard".to_string())
315        );
316    }
317}