Skip to main content

oximedia_timecode/
tc_compare.rs

1#![allow(dead_code)]
2//! Timecode comparison and distance utilities.
3//!
4//! Provides utilities for comparing timecodes, computing distances,
5//! sorting, and checking various temporal relationships between timecodes.
6
7use crate::{FrameRate, Timecode, TimecodeError};
8use std::cmp::Ordering;
9
10/// The result of comparing two timecodes.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TcRelation {
13    /// The first timecode is earlier.
14    Before,
15    /// The timecodes are identical.
16    Equal,
17    /// The first timecode is later.
18    After,
19}
20
21/// Compares two timecodes by total frame count.
22pub fn compare(a: &Timecode, b: &Timecode) -> TcRelation {
23    let fa = a.to_frames();
24    let fb = b.to_frames();
25    match fa.cmp(&fb) {
26        Ordering::Less => TcRelation::Before,
27        Ordering::Equal => TcRelation::Equal,
28        Ordering::Greater => TcRelation::After,
29    }
30}
31
32/// Returns the absolute distance in frames between two timecodes.
33pub fn distance_frames(a: &Timecode, b: &Timecode) -> u64 {
34    let fa = a.to_frames();
35    let fb = b.to_frames();
36    fa.abs_diff(fb)
37}
38
39/// Returns the distance between two timecodes in seconds (approximate for non-integer rates).
40#[allow(clippy::cast_precision_loss)]
41pub fn distance_seconds(a: &Timecode, b: &Timecode, frame_rate: FrameRate) -> f64 {
42    let d = distance_frames(a, b);
43    d as f64 / frame_rate.as_float()
44}
45
46/// Checks whether timecode `tc` falls within the range [start, end] (inclusive).
47pub fn is_within_range(tc: &Timecode, start: &Timecode, end: &Timecode) -> bool {
48    let f = tc.to_frames();
49    let fs = start.to_frames();
50    let fe = end.to_frames();
51    f >= fs && f <= fe
52}
53
54/// Returns the midpoint timecode between two timecodes.
55pub fn midpoint(
56    a: &Timecode,
57    b: &Timecode,
58    frame_rate: FrameRate,
59) -> Result<Timecode, TimecodeError> {
60    let fa = a.to_frames();
61    let fb = b.to_frames();
62    let mid = (fa + fb) / 2;
63    Timecode::from_frames(mid, frame_rate)
64}
65
66/// A timecode span defined by an in-point and an out-point.
67#[derive(Debug, Clone, Copy, PartialEq)]
68pub struct TcSpan {
69    /// The in-point timecode.
70    pub tc_in: Timecode,
71    /// The out-point timecode.
72    pub tc_out: Timecode,
73}
74
75impl TcSpan {
76    /// Creates a new span. `tc_in` must not be after `tc_out`.
77    pub fn new(tc_in: Timecode, tc_out: Timecode) -> Result<Self, TimecodeError> {
78        if tc_in.to_frames() > tc_out.to_frames() {
79            return Err(TimecodeError::InvalidConfiguration);
80        }
81        Ok(Self { tc_in, tc_out })
82    }
83
84    /// Returns the duration of the span in frames.
85    pub fn duration_frames(&self) -> u64 {
86        self.tc_out.to_frames() - self.tc_in.to_frames()
87    }
88
89    /// Returns the duration in seconds.
90    #[allow(clippy::cast_precision_loss)]
91    pub fn duration_seconds(&self, frame_rate: FrameRate) -> f64 {
92        self.duration_frames() as f64 / frame_rate.as_float()
93    }
94
95    /// Checks whether a timecode falls within this span (inclusive).
96    pub fn contains(&self, tc: &Timecode) -> bool {
97        is_within_range(tc, &self.tc_in, &self.tc_out)
98    }
99
100    /// Checks whether this span overlaps with another.
101    pub fn overlaps(&self, other: &TcSpan) -> bool {
102        let a_start = self.tc_in.to_frames();
103        let a_end = self.tc_out.to_frames();
104        let b_start = other.tc_in.to_frames();
105        let b_end = other.tc_out.to_frames();
106        a_start <= b_end && b_start <= a_end
107    }
108
109    /// Returns the intersection of two spans, or `None` if they don't overlap.
110    pub fn intersection(
111        &self,
112        other: &TcSpan,
113        frame_rate: FrameRate,
114    ) -> Option<Result<TcSpan, TimecodeError>> {
115        if !self.overlaps(other) {
116            return None;
117        }
118        let start = self.tc_in.to_frames().max(other.tc_in.to_frames());
119        let end = self.tc_out.to_frames().min(other.tc_out.to_frames());
120        let tc_in = match Timecode::from_frames(start, frame_rate) {
121            Ok(tc) => tc,
122            Err(e) => return Some(Err(e)),
123        };
124        let tc_out = match Timecode::from_frames(end, frame_rate) {
125            Ok(tc) => tc,
126            Err(e) => return Some(Err(e)),
127        };
128        Some(TcSpan::new(tc_in, tc_out))
129    }
130}
131
132/// Sorts a slice of timecodes by frame count (ascending).
133pub fn sort_timecodes(tcs: &mut [Timecode]) {
134    tcs.sort_by_key(Timecode::to_frames);
135}
136
137/// Returns the earliest timecode from a non-empty slice.
138pub fn earliest(tcs: &[Timecode]) -> Option<&Timecode> {
139    tcs.iter().min_by_key(|tc| tc.to_frames())
140}
141
142/// Returns the latest timecode from a non-empty slice.
143pub fn latest(tcs: &[Timecode]) -> Option<&Timecode> {
144    tcs.iter().max_by_key(|tc| tc.to_frames())
145}
146
147/// Checks if a sequence of timecodes is strictly ascending (no duplicates).
148pub fn is_ascending(tcs: &[Timecode]) -> bool {
149    tcs.windows(2).all(|w| w[0].to_frames() < w[1].to_frames())
150}
151
152/// Checks if a sequence of timecodes is contiguous (each consecutive pair differs by exactly 1 frame).
153pub fn is_contiguous(tcs: &[Timecode]) -> bool {
154    tcs.windows(2)
155        .all(|w| w[1].to_frames() == w[0].to_frames() + 1)
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    fn tc(h: u8, m: u8, s: u8, f: u8) -> Timecode {
163        Timecode::new(h, m, s, f, FrameRate::Fps25).unwrap()
164    }
165
166    #[test]
167    fn test_compare_before() {
168        assert_eq!(
169            compare(&tc(0, 0, 0, 0), &tc(0, 0, 0, 1)),
170            TcRelation::Before
171        );
172    }
173
174    #[test]
175    fn test_compare_equal() {
176        assert_eq!(compare(&tc(1, 2, 3, 4), &tc(1, 2, 3, 4)), TcRelation::Equal);
177    }
178
179    #[test]
180    fn test_compare_after() {
181        assert_eq!(
182            compare(&tc(0, 0, 1, 0), &tc(0, 0, 0, 24)),
183            TcRelation::After
184        );
185    }
186
187    #[test]
188    fn test_distance_frames_same() {
189        assert_eq!(distance_frames(&tc(0, 0, 0, 0), &tc(0, 0, 0, 0)), 0);
190    }
191
192    #[test]
193    fn test_distance_frames_one_second() {
194        assert_eq!(distance_frames(&tc(0, 0, 0, 0), &tc(0, 0, 1, 0)), 25);
195    }
196
197    #[test]
198    fn test_distance_frames_symmetric() {
199        let a = tc(0, 0, 0, 10);
200        let b = tc(0, 0, 1, 5);
201        assert_eq!(distance_frames(&a, &b), distance_frames(&b, &a));
202    }
203
204    #[test]
205    fn test_distance_seconds() {
206        let d = distance_seconds(&tc(0, 0, 0, 0), &tc(0, 0, 1, 0), FrameRate::Fps25);
207        assert!((d - 1.0).abs() < 0.01);
208    }
209
210    #[test]
211    fn test_is_within_range() {
212        let start = tc(0, 0, 0, 0);
213        let end = tc(0, 0, 2, 0);
214        assert!(is_within_range(&tc(0, 0, 1, 0), &start, &end));
215        assert!(is_within_range(&start, &start, &end));
216        assert!(is_within_range(&end, &start, &end));
217        assert!(!is_within_range(&tc(0, 0, 3, 0), &start, &end));
218    }
219
220    #[test]
221    fn test_midpoint() {
222        let a = tc(0, 0, 0, 0);
223        let b = tc(0, 0, 2, 0); // 50 frames
224        let mid = midpoint(&a, &b, FrameRate::Fps25).unwrap();
225        assert_eq!(mid.to_frames(), 25); // 1 second
226    }
227
228    #[test]
229    fn test_tc_span_creation() {
230        let span = TcSpan::new(tc(0, 0, 0, 0), tc(0, 0, 1, 0)).unwrap();
231        assert_eq!(span.duration_frames(), 25);
232    }
233
234    #[test]
235    fn test_tc_span_invalid() {
236        let result = TcSpan::new(tc(0, 0, 1, 0), tc(0, 0, 0, 0));
237        assert!(result.is_err());
238    }
239
240    #[test]
241    fn test_tc_span_contains() {
242        let span = TcSpan::new(tc(0, 0, 0, 0), tc(0, 0, 2, 0)).unwrap();
243        assert!(span.contains(&tc(0, 0, 1, 0)));
244        assert!(!span.contains(&tc(0, 0, 3, 0)));
245    }
246
247    #[test]
248    fn test_tc_span_overlaps() {
249        let a = TcSpan::new(tc(0, 0, 0, 0), tc(0, 0, 2, 0)).unwrap();
250        let b = TcSpan::new(tc(0, 0, 1, 0), tc(0, 0, 3, 0)).unwrap();
251        assert!(a.overlaps(&b));
252        let c = TcSpan::new(tc(0, 0, 5, 0), tc(0, 0, 6, 0)).unwrap();
253        assert!(!a.overlaps(&c));
254    }
255
256    #[test]
257    fn test_tc_span_intersection() {
258        let a = TcSpan::new(tc(0, 0, 0, 0), tc(0, 0, 2, 0)).unwrap();
259        let b = TcSpan::new(tc(0, 0, 1, 0), tc(0, 0, 3, 0)).unwrap();
260        let inter = a.intersection(&b, FrameRate::Fps25).unwrap().unwrap();
261        assert_eq!(inter.tc_in.to_frames(), tc(0, 0, 1, 0).to_frames());
262        assert_eq!(inter.tc_out.to_frames(), tc(0, 0, 2, 0).to_frames());
263    }
264
265    #[test]
266    fn test_sort_timecodes() {
267        let mut tcs = vec![tc(0, 0, 2, 0), tc(0, 0, 0, 0), tc(0, 0, 1, 0)];
268        sort_timecodes(&mut tcs);
269        assert_eq!(tcs[0].to_frames(), tc(0, 0, 0, 0).to_frames());
270        assert_eq!(tcs[1].to_frames(), tc(0, 0, 1, 0).to_frames());
271        assert_eq!(tcs[2].to_frames(), tc(0, 0, 2, 0).to_frames());
272    }
273
274    #[test]
275    fn test_earliest_latest() {
276        let tcs = vec![tc(0, 0, 2, 0), tc(0, 0, 0, 0), tc(0, 0, 1, 0)];
277        assert_eq!(
278            earliest(&tcs).unwrap().to_frames(),
279            tc(0, 0, 0, 0).to_frames()
280        );
281        assert_eq!(
282            latest(&tcs).unwrap().to_frames(),
283            tc(0, 0, 2, 0).to_frames()
284        );
285    }
286
287    #[test]
288    fn test_is_ascending() {
289        let asc = vec![tc(0, 0, 0, 0), tc(0, 0, 0, 1), tc(0, 0, 0, 2)];
290        assert!(is_ascending(&asc));
291        let not_asc = vec![tc(0, 0, 0, 1), tc(0, 0, 0, 0)];
292        assert!(!is_ascending(&not_asc));
293    }
294
295    #[test]
296    fn test_is_contiguous() {
297        let contig = vec![tc(0, 0, 0, 0), tc(0, 0, 0, 1), tc(0, 0, 0, 2)];
298        assert!(is_contiguous(&contig));
299        let gap = vec![tc(0, 0, 0, 0), tc(0, 0, 0, 5)];
300        assert!(!is_contiguous(&gap));
301    }
302}