1use std::path::Path;
13
14use crate::error::IoError;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SpectrumUnit {
19 TofMicroseconds,
21 EnergyEv,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum SpectrumValueKind {
28 BinEdges,
30 BinCenters,
32}
33
34pub fn parse_spectrum_file(path: &Path) -> Result<Vec<f64>, IoError> {
51 let content = std::fs::read_to_string(path)
52 .map_err(|e| IoError::FileNotFound(path.to_string_lossy().into_owned(), e))?;
53 parse_spectrum_text(&content)
54}
55
56pub fn parse_spectrum_text(text: &str) -> Result<Vec<f64>, IoError> {
62 let mut values = Vec::new();
63 let mut skipped_header = false;
64
65 for line in text.lines() {
66 let trimmed = line.trim();
67 if trimmed.is_empty() || trimmed.starts_with('#') {
68 continue;
69 }
70 let first_token = trimmed
72 .split(|c: char| c == ',' || c == '\t' || c.is_ascii_whitespace())
73 .next()
74 .unwrap_or("")
75 .trim();
76
77 match first_token.parse::<f64>() {
78 Ok(val) => {
79 if !val.is_finite() {
80 return Err(IoError::InvalidParameter(format!(
81 "Non-finite value in spectrum file: {}",
82 val
83 )));
84 }
85 values.push(val);
86 }
87 Err(_) => {
88 if !skipped_header && values.is_empty() {
89 skipped_header = true;
90 continue;
91 }
92 return Err(IoError::InvalidParameter(format!(
93 "Unparseable value in spectrum file: '{}'",
94 first_token
95 )));
96 }
97 }
98 }
99
100 if values.len() < 2 {
101 return Err(IoError::InvalidParameter(
102 "Spectrum file must contain at least 2 values".into(),
103 ));
104 }
105
106 Ok(values)
107}
108
109pub fn validate_spectrum_frame_count(
114 n_values: usize,
115 n_frames: usize,
116 kind: SpectrumValueKind,
117) -> Result<(), IoError> {
118 let expected = match kind {
119 SpectrumValueKind::BinEdges => n_frames + 1,
120 SpectrumValueKind::BinCenters => n_frames,
121 };
122 if n_values != expected {
123 return Err(IoError::InvalidParameter(format!(
124 "Spectrum has {} values but TIFF has {} frames (expected {} for {:?})",
125 n_values, n_frames, expected, kind,
126 )));
127 }
128 Ok(())
129}
130
131pub fn validate_monotonic(values: &[f64]) -> Result<(), IoError> {
133 for window in values.windows(2) {
134 match window[0].partial_cmp(&window[1]) {
135 Some(std::cmp::Ordering::Less) => {} _ => {
137 return Err(IoError::InvalidParameter(format!(
139 "Spectrum values must be strictly increasing, but found {} >= {}",
140 window[0], window[1],
141 )));
142 }
143 }
144 }
145 Ok(())
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_parse_single_column() {
154 let text = "1000.0\n2000.0\n3000.0\n4000.0\n";
155 let values = parse_spectrum_text(text).unwrap();
156 assert_eq!(values, vec![1000.0, 2000.0, 3000.0, 4000.0]);
157 }
158
159 #[test]
160 fn test_parse_two_column_csv() {
161 let text = "1000.0,0.5\n2000.0,0.6\n3000.0,0.7\n";
162 let values = parse_spectrum_text(text).unwrap();
163 assert_eq!(values, vec![1000.0, 2000.0, 3000.0]);
164 }
165
166 #[test]
167 fn test_parse_whitespace_separated() {
168 let text = "1000.0 0.5\n2000.0 0.6\n3000.0 0.7\n";
169 let values = parse_spectrum_text(text).unwrap();
170 assert_eq!(values, vec![1000.0, 2000.0, 3000.0]);
171 }
172
173 #[test]
174 fn test_parse_comments_and_header() {
175 let text = "\
176# This is a comment
177# Another comment
178TOF_us, intensity
1791000.0, 0.5
1802000.0, 0.6
1813000.0, 0.7
182";
183 let values = parse_spectrum_text(text).unwrap();
184 assert_eq!(values, vec![1000.0, 2000.0, 3000.0]);
185 }
186
187 #[test]
188 fn test_parse_tab_separated() {
189 let text = "1000.0\t0.5\n2000.0\t0.6\n3000.0\t0.7\n";
190 let values = parse_spectrum_text(text).unwrap();
191 assert_eq!(values, vec![1000.0, 2000.0, 3000.0]);
192 }
193
194 #[test]
195 fn test_parse_empty_lines_ignored() {
196 let text = "\n1000.0\n\n2000.0\n\n3000.0\n\n";
197 let values = parse_spectrum_text(text).unwrap();
198 assert_eq!(values, vec![1000.0, 2000.0, 3000.0]);
199 }
200
201 #[test]
202 fn test_parse_too_few_values() {
203 let text = "1000.0\n";
204 let result = parse_spectrum_text(text);
205 assert!(result.is_err());
206 assert!(
207 format!("{}", result.unwrap_err()).contains("at least 2"),
208 "Expected 'at least 2' error"
209 );
210 }
211
212 #[test]
213 fn test_parse_non_finite_value() {
214 let text = "1000.0\nNaN\n3000.0\n";
215 let result = parse_spectrum_text(text);
216 assert!(result.is_err());
217 assert!(
218 format!("{}", result.unwrap_err()).contains("Non-finite"),
219 "Expected non-finite error"
220 );
221 }
222
223 #[test]
224 fn test_parse_unparseable_after_data() {
225 let text = "1000.0\n2000.0\nbad_value\n";
226 let result = parse_spectrum_text(text);
227 assert!(result.is_err());
228 assert!(
229 format!("{}", result.unwrap_err()).contains("Unparseable"),
230 "Expected unparseable error"
231 );
232 }
233
234 #[test]
235 fn test_validate_frame_count_edges() {
236 assert!(validate_spectrum_frame_count(6, 5, SpectrumValueKind::BinEdges).is_ok());
238 assert!(validate_spectrum_frame_count(5, 5, SpectrumValueKind::BinEdges).is_err());
239 assert!(validate_spectrum_frame_count(7, 5, SpectrumValueKind::BinEdges).is_err());
240 }
241
242 #[test]
243 fn test_validate_frame_count_centers() {
244 assert!(validate_spectrum_frame_count(5, 5, SpectrumValueKind::BinCenters).is_ok());
246 assert!(validate_spectrum_frame_count(6, 5, SpectrumValueKind::BinCenters).is_err());
247 }
248
249 #[test]
250 fn test_validate_monotonic_ok() {
251 assert!(validate_monotonic(&[1.0, 2.0, 3.0, 4.0]).is_ok());
252 }
253
254 #[test]
255 fn test_validate_monotonic_equal() {
256 let result = validate_monotonic(&[1.0, 2.0, 2.0, 4.0]);
257 assert!(result.is_err());
258 }
259
260 #[test]
261 fn test_validate_monotonic_decreasing() {
262 let result = validate_monotonic(&[1.0, 3.0, 2.0, 4.0]);
263 assert!(result.is_err());
264 }
265
266 #[test]
267 fn test_validate_monotonic_nan() {
268 let result = validate_monotonic(&[1.0, f64::NAN, 3.0]);
269 assert!(result.is_err(), "NaN should fail monotonicity check");
270 }
271
272 #[test]
273 fn test_parse_spectrum_file_not_found() {
274 let result = parse_spectrum_file(Path::new("/nonexistent/spectrum.csv"));
275 assert!(result.is_err());
276 }
277}