stellar_class/
classification.rs

1use crate::{error::Error, luminosity::LuminosityClass, spectral_types::SpectralType};
2
3/// Represents a MK stellar classification.
4/// Peculiarities not yet supported.
5#[derive(Debug, Clone, PartialEq)]
6#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
7pub struct Classification {
8    pub spectral_type: SpectralType,
9    pub subtype: f32,
10    pub luminosity_class: LuminosityClass,
11}
12
13impl Classification {
14    /// Parses a string into the struct.
15    pub fn from_string(class_str: &str) -> Result<Self, Error> {
16        let class_str = preprocess_class_str(class_str)?;
17
18        // Spectral type
19        let (spectral_str, remaining) = class_str.split_at(1);
20        let spectral_type = spectral_str.try_into()?;
21
22        // Subtype
23        let mut chars = remaining.chars();
24
25        let mut subtype_str = String::new();
26
27        let first_digit = chars.next().unwrap();
28        if !first_digit.is_digit(10) {
29            return Err(Error::InvalidSubtype);
30        }
31        subtype_str.push(first_digit);
32
33        let possible_dot = chars.next().unwrap();
34        if possible_dot == '.' {
35            subtype_str.push(possible_dot);
36
37            while let Some(next_digit) = chars.next() {
38                if !next_digit.is_digit(10) {
39                    break;
40                }
41
42                subtype_str.push(next_digit);
43            }
44        }
45
46        if subtype_str == "00" {
47            let _ = subtype_str.pop();
48        }
49
50        if subtype_str.ends_with('.') || subtype_str.ends_with("00") {
51            return Err(Error::InvalidSubtype);
52        }
53
54        let subtype = subtype_str
55            .parse::<f32>()
56            .map_err(|_| Error::InvalidSubtype)?;
57        if subtype == 0.0 && subtype_str.contains('.') {
58            return Err(Error::InvalidSubtype);
59        }
60
61        let remaining = remaining.split_at(subtype_str.len()).1;
62
63        // Luminosity class
64        let mut luminosity_class = None;
65        for lum_len in (1..remaining.len() + 1).rev() {
66            if let Ok(lum_class) = remaining.get(0..lum_len).unwrap().try_into() {
67                luminosity_class = Some(lum_class);
68                break;
69            }
70        }
71
72        let Some(luminosity_class) = luminosity_class else {
73            return Err(Error::InvalidLuminosityClass);
74        };
75
76        Ok(Classification {
77            spectral_type,
78            subtype,
79            luminosity_class,
80        })
81    }
82
83    pub fn to_string(&self) -> String {
84        let spectral: &str = self.spectral_type.into();
85
86        let subtype = if self.subtype.fract() == 0.0 {
87            format!("{:.0}", self.subtype)
88        } else {
89            format!("{:.1}", self.subtype)
90        };
91
92        let luminosity: &str = self.luminosity_class.into();
93
94        format!("{}{}{}", spectral, subtype, luminosity)
95    }
96}
97
98fn preprocess_class_str(class_str: &str) -> Result<&str, Error> {
99    let class_str = class_str.trim();
100    if !class_str.is_ascii() {
101        return Err(Error::InvalidStringNonAscii);
102    }
103    if class_str.len() < 3 {
104        return Err(Error::InvalidStringTooShort);
105    }
106
107    Ok(class_str)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_from_string_o1v() {
116        let class_str = "O1V";
117        let result = Classification::from_string(class_str).unwrap();
118        assert_eq!(result.spectral_type, SpectralType::O);
119        assert_eq!(result.subtype, 1.0);
120        assert_eq!(result.luminosity_class, LuminosityClass::V);
121    }
122
123    #[test]
124    fn test_from_string_a0v() {
125        let class_str = "A0V";
126        let result = Classification::from_string(class_str).unwrap();
127        assert_eq!(result.spectral_type, SpectralType::A);
128        assert_eq!(result.subtype, 0.0);
129        assert_eq!(result.luminosity_class, LuminosityClass::V);
130    }
131
132    #[test]
133    fn test_from_string_b0_5iv() {
134        let class_str = "B0.5IV";
135        let result = Classification::from_string(class_str).unwrap();
136        assert_eq!(result.spectral_type, SpectralType::B);
137        assert_eq!(result.subtype, 0.5);
138        assert_eq!(result.luminosity_class, LuminosityClass::IV);
139    }
140
141    #[test]
142    fn test_from_string_g2v() {
143        let class_str = "G2V";
144        let result = Classification::from_string(class_str).unwrap();
145        assert_eq!(result.spectral_type, SpectralType::G);
146        assert_eq!(result.subtype, 2.0);
147        assert_eq!(result.luminosity_class, LuminosityClass::V);
148    }
149
150    #[test]
151    fn test_from_string_m1iab() {
152        let class_str = "M1Iab";
153        let result = Classification::from_string(class_str).unwrap();
154        assert_eq!(result.spectral_type, SpectralType::M);
155        assert_eq!(result.subtype, 1.0);
156        assert_eq!(result.luminosity_class, LuminosityClass::Iab);
157    }
158
159    #[test]
160    fn test_from_string_k0iii() {
161        let class_str = "K0III";
162        let result = Classification::from_string(class_str).unwrap();
163        assert_eq!(result.spectral_type, SpectralType::K);
164        assert_eq!(result.subtype, 0.0);
165        assert_eq!(result.luminosity_class, LuminosityClass::III);
166    }
167
168    #[test]
169    fn test_from_string_f5vi() {
170        let class_str = "F5VI";
171        let result = Classification::from_string(class_str).unwrap();
172        assert_eq!(result.spectral_type, SpectralType::F);
173        assert_eq!(result.subtype, 5.0);
174        assert_eq!(result.luminosity_class, LuminosityClass::VI);
175    }
176
177    #[test]
178    fn test_from_string_with_peculiarities() {
179        let class_str = "O1Vpe";
180        let result = Classification::from_string(class_str).unwrap();
181        assert_eq!(result.spectral_type, SpectralType::O);
182        assert_eq!(result.subtype, 1.0);
183        assert_eq!(result.luminosity_class, LuminosityClass::V);
184    }
185
186    #[test]
187    fn test_from_string_invalid_spectral_type() {
188        let class_str = "X1V";
189        let result = Classification::from_string(class_str);
190        assert_eq!(result, Err(Error::InvalidSpectralType));
191    }
192
193    #[test]
194    fn test_from_string_invalid_subtype() {
195        let class_str = "OxV";
196        let result = Classification::from_string(class_str);
197        assert_eq!(result, Err(Error::InvalidSubtype));
198    }
199
200    #[test]
201    fn test_from_string_subtype_out_of_range() {
202        let class_str = "O11V";
203        let result = Classification::from_string(class_str);
204        assert_eq!(result, Err(Error::InvalidLuminosityClass));
205    }
206
207    #[test]
208    fn test_from_string_subtype_out_of_range_decimal() {
209        let class_str = "O11.1V";
210        let result = Classification::from_string(class_str);
211        assert_eq!(result, Err(Error::InvalidLuminosityClass));
212    }
213
214    #[test]
215    fn test_from_string_redundant_dot() {
216        let class_str = "O1.V";
217        let result = Classification::from_string(class_str);
218        assert_eq!(result, Err(Error::InvalidSubtype));
219    }
220
221    #[test]
222    fn test_from_string_redundant_zero_and_dot() {
223        let class_str = "O0.0V";
224        let result = Classification::from_string(class_str);
225        assert_eq!(result, Err(Error::InvalidSubtype));
226    }
227
228    #[test]
229    fn test_from_string_redundant_zeros() {
230        let class_str = "O1.00V";
231        let result = Classification::from_string(class_str);
232        assert_eq!(result, Err(Error::InvalidSubtype));
233    }
234
235    #[test]
236    fn test_from_string_zero_with_dot() {
237        let class_str = "O0.0V";
238        let result = Classification::from_string(class_str);
239        assert_eq!(result, Err(Error::InvalidSubtype));
240    }
241
242    #[test]
243    fn test_to_string_o1v() {
244        let class = Classification {
245            spectral_type: SpectralType::O,
246            subtype: 1.0,
247            luminosity_class: LuminosityClass::V,
248        };
249        assert_eq!(class.to_string(), "O1V");
250    }
251
252    #[test]
253    fn test_to_string_a0v() {
254        let class = Classification {
255            spectral_type: SpectralType::A,
256            subtype: 0.0,
257            luminosity_class: LuminosityClass::V,
258        };
259        assert_eq!(class.to_string(), "A0V");
260    }
261
262    #[test]
263    fn test_to_string_b0_5iv() {
264        let class = Classification {
265            spectral_type: SpectralType::B,
266            subtype: 0.5,
267            luminosity_class: LuminosityClass::IV,
268        };
269        assert_eq!(class.to_string(), "B0.5IV");
270    }
271
272    #[test]
273    fn test_to_string_g2v() {
274        let class = Classification {
275            spectral_type: SpectralType::G,
276            subtype: 2.0,
277            luminosity_class: LuminosityClass::V,
278        };
279        assert_eq!(class.to_string(), "G2V");
280    }
281
282    #[test]
283    fn test_to_string_with_decimal() {
284        let class = Classification {
285            spectral_type: SpectralType::B,
286            subtype: 2.5,
287            luminosity_class: LuminosityClass::V,
288        };
289        assert_eq!(class.to_string(), "B2.5V");
290    }
291}