1#![allow(dead_code)]
2use crate::{FrameRate, Timecode, TimecodeError};
8use std::cmp::Ordering;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TcRelation {
13 Before,
15 Equal,
17 After,
19}
20
21pub 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
32pub 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#[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
46pub 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
54pub 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#[derive(Debug, Clone, Copy, PartialEq)]
68pub struct TcSpan {
69 pub tc_in: Timecode,
71 pub tc_out: Timecode,
73}
74
75impl TcSpan {
76 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 pub fn duration_frames(&self) -> u64 {
86 self.tc_out.to_frames() - self.tc_in.to_frames()
87 }
88
89 #[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 pub fn contains(&self, tc: &Timecode) -> bool {
97 is_within_range(tc, &self.tc_in, &self.tc_out)
98 }
99
100 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 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
132pub fn sort_timecodes(tcs: &mut [Timecode]) {
134 tcs.sort_by_key(Timecode::to_frames);
135}
136
137pub fn earliest(tcs: &[Timecode]) -> Option<&Timecode> {
139 tcs.iter().min_by_key(|tc| tc.to_frames())
140}
141
142pub fn latest(tcs: &[Timecode]) -> Option<&Timecode> {
144 tcs.iter().max_by_key(|tc| tc.to_frames())
145}
146
147pub fn is_ascending(tcs: &[Timecode]) -> bool {
149 tcs.windows(2).all(|w| w[0].to_frames() < w[1].to_frames())
150}
151
152pub 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); let mid = midpoint(&a, &b, FrameRate::Fps25).unwrap();
225 assert_eq!(mid.to_frames(), 25); }
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(¬_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}