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 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}