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