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