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};
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    pub fn label_at(&self, time: u64) -> Option<&Label> {
82        self.labels.iter().find(|l| l.contains(time))
83    }
84
85    /// returns the label whose span contains the time `secs` seconds
86    pub fn label_at_secs(&self, secs: f64) -> Option<&Label> {
87        self.label_at(secs_to_units(secs))
88    }
89
90    /// shifts every time by `offset` in 100ns units, clamping at zero
91    pub fn shift(&mut self, offset: i64) {
92        let apply = |t: u64| -> u64 {
93            if offset >= 0 {
94                t.saturating_add(offset as u64)
95            } else {
96                t.saturating_sub(offset.unsigned_abs())
97            }
98        };
99        for label in &mut self.labels {
100            label.start = label.start.map(apply);
101            label.end = label.end.map(apply);
102        }
103    }
104
105    /// shifts every time by `secs` seconds, clamping at zero
106    pub fn shift_secs(&mut self, secs: f64) {
107        let offset = (secs * crate::UNITS_PER_SECOND as f64).round() as i64;
108        self.shift(offset);
109    }
110
111    /// multiplies every time by `factor`, rounding to the nearest unit
112    pub fn scale(&mut self, factor: f64) {
113        let apply = |t: u64| secs_to_units(units_to_secs(t) * factor);
114        for label in &mut self.labels {
115            label.start = label.start.map(apply);
116            label.end = label.end.map(apply);
117        }
118    }
119
120    /// merges consecutive labels with identical text, merged labels lose
121    /// their scores
122    pub fn merge_adjacent(&mut self) {
123        let mut merged: Vec<Label> = Vec::with_capacity(self.labels.len());
124        for label in self.labels.drain(..) {
125            match merged.last_mut() {
126                Some(prev) if prev.text == label.text => {
127                    prev.end = label.end.or(prev.end);
128                    prev.score = None;
129                }
130                _ => merged.push(label),
131            }
132        }
133        self.labels = merged;
134    }
135
136    /// returns a message for each gap, overlap, or out-of-order pair of
137    /// consecutive labels
138    pub fn validate(&self) -> Vec<String> {
139        let mut problems = Vec::new();
140        for (i, pair) in self.labels.windows(2).enumerate() {
141            let (a, b) = (&pair[0], &pair[1]);
142            let (Some(a_end), Some(b_start)) = (a.end, b.start) else {
143                continue;
144            };
145            if b_start < a_end {
146                let msg = if b.end.is_some_and(|b_end| b_end <= a_end) {
147                    "is out of order with"
148                } else {
149                    "overlaps"
150                };
151                problems.push(format!(
152                    "label {} (`{}`) {} label {} (`{}`)",
153                    i + 2,
154                    b.text,
155                    msg,
156                    i + 1,
157                    a.text,
158                ));
159            } else if b_start > a_end {
160                problems.push(format!(
161                    "gap of {:.4}s between label {} (`{}`) and label {} (`{}`)",
162                    units_to_secs(b_start - a_end),
163                    i + 1,
164                    a.text,
165                    i + 2,
166                    b.text,
167                ));
168            }
169        }
170        problems
171    }
172}
173
174impl fmt::Display for LabFile {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        for label in &self.labels {
177            writeln!(f, "{label}")?;
178        }
179        Ok(())
180    }
181}
182
183impl FromStr for LabFile {
184    type Err = ParseError;
185
186    fn from_str(s: &str) -> Result<Self, Self::Err> {
187        let mut labels = Vec::new();
188        for (i, line) in s.lines().enumerate() {
189            if line.trim().is_empty() {
190                continue;
191            }
192            labels.push(Label::parse_line(line, i + 1)?);
193        }
194        Ok(LabFile { labels })
195    }
196}
197
198impl std::ops::Deref for LabFile {
199    type Target = Vec<Label>;
200
201    fn deref(&self) -> &Self::Target {
202        &self.labels
203    }
204}
205
206impl std::ops::DerefMut for LabFile {
207    fn deref_mut(&mut self) -> &mut Self::Target {
208        &mut self.labels
209    }
210}
211
212impl FromIterator<Label> for LabFile {
213    fn from_iter<I: IntoIterator<Item = Label>>(iter: I) -> Self {
214        LabFile {
215            labels: iter.into_iter().collect(),
216        }
217    }
218}
219
220impl IntoIterator for LabFile {
221    type Item = Label;
222    type IntoIter = std::vec::IntoIter<Label>;
223
224    fn into_iter(self) -> Self::IntoIter {
225        self.labels.into_iter()
226    }
227}
228
229impl<'a> IntoIterator for &'a LabFile {
230    type Item = &'a Label;
231    type IntoIter = std::slice::Iter<'a, Label>;
232
233    fn into_iter(self) -> Self::IntoIter {
234        self.labels.iter()
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    const SAMPLE: &str = "0 1000 a\n1000 2500 b\n2500 4000 b\n";
243
244    fn sample() -> LabFile {
245        SAMPLE.parse().unwrap()
246    }
247
248    #[test]
249    fn parses_and_round_trips() {
250        let lab = sample();
251        assert_eq!(lab.len(), 3);
252        assert_eq!(lab.to_string(), SAMPLE);
253    }
254
255    #[test]
256    fn skips_blank_lines_and_reports_line_numbers() {
257        let lab: LabFile = "0 10 a\n\n  \n10 20 b\n".parse().unwrap();
258        assert_eq!(lab.len(), 2);
259        let err = "0 10 a\n\n20 10 b\n".parse::<LabFile>().unwrap_err();
260        assert_eq!(err.line, 3);
261    }
262
263    #[test]
264    fn duration_and_lookup() {
265        let lab = sample();
266        assert_eq!(lab.duration(), Some(4000));
267        assert_eq!(lab.label_at(1500).unwrap().text, "b");
268        assert_eq!(lab.label_at(4000), None);
269    }
270
271    #[test]
272    fn shift_clamps_at_zero() {
273        let mut lab = sample();
274        lab.shift(-1500);
275        assert_eq!(lab[0].start, Some(0));
276        assert_eq!(lab[0].end, Some(0));
277        assert_eq!(lab[1].start, Some(0));
278        assert_eq!(lab[2].end, Some(2500));
279    }
280
281    #[test]
282    fn scale_doubles_times() {
283        let mut lab = sample();
284        lab.scale(2.0);
285        assert_eq!(lab[2].end, Some(8000));
286    }
287
288    #[test]
289    fn merges_adjacent_identical_labels() {
290        let mut lab = sample();
291        lab.merge_adjacent();
292        assert_eq!(lab.len(), 2);
293        assert_eq!(lab[1], Label::new(1000, 4000, "b"));
294    }
295
296    #[test]
297    fn validate_finds_gaps_overlaps_and_disorder() {
298        let lab: LabFile = "0 10 a\n5 20 b\n30 40 c\n32 35 d\n".parse().unwrap();
299        let problems = lab.validate();
300        assert_eq!(problems.len(), 3);
301        assert!(problems[0].contains("overlaps"));
302        assert!(problems[1].contains("gap"));
303        assert!(problems[2].contains("out of order"));
304        assert!(sample().validate().is_empty());
305    }
306
307    #[test]
308    fn reader_and_writer_round_trip() {
309        let lab = LabFile::from_reader(SAMPLE.as_bytes()).unwrap();
310        let mut out = Vec::new();
311        lab.write_to(&mut out).unwrap();
312        assert_eq!(out, SAMPLE.as_bytes());
313    }
314}