stellar_class/
classification.rs1use crate::{error::Error, luminosity::LuminosityClass, spectral_types::SpectralType};
2
3#[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 pub fn from_string(class_str: &str) -> Result<Self, Error> {
16 let class_str = preprocess_class_str(class_str)?;
17
18 let (spectral_str, remaining) = class_str.split_at(1);
20 let spectral_type = spectral_str.try_into()?;
21
22 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 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}