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, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
19pub enum OrbitalElementKind {
20 SemiMajorAxis,
21 Eccentricity,
22 Inclination,
23 LongitudeOfAscendingNode,
24 ArgumentOfPeriapsis,
25 TrueAnomaly,
26 MeanAnomaly,
27 Epoch,
28 Unknown,
29 Custom(String),
30}
31
32impl fmt::Display for OrbitalElementKind {
33 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Self::SemiMajorAxis => formatter.write_str("semi-major-axis"),
36 Self::Eccentricity => formatter.write_str("eccentricity"),
37 Self::Inclination => formatter.write_str("inclination"),
38 Self::LongitudeOfAscendingNode => formatter.write_str("longitude-of-ascending-node"),
39 Self::ArgumentOfPeriapsis => formatter.write_str("argument-of-periapsis"),
40 Self::TrueAnomaly => formatter.write_str("true-anomaly"),
41 Self::MeanAnomaly => formatter.write_str("mean-anomaly"),
42 Self::Epoch => formatter.write_str("epoch"),
43 Self::Unknown => formatter.write_str("unknown"),
44 Self::Custom(value) => formatter.write_str(value),
45 }
46 }
47}
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub enum OrbitalElementKindParseError {
51 Empty,
52}
53
54impl fmt::Display for OrbitalElementKindParseError {
55 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Self::Empty => formatter.write_str("orbital element kind cannot be empty"),
58 }
59 }
60}
61
62impl Error for OrbitalElementKindParseError {}
63
64impl FromStr for OrbitalElementKind {
65 type Err = OrbitalElementKindParseError;
66
67 fn from_str(value: &str) -> Result<Self, Self::Err> {
68 let trimmed = value.trim();
69
70 if trimmed.is_empty() {
71 return Err(OrbitalElementKindParseError::Empty);
72 }
73
74 match normalized_key(trimmed).as_str() {
75 "semi-major-axis" | "semimajoraxis" => Ok(Self::SemiMajorAxis),
76 "eccentricity" => Ok(Self::Eccentricity),
77 "inclination" => Ok(Self::Inclination),
78 "longitude-of-ascending-node" | "loan" => Ok(Self::LongitudeOfAscendingNode),
79 "argument-of-periapsis" | "aop" => Ok(Self::ArgumentOfPeriapsis),
80 "true-anomaly" | "trueanomaly" => Ok(Self::TrueAnomaly),
81 "mean-anomaly" | "meananomaly" => Ok(Self::MeanAnomaly),
82 "epoch" => Ok(Self::Epoch),
83 "unknown" => Ok(Self::Unknown),
84 _ => Ok(Self::Custom(trimmed.to_string())),
85 }
86 }
87}
88
89#[derive(Clone, Debug, Eq, PartialEq)]
90pub enum OrbitalElementValueError {
91 NonFiniteValue,
92 EmptyUnitLabel,
93 NegativeEccentricity,
94 InvalidInclination,
95}
96
97impl fmt::Display for OrbitalElementValueError {
98 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 Self::NonFiniteValue => formatter.write_str("orbital element value must be finite"),
101 Self::EmptyUnitLabel => {
102 formatter.write_str("orbital element unit label cannot be empty")
103 },
104 Self::NegativeEccentricity => formatter.write_str("eccentricity cannot be negative"),
105 Self::InvalidInclination => {
106 formatter.write_str("inclination must be within 0.0..=180.0 degrees")
107 },
108 }
109 }
110}
111
112impl Error for OrbitalElementValueError {}
113
114#[derive(Clone, Debug, PartialEq)]
115pub struct OrbitalElementValue {
116 value: f64,
117 unit_label: Option<String>,
118}
119
120impl OrbitalElementValue {
121 pub const fn new(value: f64) -> Result<Self, OrbitalElementValueError> {
127 if !value.is_finite() {
128 return Err(OrbitalElementValueError::NonFiniteValue);
129 }
130
131 Ok(Self {
132 value,
133 unit_label: None,
134 })
135 }
136
137 pub fn with_unit(
144 value: f64,
145 unit_label: impl AsRef<str>,
146 ) -> Result<Self, OrbitalElementValueError> {
147 let unit_label = unit_label.as_ref().trim();
148 if unit_label.is_empty() {
149 return Err(OrbitalElementValueError::EmptyUnitLabel);
150 }
151
152 let mut element_value = Self::new(value)?;
153 element_value.unit_label = Some(unit_label.to_string());
154 Ok(element_value)
155 }
156
157 #[must_use]
158 pub const fn value(&self) -> f64 {
159 self.value
160 }
161
162 #[must_use]
163 pub fn unit_label(&self) -> Option<&str> {
164 self.unit_label.as_deref()
165 }
166}
167
168#[derive(Clone, Debug, PartialEq)]
169pub struct OrbitalElement {
170 kind: OrbitalElementKind,
171 value: OrbitalElementValue,
172}
173
174impl OrbitalElement {
175 pub fn new(
182 kind: OrbitalElementKind,
183 value: OrbitalElementValue,
184 ) -> Result<Self, OrbitalElementValueError> {
185 match kind {
186 OrbitalElementKind::Eccentricity if value.value() < 0.0 => {
187 return Err(OrbitalElementValueError::NegativeEccentricity);
188 },
189 OrbitalElementKind::Inclination if !(0.0..=180.0).contains(&value.value()) => {
190 return Err(OrbitalElementValueError::InvalidInclination);
191 },
192 _ => {},
193 }
194
195 Ok(Self { kind, value })
196 }
197
198 #[must_use]
199 pub const fn kind(&self) -> &OrbitalElementKind {
200 &self.kind
201 }
202
203 #[must_use]
204 pub const fn value(&self) -> &OrbitalElementValue {
205 &self.value
206 }
207}
208
209#[derive(Clone, Debug, Default, PartialEq)]
210pub struct OrbitalElementSet {
211 elements: Vec<OrbitalElement>,
212}
213
214impl OrbitalElementSet {
215 #[must_use]
216 pub const fn new(elements: Vec<OrbitalElement>) -> Self {
217 Self { elements }
218 }
219
220 #[must_use]
221 pub fn elements(&self) -> &[OrbitalElement] {
222 &self.elements
223 }
224
225 #[must_use]
226 pub const fn len(&self) -> usize {
227 self.elements.len()
228 }
229
230 #[must_use]
231 pub const fn is_empty(&self) -> bool {
232 self.elements.is_empty()
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::{
239 OrbitalElement, OrbitalElementKind, OrbitalElementSet, OrbitalElementValue,
240 OrbitalElementValueError,
241 };
242
243 #[test]
244 fn orbital_element_kind_display_and_parse() {
245 assert_eq!(
246 OrbitalElementKind::SemiMajorAxis.to_string(),
247 "semi-major-axis"
248 );
249 assert_eq!(
250 "mean anomaly".parse::<OrbitalElementKind>().unwrap(),
251 OrbitalElementKind::MeanAnomaly
252 );
253 }
254
255 #[test]
256 fn custom_orbital_element_kind() {
257 assert_eq!(
258 "perihelion-time".parse::<OrbitalElementKind>().unwrap(),
259 OrbitalElementKind::Custom("perihelion-time".to_string())
260 );
261 }
262
263 #[test]
264 fn valid_eccentricity() {
265 let element = OrbitalElement::new(
266 OrbitalElementKind::Eccentricity,
267 OrbitalElementValue::new(0.0167).unwrap(),
268 )
269 .unwrap();
270
271 assert!((element.value().value() - 0.0167).abs() < f64::EPSILON);
272 }
273
274 #[test]
275 fn negative_eccentricity_rejected() {
276 assert_eq!(
277 OrbitalElement::new(
278 OrbitalElementKind::Eccentricity,
279 OrbitalElementValue::new(-0.1).unwrap(),
280 ),
281 Err(OrbitalElementValueError::NegativeEccentricity)
282 );
283 }
284
285 #[test]
286 fn valid_inclination() {
287 let element = OrbitalElement::new(
288 OrbitalElementKind::Inclination,
289 OrbitalElementValue::with_unit(98.7, "deg").unwrap(),
290 )
291 .unwrap();
292
293 assert_eq!(element.value().unit_label(), Some("deg"));
294 }
295
296 #[test]
297 fn invalid_inclination_rejected() {
298 assert_eq!(
299 OrbitalElement::new(
300 OrbitalElementKind::Inclination,
301 OrbitalElementValue::new(181.0).unwrap(),
302 ),
303 Err(OrbitalElementValueError::InvalidInclination)
304 );
305 }
306
307 #[test]
308 fn orbital_element_set_construction() {
309 let elements = vec![
310 OrbitalElement::new(
311 OrbitalElementKind::SemiMajorAxis,
312 OrbitalElementValue::with_unit(1.0, "AU").unwrap(),
313 )
314 .unwrap(),
315 OrbitalElement::new(
316 OrbitalElementKind::Eccentricity,
317 OrbitalElementValue::new(0.0167).unwrap(),
318 )
319 .unwrap(),
320 ];
321
322 let set = OrbitalElementSet::new(elements);
323
324 assert_eq!(set.len(), 2);
325 assert!(!set.is_empty());
326 }
327}