1use std::fmt;
4use std::str::FromStr;
5
6use crate::error::{ParseError, ParseErrorKind};
7
8pub const UNITS_PER_SECOND: u64 = 10_000_000;
10
11#[derive(Debug, Clone, PartialEq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub struct Label {
16 #[cfg_attr(
18 feature = "serde",
19 serde(default, skip_serializing_if = "Option::is_none")
20 )]
21 pub start: Option<u64>,
22 #[cfg_attr(
24 feature = "serde",
25 serde(default, skip_serializing_if = "Option::is_none")
26 )]
27 pub end: Option<u64>,
28 pub text: String,
30 #[cfg_attr(
32 feature = "serde",
33 serde(default, skip_serializing_if = "Option::is_none")
34 )]
35 pub score: Option<f64>,
36}
37
38impl Label {
39 pub fn new(start: u64, end: u64, text: impl Into<String>) -> Self {
41 Label {
42 start: Some(start),
43 end: Some(end),
44 text: text.into(),
45 score: None,
46 }
47 }
48
49 pub fn from_secs(start: f64, end: f64, text: impl Into<String>) -> Self {
51 Label::new(secs_to_units(start), secs_to_units(end), text)
52 }
53
54 pub fn start_secs(&self) -> Option<f64> {
56 self.start.map(units_to_secs)
57 }
58
59 pub fn end_secs(&self) -> Option<f64> {
61 self.end.map(units_to_secs)
62 }
63
64 pub fn duration(&self) -> Option<u64> {
66 match (self.start, self.end) {
67 (Some(s), Some(e)) => Some(e.saturating_sub(s)),
68 _ => None,
69 }
70 }
71
72 pub fn duration_secs(&self) -> Option<f64> {
74 self.duration().map(units_to_secs)
75 }
76
77 pub fn contains(&self, time: u64) -> bool {
79 matches!((self.start, self.end), (Some(s), Some(e)) if s <= time && time < e)
80 }
81
82 pub(crate) fn parse_line(line: &str, line_no: usize) -> Result<Self, ParseError> {
84 let tokens: Vec<&str> = line.split_whitespace().collect();
85 debug_assert!(!tokens.is_empty());
86
87 let mut idx = 0;
88 let mut start = None;
89 let mut end = None;
90 while idx < tokens.len() - 1 && idx < 2 {
93 match parse_time(tokens[idx], line_no)? {
94 Some(t) if idx == 0 => start = Some(t),
95 Some(t) => end = Some(t),
96 None => break,
97 }
98 idx += 1;
99 }
100
101 if let (Some(s), Some(e)) = (start, end) {
102 if s > e {
103 return Err(ParseError {
104 line: line_no,
105 kind: ParseErrorKind::StartAfterEnd { start: s, end: e },
106 });
107 }
108 }
109
110 let rest = &tokens[idx..];
111 let (text_tokens, score) = match rest.split_last() {
112 Some((last, init)) if !init.is_empty() => match last.parse::<f64>() {
114 Ok(score) => (init, Some(score)),
115 Err(_) => (rest, None),
116 },
117 _ => (rest, None),
118 };
119
120 Ok(Label {
121 start,
122 end,
123 text: text_tokens.join(" "),
124 score,
125 })
126 }
127}
128
129impl fmt::Display for Label {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 if let Some(s) = self.start {
132 write!(f, "{s} ")?;
133 }
134 if let Some(e) = self.end {
135 write!(f, "{e} ")?;
136 }
137 f.write_str(&self.text)?;
138 if let Some(score) = self.score {
139 write!(f, " {score}")?;
140 }
141 Ok(())
142 }
143}
144
145impl FromStr for Label {
146 type Err = ParseError;
147
148 fn from_str(s: &str) -> Result<Self, Self::Err> {
149 Label::parse_line(s, 1)
150 }
151}
152
153pub fn units_to_secs(units: u64) -> f64 {
155 units as f64 / UNITS_PER_SECOND as f64
156}
157
158pub fn secs_to_units(secs: f64) -> u64 {
160 (secs * UNITS_PER_SECOND as f64).round().max(0.0) as u64
161}
162
163fn parse_time(token: &str, line_no: usize) -> Result<Option<u64>, ParseError> {
166 match token.parse::<u64>() {
167 Ok(t) => Ok(Some(t)),
168 Err(_) if token.bytes().all(|b| b.is_ascii_digit()) => Err(ParseError {
169 line: line_no,
170 kind: ParseErrorKind::InvalidTime(token.to_string()),
171 }),
172 Err(_) => Ok(None),
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn parses_full_line() {
182 let l: Label = "0 23823130 pau".parse().unwrap();
183 assert_eq!(l, Label::new(0, 23823130, "pau"));
184 }
185
186 #[test]
187 fn parses_score() {
188 let l: Label = "0 100 a -42.5".parse().unwrap();
189 assert_eq!(l.score, Some(-42.5));
190 assert_eq!(l.text, "a");
191 }
192
193 #[test]
194 fn parses_label_only() {
195 let l: Label = "sil".parse().unwrap();
196 assert_eq!((l.start, l.end), (None, None));
197 assert_eq!(l.text, "sil");
198 }
199
200 #[test]
201 fn parses_single_time() {
202 let l: Label = "100 sil".parse().unwrap();
203 assert_eq!((l.start, l.end), (Some(100), None));
204 }
205
206 #[test]
207 fn numeric_only_line_is_a_label() {
208 let l: Label = "100 200".parse().unwrap();
210 assert_eq!((l.start, l.end), (Some(100), None));
211 assert_eq!(l.text, "200");
212 }
213
214 #[test]
215 fn rejects_start_after_end() {
216 let err = "200 100 a".parse::<Label>().unwrap_err();
217 assert_eq!(
218 err.kind,
219 ParseErrorKind::StartAfterEnd {
220 start: 200,
221 end: 100
222 }
223 );
224 }
225
226 #[test]
227 fn rejects_overflowing_time() {
228 let err = "99999999999999999999999 100 a"
229 .parse::<Label>()
230 .unwrap_err();
231 assert!(matches!(err.kind, ParseErrorKind::InvalidTime(_)));
232 }
233
234 #[test]
235 fn display_round_trips() {
236 for line in ["0 23823130 pau", "100 sil", "sil", "0 100 a -42.5"] {
237 let l: Label = line.parse().unwrap();
238 assert_eq!(l.to_string(), line);
239 }
240 }
241
242 #[test]
243 fn seconds_conversion() {
244 let l = Label::from_secs(1.0, 2.5, "a");
245 assert_eq!(l.start, Some(10_000_000));
246 assert_eq!(l.end, Some(25_000_000));
247 assert_eq!(l.duration_secs(), Some(1.5));
248 assert!(l.contains(15_000_000));
249 assert!(!l.contains(25_000_000));
250 }
251}