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> {
82 self.labels.iter().find(|l| l.contains(time))
83 }
84
85 pub fn label_at_secs(&self, secs: f64) -> Option<&Label> {
87 self.label_at(secs_to_units(secs))
88 }
89
90 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 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 pub fn scale(&mut self, factor: f64) -> Result<(), ScaleError> {
114 if !factor.is_finite() {
115 return Err(ScaleError::NonFiniteFactor);
116 }
117 if factor < 0.0 {
118 return Err(ScaleError::NegativeFactor);
119 }
120 if factor == 1.0 {
121 return Ok(());
122 }
123 let apply = |t: u64| secs_to_units(units_to_secs(t) * factor);
124 for label in &mut self.labels {
125 label.start = label.start.map(apply);
126 label.end = label.end.map(apply);
127 }
128 Ok(())
129 }
130
131 pub fn merge_adjacent(&mut self) {
134 let mut merged: Vec<Label> = Vec::with_capacity(self.labels.len());
135 for label in self.labels.drain(..) {
136 match merged.last_mut() {
137 Some(prev)
138 if prev.text == label.text
139 && (matches!((prev.end, label.start), (Some(end), Some(start)) if end == start)
140 || matches!((prev.end, label.start), (None, _) | (_, None))) =>
141 {
142 prev.end = label.end.or(prev.end);
143 prev.score = None;
144 }
145 _ => merged.push(label),
146 }
147 }
148 self.labels = merged;
149 }
150
151 pub fn validate(&self) -> Vec<String> {
154 let mut problems = Vec::new();
155 for (i, pair) in self.labels.windows(2).enumerate() {
156 let (a, b) = (&pair[0], &pair[1]);
157 let (Some(a_end), Some(b_start)) = (a.end, b.start) else {
158 continue;
159 };
160 if b_start < a_end {
161 let msg = if b.end.is_some_and(|b_end| b_end <= a_end) {
162 "is out of order with"
163 } else {
164 "overlaps"
165 };
166 problems.push(format!(
167 "label {} (`{}`) {} label {} (`{}`)",
168 i + 2,
169 b.text,
170 msg,
171 i + 1,
172 a.text,
173 ));
174 } else if b_start > a_end {
175 problems.push(format!(
176 "gap of {:.4}s between label {} (`{}`) and label {} (`{}`)",
177 units_to_secs(b_start - a_end),
178 i + 1,
179 a.text,
180 i + 2,
181 b.text,
182 ));
183 }
184 }
185 problems
186 }
187}
188
189impl fmt::Display for LabFile {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 for label in &self.labels {
192 writeln!(f, "{label}")?;
193 }
194 Ok(())
195 }
196}
197
198impl FromStr for LabFile {
199 type Err = ParseError;
200
201 fn from_str(s: &str) -> Result<Self, Self::Err> {
202 let mut labels = Vec::new();
203 for (i, line) in s.lines().enumerate() {
204 if line.trim().is_empty() {
205 continue;
206 }
207 labels.push(Label::parse_line(line, i + 1)?);
208 }
209 Ok(LabFile { labels })
210 }
211}
212
213impl std::ops::Deref for LabFile {
214 type Target = Vec<Label>;
215
216 fn deref(&self) -> &Self::Target {
217 &self.labels
218 }
219}
220
221impl std::ops::DerefMut for LabFile {
222 fn deref_mut(&mut self) -> &mut Self::Target {
223 &mut self.labels
224 }
225}
226
227impl FromIterator<Label> for LabFile {
228 fn from_iter<I: IntoIterator<Item = Label>>(iter: I) -> Self {
229 LabFile {
230 labels: iter.into_iter().collect(),
231 }
232 }
233}
234
235impl IntoIterator for LabFile {
236 type Item = Label;
237 type IntoIter = std::vec::IntoIter<Label>;
238
239 fn into_iter(self) -> Self::IntoIter {
240 self.labels.into_iter()
241 }
242}
243
244impl<'a> IntoIterator for &'a LabFile {
245 type Item = &'a Label;
246 type IntoIter = std::slice::Iter<'a, Label>;
247
248 fn into_iter(self) -> Self::IntoIter {
249 self.labels.iter()
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 const SAMPLE: &str = "0 1000 a\n1000 2500 b\n2500 4000 b\n";
258
259 fn sample() -> LabFile {
260 SAMPLE.parse().unwrap()
261 }
262
263 #[test]
264 fn parses_and_round_trips() {
265 let lab = sample();
266 assert_eq!(lab.len(), 3);
267 assert_eq!(lab.to_string(), SAMPLE);
268 }
269
270 #[test]
271 fn skips_blank_lines_and_reports_line_numbers() {
272 let lab: LabFile = "0 10 a\n\n \n10 20 b\n".parse().unwrap();
273 assert_eq!(lab.len(), 2);
274 let err = "0 10 a\n\n20 10 b\n".parse::<LabFile>().unwrap_err();
275 assert_eq!(err.line, 3);
276 }
277
278 #[test]
279 fn duration_and_lookup() {
280 let lab = sample();
281 assert_eq!(lab.duration(), Some(4000));
282 assert_eq!(lab.label_at(1500).unwrap().text, "b");
283 assert_eq!(lab.label_at(4000), None);
284 }
285
286 #[test]
287 fn shift_clamps_at_zero() {
288 let mut lab = sample();
289 lab.shift(-1500);
290 assert_eq!(lab[0].start, Some(0));
291 assert_eq!(lab[0].end, Some(0));
292 assert_eq!(lab[1].start, Some(0));
293 assert_eq!(lab[2].end, Some(2500));
294 }
295
296 #[test]
297 fn scale_doubles_times() {
298 let mut lab = sample();
299 lab.scale(2.0).unwrap();
300 assert_eq!(lab[2].end, Some(8000));
301 }
302
303 #[test]
304 fn merges_adjacent_identical_labels() {
305 let mut lab = sample();
306 lab.merge_adjacent();
307 assert_eq!(lab.len(), 2);
308 assert_eq!(lab[1], Label::new(1000, 4000, "b"));
309 }
310
311 #[test]
312 fn merge_does_not_cross_gaps_or_overlaps() {
313 let mut gapped: LabFile = "0 10 a\n20 30 a\n".parse().unwrap();
314 gapped.merge_adjacent();
315 assert_eq!(gapped.len(), 2);
316
317 let mut nested: LabFile = "0 100 a\n10 20 a\n".parse().unwrap();
318 nested.merge_adjacent();
319 assert_eq!(nested.len(), 2);
320 }
321
322 #[test]
323 fn scale_rejects_invalid_factors() {
324 let mut lab = sample();
325 assert_eq!(lab.scale(f64::NAN), Err(ScaleError::NonFiniteFactor));
326 assert_eq!(lab.scale(-1.0), Err(ScaleError::NegativeFactor));
327 assert_eq!(lab, sample());
328 }
329
330 #[test]
331 fn validate_finds_gaps_overlaps_and_disorder() {
332 let lab: LabFile = "0 10 a\n5 20 b\n30 40 c\n32 35 d\n".parse().unwrap();
333 let problems = lab.validate();
334 assert_eq!(problems.len(), 3);
335 assert!(problems[0].contains("overlaps"));
336 assert!(problems[1].contains("gap"));
337 assert!(problems[2].contains("out of order"));
338 assert!(sample().validate().is_empty());
339 }
340
341 #[test]
342 fn reader_and_writer_round_trip() {
343 let lab = LabFile::from_reader(SAMPLE.as_bytes()).unwrap();
344 let mut out = Vec::new();
345 lab.write_to(&mut out).unwrap();
346 assert_eq!(out, SAMPLE.as_bytes());
347 }
348}