1use std::fmt;
4use std::io::{BufRead, BufReader, Read, Write};
5use std::path::Path;
6use std::str::FromStr;
7
8use crate::error::{ParseError, ReadError, ScaleError};
9use crate::label::{secs_to_units, units_to_secs, Label};
10
11#[derive(Debug, Clone, Default, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "serde", serde(transparent))]
15pub struct LabFile {
16 pub labels: Vec<Label>,
18}
19
20impl LabFile {
21 pub fn new() -> Self {
23 Self::default()
24 }
25
26 pub fn from_reader<R: Read>(reader: R) -> Result<Self, ReadError> {
28 let mut labels = Vec::new();
29 for (i, line) in BufReader::new(reader).lines().enumerate() {
30 let line = line?;
31 if line.trim().is_empty() {
32 continue;
33 }
34 labels.push(Label::parse_line(&line, i + 1)?);
35 }
36 Ok(LabFile { labels })
37 }
38
39 pub fn from_path(path: impl AsRef<Path>) -> Result<Self, ReadError> {
41 Self::from_reader(std::fs::File::open(path)?)
42 }
43
44 pub fn write_to<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
46 for label in &self.labels {
47 writeln!(writer, "{label}")?;
48 }
49 Ok(())
50 }
51
52 pub fn save(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
54 self.write_to(std::fs::File::create(path)?)
55 }
56
57 pub fn start(&self) -> Option<u64> {
59 self.labels.iter().filter_map(|l| l.start).min()
60 }
61
62 pub fn end(&self) -> Option<u64> {
64 self.labels.iter().filter_map(|l| l.end).max()
65 }
66
67 pub fn duration(&self) -> Option<u64> {
69 match (self.start(), self.end()) {
70 (Some(s), Some(e)) => Some(e.saturating_sub(s)),
71 _ => None,
72 }
73 }
74
75 pub fn duration_secs(&self) -> Option<f64> {
77 self.duration().map(units_to_secs)
78 }
79
80 pub fn label_at(&self, time: u64) -> Option<&Label> {
83 self.labels.iter().find(|l| l.contains(time))
84 }
85
86 pub fn label_at_secs(&self, secs: f64) -> Option<&Label> {
88 self.label_at(secs_to_units(secs))
89 }
90
91 pub fn labels_at(&self, time: u64) -> Vec<&Label> {
94 self.labels.iter().filter(|l| l.contains(time)).collect()
95 }
96
97 pub fn labels_at_secs(&self, secs: f64) -> Vec<&Label> {
100 self.labels_at(secs_to_units(secs))
101 }
102
103 pub fn overlapping_pairs(&self) -> Vec<(usize, &Label, &Label)> {
107 let mut overlaps = Vec::new();
108 for (i, pair) in self.labels.windows(2).enumerate() {
109 let (a, b) = (&pair[0], &pair[1]);
110 if let (Some(a_end), Some(b_start)) = (a.end, b.start) {
111 if b_start < a_end {
112 overlaps.push((i + 1, a, b));
113 }
114 }
115 }
116 overlaps
117 }
118
119 pub fn shift(&mut self, offset: i64) {
121 let apply = |t: u64| -> u64 {
122 if offset >= 0 {
123 t.saturating_add(offset as u64)
124 } else {
125 t.saturating_sub(offset.unsigned_abs())
126 }
127 };
128 for label in &mut self.labels {
129 label.start = label.start.map(apply);
130 label.end = label.end.map(apply);
131 }
132 }
133
134 pub fn shift_secs(&mut self, secs: f64) {
136 let offset = (secs * crate::UNITS_PER_SECOND as f64).round() as i64;
137 self.shift(offset);
138 }
139
140 pub fn scale(&mut self, factor: f64) -> Result<(), ScaleError> {
143 if !factor.is_finite() {
144 return Err(ScaleError::NonFiniteFactor);
145 }
146 if factor < 0.0 {
147 return Err(ScaleError::NegativeFactor);
148 }
149 if factor == 1.0 {
150 return Ok(());
151 }
152 let apply = |t: u64| secs_to_units(units_to_secs(t) * factor);
153 for label in &mut self.labels {
154 label.start = label.start.map(apply);
155 label.end = label.end.map(apply);
156 }
157 Ok(())
158 }
159
160 pub fn merge_adjacent(&mut self) {
163 let mut merged: Vec<Label> = Vec::with_capacity(self.labels.len());
164 for label in self.labels.drain(..) {
165 match merged.last_mut() {
166 Some(prev)
167 if prev.text == label.text
168 && (matches!((prev.end, label.start), (Some(end), Some(start)) if end == start)
169 || matches!((prev.end, label.start), (None, _) | (_, None))) =>
170 {
171 prev.end = label.end.or(prev.end);
172 prev.score = None;
173 }
174 _ => merged.push(label),
175 }
176 }
177 self.labels = merged;
178 }
179
180 pub fn validate(&self) -> Vec<String> {
183 let mut problems = Vec::new();
184 for (i, pair) in self.labels.windows(2).enumerate() {
185 let (a, b) = (&pair[0], &pair[1]);
186 let (Some(a_end), Some(b_start)) = (a.end, b.start) else {
187 continue;
188 };
189 if b_start < a_end {
190 let msg = if b.end.is_some_and(|b_end| b_end <= a_end) {
191 "is out of order with"
192 } else {
193 "overlaps"
194 };
195 problems.push(format!(
196 "label {} (`{}`) {} label {} (`{}`)",
197 i + 2,
198 b.text,
199 msg,
200 i + 1,
201 a.text,
202 ));
203 } else if b_start > a_end {
204 problems.push(format!(
205 "gap of {:.4}s between label {} (`{}`) and label {} (`{}`)",
206 units_to_secs(b_start - a_end),
207 i + 1,
208 a.text,
209 i + 2,
210 b.text,
211 ));
212 }
213 }
214 problems
215 }
216}
217
218impl fmt::Display for LabFile {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 for label in &self.labels {
221 writeln!(f, "{label}")?;
222 }
223 Ok(())
224 }
225}
226
227impl FromStr for LabFile {
228 type Err = ParseError;
229
230 fn from_str(s: &str) -> Result<Self, Self::Err> {
231 let mut labels = Vec::new();
232 for (i, line) in s.lines().enumerate() {
233 if line.trim().is_empty() {
234 continue;
235 }
236 labels.push(Label::parse_line(line, i + 1)?);
237 }
238 Ok(LabFile { labels })
239 }
240}
241
242impl std::ops::Deref for LabFile {
243 type Target = Vec<Label>;
244
245 fn deref(&self) -> &Self::Target {
246 &self.labels
247 }
248}
249
250impl std::ops::DerefMut for LabFile {
251 fn deref_mut(&mut self) -> &mut Self::Target {
252 &mut self.labels
253 }
254}
255
256impl FromIterator<Label> for LabFile {
257 fn from_iter<I: IntoIterator<Item = Label>>(iter: I) -> Self {
258 LabFile {
259 labels: iter.into_iter().collect(),
260 }
261 }
262}
263
264impl IntoIterator for LabFile {
265 type Item = Label;
266 type IntoIter = std::vec::IntoIter<Label>;
267
268 fn into_iter(self) -> Self::IntoIter {
269 self.labels.into_iter()
270 }
271}
272
273impl<'a> IntoIterator for &'a LabFile {
274 type Item = &'a Label;
275 type IntoIter = std::slice::Iter<'a, Label>;
276
277 fn into_iter(self) -> Self::IntoIter {
278 self.labels.iter()
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 const SAMPLE: &str = "0 1000 a\n1000 2500 b\n2500 4000 b\n";
287
288 fn sample() -> LabFile {
289 SAMPLE.parse().unwrap()
290 }
291
292 #[test]
293 fn parses_and_round_trips() {
294 let lab = sample();
295 assert_eq!(lab.len(), 3);
296 assert_eq!(lab.to_string(), SAMPLE);
297 }
298
299 #[test]
300 fn skips_blank_lines_and_reports_line_numbers() {
301 let lab: LabFile = "0 10 a\n\n \n10 20 b\n".parse().unwrap();
302 assert_eq!(lab.len(), 2);
303 let err = "0 10 a\n\n20 10 b\n".parse::<LabFile>().unwrap_err();
304 assert_eq!(err.line, 3);
305 }
306
307 #[test]
308 fn duration_and_lookup() {
309 let lab = sample();
310 assert_eq!(lab.duration(), Some(4000));
311 assert_eq!(lab.label_at(1500).unwrap().text, "b");
312 assert_eq!(lab.label_at(4000), None);
313 }
314
315 #[test]
316 fn shift_clamps_at_zero() {
317 let mut lab = sample();
318 lab.shift(-1500);
319 assert_eq!(lab[0].start, Some(0));
320 assert_eq!(lab[0].end, Some(0));
321 assert_eq!(lab[1].start, Some(0));
322 assert_eq!(lab[2].end, Some(2500));
323 }
324
325 #[test]
326 fn scale_doubles_times() {
327 let mut lab = sample();
328 lab.scale(2.0).unwrap();
329 assert_eq!(lab[2].end, Some(8000));
330 }
331
332 #[test]
333 fn merges_adjacent_identical_labels() {
334 let mut lab = sample();
335 lab.merge_adjacent();
336 assert_eq!(lab.len(), 2);
337 assert_eq!(lab[1], Label::new(1000, 4000, "b"));
338 }
339
340 #[test]
341 fn merge_does_not_cross_gaps_or_overlaps() {
342 let mut gapped: LabFile = "0 10 a\n20 30 a\n".parse().unwrap();
343 gapped.merge_adjacent();
344 assert_eq!(gapped.len(), 2);
345
346 let mut nested: LabFile = "0 100 a\n10 20 a\n".parse().unwrap();
347 nested.merge_adjacent();
348 assert_eq!(nested.len(), 2);
349 }
350
351 #[test]
352 fn scale_rejects_invalid_factors() {
353 let mut lab = sample();
354 assert_eq!(lab.scale(f64::NAN), Err(ScaleError::NonFiniteFactor));
355 assert_eq!(lab.scale(-1.0), Err(ScaleError::NegativeFactor));
356 assert_eq!(lab, sample());
357 }
358
359 #[test]
360 fn validate_finds_gaps_overlaps_and_disorder() {
361 let lab: LabFile = "0 10 a\n5 20 b\n30 40 c\n32 35 d\n".parse().unwrap();
362 let problems = lab.validate();
363 assert_eq!(problems.len(), 3);
364 assert!(problems[0].contains("overlaps"));
365 assert!(problems[1].contains("gap"));
366 assert!(problems[2].contains("out of order"));
367 assert!(sample().validate().is_empty());
368 }
369
370 #[test]
371 fn reader_and_writer_round_trip() {
372 let lab = LabFile::from_reader(SAMPLE.as_bytes()).unwrap();
373 let mut out = Vec::new();
374 lab.write_to(&mut out).unwrap();
375 assert_eq!(out, SAMPLE.as_bytes());
376 }
377}