Skip to main content

lab_rs/
file.rs

1//! a parsed .lab: an ordered sequence of [`Label`]s
2
3use 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/// a parsed `.lab` file, derefs to `Vec<Label>`
12#[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    /// the labels in file order
17    pub labels: Vec<Label>,
18}
19
20impl LabFile {
21    /// creates an empty label file
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// reads and parses a .lab from a reader
27    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    /// reads and parses the .lab at `path`
40    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, ReadError> {
41        Self::from_reader(std::fs::File::open(path)?)
42    }
43
44    /// writes the labels to a writer in .lab format
45    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    /// writes the labels to the file at `path`, replacing its contents
53    pub fn save(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
54        self.write_to(std::fs::File::create(path)?)
55    }
56
57    /// earliest start time across all labels, in 100ns units
58    pub fn start(&self) -> Option<u64> {
59        self.labels.iter().filter_map(|l| l.start).min()
60    }
61
62    /// latest end time across all labels, in 100ns units
63    pub fn end(&self) -> Option<u64> {
64        self.labels.iter().filter_map(|l| l.end).max()
65    }
66
67    /// total time span (earliest start to latest end) in 100ns units
68    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    /// total time span in seconds
76    pub fn duration_secs(&self) -> Option<f64> {
77        self.duration().map(units_to_secs)
78    }
79
80    /// returns the label whose span contains `time` (in 100ns units)
81    /// label matches when `start <= time < end`
82    pub fn label_at(&self, time: u64) -> Option<&Label> {
83        self.labels.iter().find(|l| l.contains(time))
84    }
85
86    /// returns the label whose span contains the time `secs` seconds
87    pub fn label_at_secs(&self, secs: f64) -> Option<&Label> {
88        self.label_at(secs_to_units(secs))
89    }
90
91    /// returns labels overlapping `time` (in 100ns units)
92    /// label matches when `start <= time < end`
93    pub fn labels_at(&self, time: u64) -> Vec<&Label> {
94        self.labels.iter().filter(|l| l.contains(time)).collect()
95    }
96
97    /// returns labels overlapping the time `secs` in seconds
98    /// label matches when `start <= time < end`
99    pub fn labels_at_secs(&self, secs: f64) -> Vec<&Label> {
100        self.labels_at(secs_to_units(secs))
101    }
102
103    /// finds pairs of consecutive labels that overlap or are out of order
104    /// adjacent labels sharing a boundary (`a.end == b.start`) are fine
105    /// returns (index of the second label, first label, second label) per problematic pair
106    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    /// shifts every time by `offset` in 100ns units, clamping at zero
120    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    /// shifts every time by `secs` seconds, clamping at zero
135    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    /// multiplies every time by a non-negative finite `factor`, rounding to
141    /// the nearest unit
142    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    /// merges consecutive labels with identical text, merged labels lose
161    /// their scores
162    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    /// returns a message for each gap, overlap, or out-of-order pair of
181    /// consecutive labels
182    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}