Skip to main content

oximedia_timecode/
tc_range.rs

1#![allow(dead_code)]
2//! Timecode range operations for defining and manipulating spans of timecode.
3//!
4//! Provides `TcRange` for representing a contiguous span of timecode values,
5//! with support for iteration, containment checks, overlap detection,
6//! splitting, and merging.
7
8use crate::{FrameRateInfo, Timecode, TimecodeError};
9
10/// A contiguous range of timecodes defined by an inclusive start and exclusive end.
11///
12/// The range is measured in absolute frame counts for a given frame rate.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct TcRange {
15    /// Start timecode (inclusive)
16    start_frames: u64,
17    /// End timecode (exclusive)
18    end_frames: u64,
19    /// Frames per second (rounded)
20    fps: u8,
21    /// Whether drop-frame accounting is active
22    drop_frame: bool,
23}
24
25/// Result of splitting a range at a given timecode.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct SplitResult {
28    /// The portion before the split point
29    pub before: Option<TcRange>,
30    /// The portion from the split point onward
31    pub after: Option<TcRange>,
32}
33
34/// Describes the overlap relationship between two ranges.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum OverlapKind {
37    /// No overlap at all
38    None,
39    /// The ranges are exactly equal
40    Equal,
41    /// The first range fully contains the second
42    Contains,
43    /// The first range is fully contained by the second
44    ContainedBy,
45    /// Partial overlap (neither contains the other)
46    Partial,
47}
48
49impl TcRange {
50    /// Creates a new timecode range from two timecodes.
51    ///
52    /// # Errors
53    ///
54    /// Returns `TimecodeError::InvalidConfiguration` if start >= end or frame rate info differs.
55    pub fn new(start: &Timecode, end: &Timecode) -> Result<Self, TimecodeError> {
56        if start.frame_rate != end.frame_rate {
57            return Err(TimecodeError::InvalidConfiguration);
58        }
59        let s = start.to_frames();
60        let e = end.to_frames();
61        if s >= e {
62            return Err(TimecodeError::InvalidConfiguration);
63        }
64        Ok(Self {
65            start_frames: s,
66            end_frames: e,
67            fps: start.frame_rate.fps,
68            drop_frame: start.frame_rate.drop_frame,
69        })
70    }
71
72    /// Creates a range from raw frame numbers.
73    ///
74    /// # Errors
75    ///
76    /// Returns `TimecodeError::InvalidConfiguration` if start >= end.
77    pub fn from_frames(
78        start: u64,
79        end: u64,
80        fps: u8,
81        drop_frame: bool,
82    ) -> Result<Self, TimecodeError> {
83        if start >= end {
84            return Err(TimecodeError::InvalidConfiguration);
85        }
86        Ok(Self {
87            start_frames: start,
88            end_frames: end,
89            fps,
90            drop_frame,
91        })
92    }
93
94    /// Returns the start frame number (inclusive).
95    pub fn start_frames(&self) -> u64 {
96        self.start_frames
97    }
98
99    /// Returns the end frame number (exclusive).
100    pub fn end_frames(&self) -> u64 {
101        self.end_frames
102    }
103
104    /// Returns the duration in frames.
105    pub fn duration_frames(&self) -> u64 {
106        self.end_frames - self.start_frames
107    }
108
109    /// Returns the duration in seconds.
110    #[allow(clippy::cast_precision_loss)]
111    pub fn duration_seconds(&self) -> f64 {
112        self.duration_frames() as f64 / self.fps as f64
113    }
114
115    /// Checks whether the given frame number falls within this range.
116    pub fn contains_frame(&self, frame: u64) -> bool {
117        frame >= self.start_frames && frame < self.end_frames
118    }
119
120    /// Checks whether the given timecode falls within this range.
121    pub fn contains_timecode(&self, tc: &Timecode) -> bool {
122        self.contains_frame(tc.to_frames())
123    }
124
125    /// Returns the `FrameRateInfo` associated with this range.
126    pub fn frame_rate_info(&self) -> FrameRateInfo {
127        FrameRateInfo {
128            fps: self.fps,
129            drop_frame: self.drop_frame,
130        }
131    }
132
133    /// Checks whether two ranges overlap.
134    pub fn overlaps(&self, other: &Self) -> bool {
135        self.start_frames < other.end_frames && other.start_frames < self.end_frames
136    }
137
138    /// Classifies the overlap between two ranges.
139    pub fn overlap_kind(&self, other: &Self) -> OverlapKind {
140        if self == other {
141            return OverlapKind::Equal;
142        }
143        if !self.overlaps(other) {
144            return OverlapKind::None;
145        }
146        if self.start_frames <= other.start_frames && self.end_frames >= other.end_frames {
147            return OverlapKind::Contains;
148        }
149        if other.start_frames <= self.start_frames && other.end_frames >= self.end_frames {
150            return OverlapKind::ContainedBy;
151        }
152        OverlapKind::Partial
153    }
154
155    /// Returns the intersection of two ranges, if any.
156    pub fn intersect(&self, other: &Self) -> Option<Self> {
157        if !self.overlaps(other) {
158            return None;
159        }
160        let s = self.start_frames.max(other.start_frames);
161        let e = self.end_frames.min(other.end_frames);
162        Some(Self {
163            start_frames: s,
164            end_frames: e,
165            fps: self.fps,
166            drop_frame: self.drop_frame,
167        })
168    }
169
170    /// Merges two ranges into one if they overlap or are adjacent.
171    pub fn union(&self, other: &Self) -> Option<Self> {
172        if self.end_frames < other.start_frames || other.end_frames < self.start_frames {
173            return None;
174        }
175        let s = self.start_frames.min(other.start_frames);
176        let e = self.end_frames.max(other.end_frames);
177        Some(Self {
178            start_frames: s,
179            end_frames: e,
180            fps: self.fps,
181            drop_frame: self.drop_frame,
182        })
183    }
184
185    /// Splits the range at the given frame number.
186    pub fn split_at_frame(&self, frame: u64) -> SplitResult {
187        if frame <= self.start_frames {
188            SplitResult {
189                before: None,
190                after: Some(self.clone()),
191            }
192        } else if frame >= self.end_frames {
193            SplitResult {
194                before: Some(self.clone()),
195                after: None,
196            }
197        } else {
198            SplitResult {
199                before: Some(Self {
200                    start_frames: self.start_frames,
201                    end_frames: frame,
202                    fps: self.fps,
203                    drop_frame: self.drop_frame,
204                }),
205                after: Some(Self {
206                    start_frames: frame,
207                    end_frames: self.end_frames,
208                    fps: self.fps,
209                    drop_frame: self.drop_frame,
210                }),
211            }
212        }
213    }
214
215    /// Offsets the range by a signed number of frames.
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if the result would be negative.
220    pub fn offset(&self, delta: i64) -> Result<Self, TimecodeError> {
221        let s = if delta >= 0 {
222            self.start_frames + delta as u64
223        } else {
224            let abs = (-delta) as u64;
225            if abs > self.start_frames {
226                return Err(TimecodeError::InvalidFrames);
227            }
228            self.start_frames - abs
229        };
230        let e = if delta >= 0 {
231            self.end_frames + delta as u64
232        } else {
233            let abs = (-delta) as u64;
234            if abs > self.end_frames {
235                return Err(TimecodeError::InvalidFrames);
236            }
237            self.end_frames - abs
238        };
239        Ok(Self {
240            start_frames: s,
241            end_frames: e,
242            fps: self.fps,
243            drop_frame: self.drop_frame,
244        })
245    }
246
247    /// Returns a list of frame numbers in this range.
248    pub fn frame_iter(&self) -> impl Iterator<Item = u64> {
249        self.start_frames..self.end_frames
250    }
251
252    /// Extends the range by the given number of frames on each side.
253    pub fn extend(&self, head_frames: u64, tail_frames: u64) -> Self {
254        let s = self.start_frames.saturating_sub(head_frames);
255        let e = self.end_frames.saturating_add(tail_frames);
256        Self {
257            start_frames: s,
258            end_frames: e,
259            fps: self.fps,
260            drop_frame: self.drop_frame,
261        }
262    }
263
264    /// Trims the range by the given number of frames on each side.
265    ///
266    /// Returns `None` if the range would become empty.
267    pub fn trim(&self, head_frames: u64, tail_frames: u64) -> Option<Self> {
268        let s = self.start_frames.saturating_add(head_frames);
269        let e = self.end_frames.saturating_sub(tail_frames);
270        if s >= e {
271            return None;
272        }
273        Some(Self {
274            start_frames: s,
275            end_frames: e,
276            fps: self.fps,
277            drop_frame: self.drop_frame,
278        })
279    }
280}
281
282/// Merges a list of potentially overlapping ranges into a sorted, non-overlapping set.
283pub fn merge_ranges(mut ranges: Vec<TcRange>) -> Vec<TcRange> {
284    if ranges.is_empty() {
285        return vec![];
286    }
287    ranges.sort_by_key(|r| r.start_frames);
288    let mut merged: Vec<TcRange> = vec![ranges[0].clone()];
289    for r in &ranges[1..] {
290        let last = merged.last_mut().unwrap();
291        if let Some(u) = last.union(r) {
292            *last = u;
293        } else {
294            merged.push(r.clone());
295        }
296    }
297    merged
298}
299
300/// Computes the total number of frames covered by a set of ranges (after merging).
301pub fn total_coverage(ranges: Vec<TcRange>) -> u64 {
302    merge_ranges(ranges)
303        .iter()
304        .map(|r| r.duration_frames())
305        .sum()
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    fn make_range(start: u64, end: u64) -> TcRange {
313        TcRange::from_frames(start, end, 25, false).unwrap()
314    }
315
316    #[test]
317    fn test_create_range() {
318        let r = make_range(0, 100);
319        assert_eq!(r.start_frames(), 0);
320        assert_eq!(r.end_frames(), 100);
321    }
322
323    #[test]
324    fn test_duration_frames() {
325        let r = make_range(10, 110);
326        assert_eq!(r.duration_frames(), 100);
327    }
328
329    #[test]
330    fn test_duration_seconds() {
331        let r = make_range(0, 25);
332        let d = r.duration_seconds();
333        assert!((d - 1.0).abs() < 0.001);
334    }
335
336    #[test]
337    fn test_contains_frame() {
338        let r = make_range(10, 20);
339        assert!(r.contains_frame(10));
340        assert!(r.contains_frame(19));
341        assert!(!r.contains_frame(20));
342        assert!(!r.contains_frame(9));
343    }
344
345    #[test]
346    fn test_overlaps() {
347        let a = make_range(0, 50);
348        let b = make_range(25, 75);
349        let c = make_range(50, 100);
350        assert!(a.overlaps(&b));
351        assert!(!a.overlaps(&c));
352    }
353
354    #[test]
355    fn test_overlap_kind_equal() {
356        let a = make_range(0, 100);
357        let b = make_range(0, 100);
358        assert_eq!(a.overlap_kind(&b), OverlapKind::Equal);
359    }
360
361    #[test]
362    fn test_overlap_kind_contains() {
363        let a = make_range(0, 100);
364        let b = make_range(10, 50);
365        assert_eq!(a.overlap_kind(&b), OverlapKind::Contains);
366    }
367
368    #[test]
369    fn test_overlap_kind_partial() {
370        let a = make_range(0, 50);
371        let b = make_range(25, 75);
372        assert_eq!(a.overlap_kind(&b), OverlapKind::Partial);
373    }
374
375    #[test]
376    fn test_intersect() {
377        let a = make_range(0, 50);
378        let b = make_range(25, 75);
379        let inter = a.intersect(&b).unwrap();
380        assert_eq!(inter.start_frames(), 25);
381        assert_eq!(inter.end_frames(), 50);
382    }
383
384    #[test]
385    fn test_intersect_none() {
386        let a = make_range(0, 10);
387        let b = make_range(20, 30);
388        assert!(a.intersect(&b).is_none());
389    }
390
391    #[test]
392    fn test_union() {
393        let a = make_range(0, 50);
394        let b = make_range(50, 100);
395        let u = a.union(&b).unwrap();
396        assert_eq!(u.start_frames(), 0);
397        assert_eq!(u.end_frames(), 100);
398    }
399
400    #[test]
401    fn test_split_at_frame() {
402        let r = make_range(0, 100);
403        let split = r.split_at_frame(50);
404        let before = split.before.unwrap();
405        let after = split.after.unwrap();
406        assert_eq!(before.duration_frames(), 50);
407        assert_eq!(after.duration_frames(), 50);
408    }
409
410    #[test]
411    fn test_offset_positive() {
412        let r = make_range(10, 20);
413        let shifted = r.offset(5).unwrap();
414        assert_eq!(shifted.start_frames(), 15);
415        assert_eq!(shifted.end_frames(), 25);
416    }
417
418    #[test]
419    fn test_offset_negative() {
420        let r = make_range(10, 20);
421        let shifted = r.offset(-5).unwrap();
422        assert_eq!(shifted.start_frames(), 5);
423        assert_eq!(shifted.end_frames(), 15);
424    }
425
426    #[test]
427    fn test_extend_and_trim() {
428        let r = make_range(50, 100);
429        let ext = r.extend(10, 10);
430        assert_eq!(ext.start_frames(), 40);
431        assert_eq!(ext.end_frames(), 110);
432        let trimmed = ext.trim(10, 10).unwrap();
433        assert_eq!(trimmed.start_frames(), 50);
434        assert_eq!(trimmed.end_frames(), 100);
435    }
436
437    #[test]
438    fn test_merge_ranges() {
439        let ranges = vec![make_range(0, 30), make_range(20, 50), make_range(60, 80)];
440        let merged = merge_ranges(ranges);
441        assert_eq!(merged.len(), 2);
442        assert_eq!(merged[0].start_frames(), 0);
443        assert_eq!(merged[0].end_frames(), 50);
444        assert_eq!(merged[1].start_frames(), 60);
445    }
446
447    #[test]
448    fn test_total_coverage() {
449        let ranges = vec![make_range(0, 30), make_range(20, 50), make_range(60, 80)];
450        assert_eq!(total_coverage(ranges), 70); // 50 + 20
451    }
452
453    #[test]
454    fn test_invalid_range() {
455        assert!(TcRange::from_frames(100, 50, 25, false).is_err());
456    }
457
458    #[test]
459    fn test_frame_iter_count() {
460        let r = make_range(0, 10);
461        assert_eq!(r.frame_iter().count(), 10);
462    }
463}