Skip to main content

use_astronomical_observation/
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 AstronomicalObservationTextError {
20    EmptyObservationId,
21}
22
23impl fmt::Display for AstronomicalObservationTextError {
24    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            Self::EmptyObservationId => {
27                formatter.write_str("astronomical observation identifier cannot be empty")
28            },
29        }
30    }
31}
32
33impl Error for AstronomicalObservationTextError {}
34
35#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
36pub struct AstronomicalObservationId(String);
37
38impl AstronomicalObservationId {
39    /// Creates an astronomical observation identifier from non-empty text.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`AstronomicalObservationTextError::EmptyObservationId`] when the trimmed input is empty.
44    pub fn new(value: impl AsRef<str>) -> Result<Self, AstronomicalObservationTextError> {
45        let trimmed = value.as_ref().trim();
46
47        if trimmed.is_empty() {
48            Err(AstronomicalObservationTextError::EmptyObservationId)
49        } else {
50            Ok(Self(trimmed.to_string()))
51        }
52    }
53
54    #[must_use]
55    pub fn as_str(&self) -> &str {
56        &self.0
57    }
58}
59
60impl AsRef<str> for AstronomicalObservationId {
61    fn as_ref(&self) -> &str {
62        self.as_str()
63    }
64}
65
66impl fmt::Display for AstronomicalObservationId {
67    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68        formatter.write_str(self.as_str())
69    }
70}
71
72impl FromStr for AstronomicalObservationId {
73    type Err = AstronomicalObservationTextError;
74
75    fn from_str(value: &str) -> Result<Self, Self::Err> {
76        Self::new(value)
77    }
78}
79
80#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
81pub enum ObservationKind {
82    Visual,
83    Photometric,
84    Spectroscopic,
85    Astrometric,
86    Radio,
87    Infrared,
88    Ultraviolet,
89    XRay,
90    GammaRay,
91    Unknown,
92    Custom(String),
93}
94
95impl fmt::Display for ObservationKind {
96    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97        match self {
98            Self::Visual => formatter.write_str("visual"),
99            Self::Photometric => formatter.write_str("photometric"),
100            Self::Spectroscopic => formatter.write_str("spectroscopic"),
101            Self::Astrometric => formatter.write_str("astrometric"),
102            Self::Radio => formatter.write_str("radio"),
103            Self::Infrared => formatter.write_str("infrared"),
104            Self::Ultraviolet => formatter.write_str("ultraviolet"),
105            Self::XRay => formatter.write_str("x-ray"),
106            Self::GammaRay => formatter.write_str("gamma-ray"),
107            Self::Unknown => formatter.write_str("unknown"),
108            Self::Custom(value) => formatter.write_str(value),
109        }
110    }
111}
112
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub enum ObservationKindParseError {
115    Empty,
116}
117
118impl fmt::Display for ObservationKindParseError {
119    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120        match self {
121            Self::Empty => formatter.write_str("observation kind cannot be empty"),
122        }
123    }
124}
125
126impl Error for ObservationKindParseError {}
127
128impl FromStr for ObservationKind {
129    type Err = ObservationKindParseError;
130
131    fn from_str(value: &str) -> Result<Self, Self::Err> {
132        let trimmed = value.trim();
133
134        if trimmed.is_empty() {
135            return Err(ObservationKindParseError::Empty);
136        }
137
138        match normalized_key(trimmed).as_str() {
139            "visual" => Ok(Self::Visual),
140            "photometric" => Ok(Self::Photometric),
141            "spectroscopic" => Ok(Self::Spectroscopic),
142            "astrometric" => Ok(Self::Astrometric),
143            "radio" => Ok(Self::Radio),
144            "infrared" => Ok(Self::Infrared),
145            "ultraviolet" => Ok(Self::Ultraviolet),
146            "x-ray" | "xray" => Ok(Self::XRay),
147            "gamma-ray" | "gammaray" => Ok(Self::GammaRay),
148            "unknown" => Ok(Self::Unknown),
149            _ => Ok(Self::Custom(trimmed.to_string())),
150        }
151    }
152}
153
154#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
155pub enum ObservationBand {
156    Radio,
157    Microwave,
158    Infrared,
159    Visible,
160    Ultraviolet,
161    XRay,
162    GammaRay,
163    Unknown,
164    Custom(String),
165}
166
167impl fmt::Display for ObservationBand {
168    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match self {
170            Self::Radio => formatter.write_str("radio"),
171            Self::Microwave => formatter.write_str("microwave"),
172            Self::Infrared => formatter.write_str("infrared"),
173            Self::Visible => formatter.write_str("visible"),
174            Self::Ultraviolet => formatter.write_str("ultraviolet"),
175            Self::XRay => formatter.write_str("x-ray"),
176            Self::GammaRay => formatter.write_str("gamma-ray"),
177            Self::Unknown => formatter.write_str("unknown"),
178            Self::Custom(value) => formatter.write_str(value),
179        }
180    }
181}
182
183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
184pub enum ObservationBandParseError {
185    Empty,
186}
187
188impl fmt::Display for ObservationBandParseError {
189    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
190        match self {
191            Self::Empty => formatter.write_str("observation band cannot be empty"),
192        }
193    }
194}
195
196impl Error for ObservationBandParseError {}
197
198impl FromStr for ObservationBand {
199    type Err = ObservationBandParseError;
200
201    fn from_str(value: &str) -> Result<Self, Self::Err> {
202        let trimmed = value.trim();
203
204        if trimmed.is_empty() {
205            return Err(ObservationBandParseError::Empty);
206        }
207
208        match normalized_key(trimmed).as_str() {
209            "radio" => Ok(Self::Radio),
210            "microwave" => Ok(Self::Microwave),
211            "infrared" => Ok(Self::Infrared),
212            "visible" => Ok(Self::Visible),
213            "ultraviolet" => Ok(Self::Ultraviolet),
214            "x-ray" | "xray" => Ok(Self::XRay),
215            "gamma-ray" | "gammaray" => Ok(Self::GammaRay),
216            "unknown" => Ok(Self::Unknown),
217            _ => Ok(Self::Custom(trimmed.to_string())),
218        }
219    }
220}
221
222#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
223pub enum ObservationInstrumentKind {
224    Telescope,
225    RadioTelescope,
226    Spectrograph,
227    Camera,
228    Photometer,
229    Interferometer,
230    SpaceTelescope,
231    Unknown,
232    Custom(String),
233}
234
235impl fmt::Display for ObservationInstrumentKind {
236    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
237        match self {
238            Self::Telescope => formatter.write_str("telescope"),
239            Self::RadioTelescope => formatter.write_str("radio-telescope"),
240            Self::Spectrograph => formatter.write_str("spectrograph"),
241            Self::Camera => formatter.write_str("camera"),
242            Self::Photometer => formatter.write_str("photometer"),
243            Self::Interferometer => formatter.write_str("interferometer"),
244            Self::SpaceTelescope => formatter.write_str("space-telescope"),
245            Self::Unknown => formatter.write_str("unknown"),
246            Self::Custom(value) => formatter.write_str(value),
247        }
248    }
249}
250
251#[derive(Clone, Copy, Debug, Eq, PartialEq)]
252pub enum ObservationInstrumentKindParseError {
253    Empty,
254}
255
256impl fmt::Display for ObservationInstrumentKindParseError {
257    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
258        match self {
259            Self::Empty => formatter.write_str("observation instrument kind cannot be empty"),
260        }
261    }
262}
263
264impl Error for ObservationInstrumentKindParseError {}
265
266impl FromStr for ObservationInstrumentKind {
267    type Err = ObservationInstrumentKindParseError;
268
269    fn from_str(value: &str) -> Result<Self, Self::Err> {
270        let trimmed = value.trim();
271
272        if trimmed.is_empty() {
273            return Err(ObservationInstrumentKindParseError::Empty);
274        }
275
276        match normalized_key(trimmed).as_str() {
277            "telescope" => Ok(Self::Telescope),
278            "radio-telescope" | "radiotelescope" => Ok(Self::RadioTelescope),
279            "spectrograph" => Ok(Self::Spectrograph),
280            "camera" => Ok(Self::Camera),
281            "photometer" => Ok(Self::Photometer),
282            "interferometer" => Ok(Self::Interferometer),
283            "space-telescope" | "spacetelescope" => Ok(Self::SpaceTelescope),
284            "unknown" => Ok(Self::Unknown),
285            _ => Ok(Self::Custom(trimmed.to_string())),
286        }
287    }
288}
289
290#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
291pub enum SeeingCondition {
292    Excellent,
293    Good,
294    Fair,
295    Poor,
296    Unknown,
297    Custom(String),
298}
299
300impl fmt::Display for SeeingCondition {
301    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302        match self {
303            Self::Excellent => formatter.write_str("excellent"),
304            Self::Good => formatter.write_str("good"),
305            Self::Fair => formatter.write_str("fair"),
306            Self::Poor => formatter.write_str("poor"),
307            Self::Unknown => formatter.write_str("unknown"),
308            Self::Custom(value) => formatter.write_str(value),
309        }
310    }
311}
312
313#[derive(Clone, Copy, Debug, Eq, PartialEq)]
314pub enum SeeingConditionParseError {
315    Empty,
316}
317
318impl fmt::Display for SeeingConditionParseError {
319    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
320        match self {
321            Self::Empty => formatter.write_str("seeing condition cannot be empty"),
322        }
323    }
324}
325
326impl Error for SeeingConditionParseError {}
327
328impl FromStr for SeeingCondition {
329    type Err = SeeingConditionParseError;
330
331    fn from_str(value: &str) -> Result<Self, Self::Err> {
332        let trimmed = value.trim();
333
334        if trimmed.is_empty() {
335            return Err(SeeingConditionParseError::Empty);
336        }
337
338        match normalized_key(trimmed).as_str() {
339            "excellent" => Ok(Self::Excellent),
340            "good" => Ok(Self::Good),
341            "fair" => Ok(Self::Fair),
342            "poor" => Ok(Self::Poor),
343            "unknown" => Ok(Self::Unknown),
344            _ => Ok(Self::Custom(trimmed.to_string())),
345        }
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::{
352        AstronomicalObservationId, AstronomicalObservationTextError, ObservationBand,
353        ObservationInstrumentKind, ObservationKind,
354    };
355
356    #[test]
357    fn valid_observation_id() {
358        let identifier = AstronomicalObservationId::new("obs-42").unwrap();
359
360        assert_eq!(identifier.as_str(), "obs-42");
361    }
362
363    #[test]
364    fn empty_observation_id_rejected() {
365        assert_eq!(
366            AstronomicalObservationId::new("   "),
367            Err(AstronomicalObservationTextError::EmptyObservationId)
368        );
369    }
370
371    #[test]
372    fn observation_kind_display_and_parse() {
373        assert_eq!(ObservationKind::Photometric.to_string(), "photometric");
374        assert_eq!(
375            "infrared".parse::<ObservationKind>().unwrap(),
376            ObservationKind::Infrared
377        );
378    }
379
380    #[test]
381    fn observation_band_display_and_parse() {
382        assert_eq!(ObservationBand::Visible.to_string(), "visible");
383        assert_eq!(
384            "x-ray".parse::<ObservationBand>().unwrap(),
385            ObservationBand::XRay
386        );
387    }
388
389    #[test]
390    fn instrument_kind_display_and_parse() {
391        assert_eq!(
392            ObservationInstrumentKind::Telescope.to_string(),
393            "telescope"
394        );
395        assert_eq!(
396            "space telescope"
397                .parse::<ObservationInstrumentKind>()
398                .unwrap(),
399            ObservationInstrumentKind::SpaceTelescope
400        );
401    }
402
403    #[test]
404    fn custom_observation_kind() {
405        assert_eq!(
406            "polarimetric".parse::<ObservationKind>().unwrap(),
407            ObservationKind::Custom("polarimetric".to_string())
408        );
409    }
410}