1use 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#[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) {
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 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 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}