Skip to main content

oximedia_timecode/
compare.rs

1// Copyright 2025 OxiMedia Contributors
2// Licensed under the Apache License, Version 2.0
3
4//! Timecode comparison helpers.
5//!
6//! The [`Timecode`] struct already implements `PartialOrd` and `Ord` based on
7//! total frame count (see `lib.rs`).  This module adds named helper methods
8//! that read more naturally in production code and provides distance and
9//! clamping utilities.
10
11use crate::Timecode;
12
13/// Extension trait adding named comparison helpers to `Timecode`.
14///
15/// The trait is sealed; it is only implemented for `Timecode`.
16pub trait TimecodeCompare: private::Sealed {
17    /// Return `true` if this timecode comes before `other` in time.
18    fn is_earlier_than(&self, other: &Self) -> bool;
19
20    /// Return `true` if this timecode comes after `other` in time.
21    fn is_later_than(&self, other: &Self) -> bool;
22
23    /// Return `true` if this timecode is identical to `other` (same frame count).
24    fn is_same_frame_as(&self, other: &Self) -> bool;
25
26    /// Return the absolute distance between two timecodes in frames.
27    fn frames_distance(&self, other: &Self) -> u64;
28
29    /// Clamp `self` to the range `[lo, hi]` (inclusive, by frame count).
30    ///
31    /// If `lo > hi` the result is `lo`.
32    fn clamp_to_range<'a>(&'a self, lo: &'a Self, hi: &'a Self) -> &'a Self;
33}
34
35mod private {
36    pub trait Sealed {}
37    impl Sealed for super::Timecode {}
38}
39
40impl TimecodeCompare for Timecode {
41    /// Return `true` if `self` is strictly earlier than `other`.
42    ///
43    /// # Example
44    ///
45    /// ```rust,ignore
46    /// use oximedia_timecode::{Timecode, FrameRate};
47    /// use oximedia_timecode::compare::TimecodeCompare;
48    ///
49    /// let tc1 = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).unwrap();
50    /// let tc2 = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).unwrap();
51    /// assert!(tc1.is_earlier_than(&tc2));
52    /// ```
53    fn is_earlier_than(&self, other: &Timecode) -> bool {
54        self.to_frames() < other.to_frames()
55    }
56
57    /// Return `true` if `self` is strictly later than `other`.
58    fn is_later_than(&self, other: &Timecode) -> bool {
59        self.to_frames() > other.to_frames()
60    }
61
62    /// Return `true` if `self` and `other` refer to the same frame index.
63    fn is_same_frame_as(&self, other: &Timecode) -> bool {
64        self.to_frames() == other.to_frames()
65    }
66
67    /// Absolute difference in frames between `self` and `other`.
68    fn frames_distance(&self, other: &Timecode) -> u64 {
69        self.to_frames().abs_diff(other.to_frames())
70    }
71
72    /// Clamp `self` into the inclusive range `[lo, hi]`.
73    fn clamp_to_range<'a>(&'a self, lo: &'a Timecode, hi: &'a Timecode) -> &'a Timecode {
74        if self.is_earlier_than(lo) {
75            lo
76        } else if self.is_later_than(hi) {
77            hi
78        } else {
79            self
80        }
81    }
82}
83
84// Standalone comparison helpers (for use without the trait).
85
86/// Return `true` if `a` comes before `b`.
87pub fn is_earlier_than(a: &Timecode, b: &Timecode) -> bool {
88    a.to_frames() < b.to_frames()
89}
90
91/// Return `true` if `a` comes after `b`.
92pub fn is_later_than(a: &Timecode, b: &Timecode) -> bool {
93    a.to_frames() > b.to_frames()
94}
95
96/// Return `true` if `a` and `b` represent the same frame.
97pub fn is_same_frame(a: &Timecode, b: &Timecode) -> bool {
98    a.to_frames() == b.to_frames()
99}
100
101/// Return the frame with the earlier position.
102pub fn earlier<'a>(a: &'a Timecode, b: &'a Timecode) -> &'a Timecode {
103    if a.to_frames() <= b.to_frames() {
104        a
105    } else {
106        b
107    }
108}
109
110/// Return the frame with the later position.
111pub fn later<'a>(a: &'a Timecode, b: &'a Timecode) -> &'a Timecode {
112    if a.to_frames() >= b.to_frames() {
113        a
114    } else {
115        b
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::FrameRate;
123
124    fn tc(h: u8, m: u8, s: u8, f: u8) -> Timecode {
125        Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid tc")
126    }
127
128    #[test]
129    fn is_earlier_than_true() {
130        let a = tc(0, 0, 0, 0);
131        let b = tc(0, 0, 1, 0);
132        assert!(a.is_earlier_than(&b));
133    }
134
135    #[test]
136    fn is_earlier_than_false_when_equal() {
137        let a = tc(0, 0, 1, 0);
138        assert!(!a.is_earlier_than(&a));
139    }
140
141    #[test]
142    fn is_later_than_true() {
143        let a = tc(0, 0, 1, 0);
144        let b = tc(0, 0, 0, 0);
145        assert!(a.is_later_than(&b));
146    }
147
148    #[test]
149    fn is_same_frame_as() {
150        let a = tc(1, 2, 3, 4);
151        let b = tc(1, 2, 3, 4);
152        assert!(a.is_same_frame_as(&b));
153    }
154
155    #[test]
156    fn frames_distance() {
157        let a = tc(0, 0, 0, 0);
158        let b = tc(0, 0, 1, 0); // 25 frames at 25fps
159        assert_eq!(a.frames_distance(&b), 25);
160    }
161
162    #[test]
163    fn clamp_to_range_below() {
164        let lo = tc(0, 0, 1, 0);
165        let hi = tc(0, 0, 5, 0);
166        let x = tc(0, 0, 0, 0); // below lo
167        let result = x.clamp_to_range(&lo, &hi);
168        assert!(result.is_same_frame_as(&lo));
169    }
170
171    #[test]
172    fn clamp_to_range_above() {
173        let lo = tc(0, 0, 1, 0);
174        let hi = tc(0, 0, 5, 0);
175        let x = tc(0, 0, 9, 0); // above hi
176        let result = x.clamp_to_range(&lo, &hi);
177        assert!(result.is_same_frame_as(&hi));
178    }
179
180    #[test]
181    fn clamp_to_range_within() {
182        let lo = tc(0, 0, 1, 0);
183        let hi = tc(0, 0, 5, 0);
184        let x = tc(0, 0, 3, 0);
185        let result = x.clamp_to_range(&lo, &hi);
186        assert!(result.is_same_frame_as(&x));
187    }
188
189    #[test]
190    fn standalone_is_earlier_than() {
191        assert!(is_earlier_than(&tc(0, 0, 0, 0), &tc(0, 0, 0, 1)));
192        assert!(!is_earlier_than(&tc(0, 0, 0, 1), &tc(0, 0, 0, 0)));
193    }
194
195    #[test]
196    fn standalone_earlier_returns_min() {
197        let a = tc(0, 0, 2, 0);
198        let b = tc(0, 0, 1, 0);
199        assert!(earlier(&a, &b).is_same_frame_as(&b));
200    }
201
202    #[test]
203    fn partial_ord_uses_frame_count() {
204        let a = tc(0, 0, 0, 0);
205        let b = tc(0, 0, 0, 1);
206        assert!(a < b);
207        assert!(b > a);
208    }
209}