ffmpeg_sidecar/
ffmpeg_time_duration.rs1use std::borrow::Cow;
2use std::convert::TryFrom;
3use std::fmt;
4use std::ops::{Add, Sub};
5use std::time::Duration;
6
7const MICROS_PER_SEC: f64 = 1_000_000.0;
8const MILLIS_PER_SEC: f64 = 1_000.0;
9
10#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
46pub struct FfmpegTimeDuration(i64);
47
48impl FfmpegTimeDuration {
49 #[must_use]
50 #[inline]
51 pub fn new(microseconds: i64) -> Self {
52 Self::from_micros(microseconds)
53 }
54
55 #[must_use]
56 #[inline]
57 pub fn as_micros(&self) -> i64 {
58 self.0
59 }
60
61 #[must_use]
62 #[inline]
63 pub fn from_micros(microseconds: i64) -> Self {
64 Self(microseconds)
65 }
66
67 #[must_use]
68 #[inline]
69 pub fn as_seconds(self) -> f64 {
70 self.0 as f64 / MICROS_PER_SEC
71 }
72
73 #[must_use]
74 #[inline]
75 pub fn from_seconds(seconds: f64) -> Self {
76 Self::from_micros((seconds * MICROS_PER_SEC) as i64)
77 }
78
79 #[must_use]
80 pub fn from_str(str: &str) -> Option<Self> {
81 let str = str.trim();
82
83 let (is_negative, str) = if str.starts_with('-') {
85 (true, &str[1..])
86 } else {
87 (false, str)
88 };
89
90 let mut micros: i64;
91
92 if str.ends_with("us") {
94 let value = str.trim_end_matches("us").trim().parse::<f64>().ok()?;
95 micros = value as i64;
96 }
97 else if str.ends_with("ms") {
99 let value = str.trim_end_matches("ms").trim().parse::<f64>().ok()?;
100 micros = (value * MILLIS_PER_SEC) as i64;
101 }
102 else if str.contains(':') {
104 let mut seconds = 0.0;
105 let mut smh = str.split(':').rev();
106 if let Some(sec) = smh.next() {
107 seconds += sec.parse::<f64>().ok()?;
108 }
109
110 if let Some(min) = smh.next() {
111 seconds += min.parse::<f64>().ok()? * 60.0;
112 }
113
114 if let Some(hrs) = smh.next() {
115 seconds += hrs.parse::<f64>().ok()? * 60.0 * 60.0;
116 }
117 micros = (seconds * MICROS_PER_SEC) as i64;
118 }
119 else {
121 let seconds = str.parse::<f64>().ok()?;
122 micros = (seconds * MICROS_PER_SEC) as i64;
123 }
124
125 if is_negative {
126 micros = -micros;
127 }
128
129 Some(Self::from_micros(micros))
130 }
131
132 #[must_use]
133 #[inline]
134 pub fn as_duration(&self) -> Duration {
135 Duration::from_micros(self.0.unsigned_abs())
136 }
137
138 #[must_use]
139 #[inline]
140 pub fn from_duration(duration: Duration) -> Self {
141 Self::from_micros(duration.as_micros() as i64)
142 }
143
144 #[must_use]
146 #[inline]
147 pub fn to_alt_string(&self) -> String {
148 format!("{:#}", self)
149 }
150}
151
152impl fmt::Display for FfmpegTimeDuration {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 if !f.alternate() {
155 let seconds = self.as_seconds();
156 let is_negative = seconds < 0.0;
157 let abs_seconds = seconds.abs();
158
159 let hours = (abs_seconds / 3600.0).floor() as i64;
160 let minutes = ((abs_seconds / 60.0) % 60.0).floor() as i64;
161 let secs = abs_seconds % 60.0;
162
163 if is_negative {
164 write!(f, "-{:02}:{:02}:{:06.3}", hours, minutes, secs)
165 } else {
166 write!(f, "{:02}:{:02}:{:06.3}", hours, minutes, secs)
167 }
168 } else {
169 write!(f, "{}us", self.as_micros())
170 }
171 }
172}
173
174#[derive(Debug, Clone)]
175pub struct ParseFfmpegTimeStrError;
176
177impl fmt::Display for ParseFfmpegTimeStrError {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 write!(f, "Failed to parse FFmpeg time string")
180 }
181}
182
183impl std::error::Error for ParseFfmpegTimeStrError {}
184
185impl TryFrom<String> for FfmpegTimeDuration {
186 type Error = ParseFfmpegTimeStrError;
187
188 fn try_from(value: String) -> Result<Self, Self::Error> {
189 FfmpegTimeDuration::from_str(&value).ok_or(ParseFfmpegTimeStrError)
190 }
191}
192
193impl TryFrom<&str> for FfmpegTimeDuration {
194 type Error = ParseFfmpegTimeStrError;
195
196 fn try_from(value: &str) -> Result<Self, Self::Error> {
197 FfmpegTimeDuration::from_str(value).ok_or(ParseFfmpegTimeStrError)
198 }
199}
200
201macro_rules! impl_from_to_numeric {
202 ($($t:ty),*) => {
203 $(
204 impl From<$t> for FfmpegTimeDuration {
205 fn from(value: $t) -> Self {
206 Self::from_seconds(value as f64)
207 }
208 }
209
210 impl From<FfmpegTimeDuration> for $t {
211 fn from(value: FfmpegTimeDuration) -> Self {
212 value.as_seconds() as $t
213 }
214 }
215
216 impl Add<$t> for FfmpegTimeDuration {
217 type Output = Self;
218
219 fn add(self, rhs: $t) -> Self::Output {
220 FfmpegTimeDuration::from_micros(self.as_micros() + (rhs * MICROS_PER_SEC as $t) as i64)
221 }
222 }
223 )*
224 };
225}
226
227impl_from_to_numeric!(f64, f32, i64, i32, i16, i8, u64, u32, u16, u8, isize, usize);
228
229impl Add for FfmpegTimeDuration {
230 type Output = Self;
231
232 fn add(self, rhs: Self) -> Self::Output {
233 FfmpegTimeDuration::from_micros(self.as_micros() + rhs.as_micros())
234 }
235}
236
237impl Sub for FfmpegTimeDuration {
238 type Output = Self;
239
240 fn sub(self, rhs: Self) -> Self::Output {
241 FfmpegTimeDuration::from_micros(self.as_micros() - rhs.as_micros())
242 }
243}
244
245impl From<FfmpegTimeDuration> for String {
246 fn from(value: FfmpegTimeDuration) -> Self {
247 value.to_string()
248 }
249}
250
251impl From<FfmpegTimeDuration> for Cow<'static, str> {
252 fn from(value: FfmpegTimeDuration) -> Self {
253 Cow::Owned(value.to_string())
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn test_parse_string() {
263 assert_eq!(
264 FfmpegTimeDuration::from_str("00:00:00.00"),
265 Some(FfmpegTimeDuration::from_seconds(0.0))
266 );
267 assert_eq!(
268 FfmpegTimeDuration::from_str("5"),
269 Some(FfmpegTimeDuration::from_seconds(5.0))
270 );
271 assert_eq!(
272 FfmpegTimeDuration::from_str("2.5"),
273 Some(FfmpegTimeDuration::from_seconds(2.5))
274 );
275 assert_eq!(
276 FfmpegTimeDuration::from_str("0.123"),
277 Some(FfmpegTimeDuration::from_seconds(0.123))
278 );
279 assert_eq!(
280 FfmpegTimeDuration::from_str("1:00.0"),
281 Some(FfmpegTimeDuration::from_seconds(60.0))
282 );
283 assert_eq!(
284 FfmpegTimeDuration::from_str("1:01.0"),
285 Some(FfmpegTimeDuration::from_seconds(61.0))
286 );
287 assert_eq!(
288 FfmpegTimeDuration::from_str("1:01:01.123"),
289 Some(FfmpegTimeDuration::from_seconds(3661.123))
290 );
291 assert_eq!(FfmpegTimeDuration::from_str("N/A"), None);
292 }
293
294 #[test]
295 fn test_parse_negative_value() {
296 assert_eq!(
297 FfmpegTimeDuration::from_str("-00:00:01.00"),
298 Some(FfmpegTimeDuration::from_seconds(-1.0))
299 );
300 assert_eq!(
301 FfmpegTimeDuration::from_str("-00:01.00"),
302 Some(FfmpegTimeDuration::from_seconds(-1.0))
303 );
304 assert_eq!(
305 FfmpegTimeDuration::from_str("-01.00"),
306 Some(FfmpegTimeDuration::from_seconds(-1.0))
307 );
308 assert_eq!(
309 FfmpegTimeDuration::from_str("-01"),
310 Some(FfmpegTimeDuration::from_seconds(-1.0))
311 );
312 assert_eq!(
313 FfmpegTimeDuration::from_str("-1"),
314 Some(FfmpegTimeDuration::from_seconds(-1.0))
315 );
316 assert_eq!(
317 FfmpegTimeDuration::from_str("-1000ms"),
318 Some(FfmpegTimeDuration::from_seconds(-1.0))
319 );
320 }
321
322 #[test]
323 fn test_parse_string_with_suffix() {
324 assert_eq!(
325 FfmpegTimeDuration::from_str("400ms"),
326 Some(FfmpegTimeDuration::from_seconds(0.4))
327 );
328 assert_eq!(
329 FfmpegTimeDuration::from_str("3000us"),
330 Some(FfmpegTimeDuration::from_seconds(0.003))
331 );
332 }
333
334 #[test]
335 fn test_format() {
336 assert_eq!(
337 format!("{}", FfmpegTimeDuration::from_seconds(0.0)),
338 "00:00:00.000"
339 );
340 assert_eq!(
341 format!("{}", FfmpegTimeDuration::from_seconds(-1.0)),
342 "-00:00:01.000"
343 );
344 assert_eq!(
345 format!("{}", FfmpegTimeDuration::from_seconds(3661.123)),
346 "01:01:01.123"
347 );
348 assert_eq!(
349 format!("{}", FfmpegTimeDuration::from_seconds(0.547)),
350 "00:00:00.547"
351 );
352 }
353
354 #[test]
355 fn test_alternative_format() {
356 assert_eq!(
357 format!("{:#}", FfmpegTimeDuration::from_seconds(0.0)),
358 "0us"
359 );
360 assert_eq!(
361 format!("{:#}", FfmpegTimeDuration::from_seconds(-0.000001)),
362 "-1us"
363 );
364 assert_eq!(
365 format!("{:#}", FfmpegTimeDuration::from_seconds(3661.123)),
366 "3661123000us"
367 );
368 assert_eq!(
369 format!("{:#}", FfmpegTimeDuration::from_seconds(0.547)),
370 "547000us"
371 );
372 }
373}