Skip to main content

oximedia_timecode/
timecode_range.rs

1#![allow(dead_code)]
2//! Timecode range operations for start/end interval management.
3
4/// A range defined by a start frame and an end frame (inclusive).
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct TimecodeRange {
7    /// Start frame (inclusive).
8    pub start: u64,
9    /// End frame (inclusive).
10    pub end: u64,
11}
12
13impl TimecodeRange {
14    /// Create a new [`TimecodeRange`].
15    ///
16    /// Returns `None` if `end < start`.
17    pub fn new(start: u64, end: u64) -> Option<Self> {
18        if end < start {
19            None
20        } else {
21            Some(Self { start, end })
22        }
23    }
24
25    /// Number of frames contained in this range (inclusive on both ends).
26    pub fn duration_frames(&self) -> u64 {
27        self.end - self.start + 1
28    }
29
30    /// Whether `frame` is within `[start, end]`.
31    pub fn contains_frame(&self, frame: u64) -> bool {
32        frame >= self.start && frame <= self.end
33    }
34
35    /// Whether this range overlaps with `other`.
36    pub fn overlaps(&self, other: &TimecodeRange) -> bool {
37        self.start <= other.end && other.start <= self.end
38    }
39
40    /// Split this range at `frame`, returning two sub-ranges.
41    ///
42    /// The split point becomes the last frame of the first range and the
43    /// first frame of the second.  Returns `None` if `frame` is outside
44    /// `[start, end - 1]` (there must be at least one frame on each side).
45    pub fn split_at(&self, frame: u64) -> Option<(TimecodeRange, TimecodeRange)> {
46        if frame < self.start || frame >= self.end {
47            return None;
48        }
49        let left = TimecodeRange {
50            start: self.start,
51            end: frame,
52        };
53        let right = TimecodeRange {
54            start: frame + 1,
55            end: self.end,
56        };
57        Some((left, right))
58    }
59}
60
61/// A collection of [`TimecodeRange`] intervals.
62#[derive(Debug, Clone, Default)]
63pub struct TimecodeRangeList {
64    ranges: Vec<TimecodeRange>,
65}
66
67impl TimecodeRangeList {
68    /// Create an empty list.
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    /// Append a range to the list.
74    pub fn add(&mut self, range: TimecodeRange) {
75        self.ranges.push(range);
76    }
77
78    /// Total frame count across all ranges.
79    pub fn total_frames(&self) -> u64 {
80        self.ranges.iter().map(|r| r.duration_frames()).sum()
81    }
82
83    /// Merge consecutive / overlapping ranges that are adjacent (end + 1 == next.start).
84    pub fn merge_adjacent(&self) -> TimecodeRangeList {
85        let mut sorted = self.ranges.clone();
86        sorted.sort_by_key(|r| r.start);
87
88        let mut merged: Vec<TimecodeRange> = Vec::new();
89        for range in sorted {
90            if let Some(last) = merged.last_mut() {
91                if range.start <= last.end + 1 {
92                    // Extend the last range if necessary.
93                    if range.end > last.end {
94                        last.end = range.end;
95                    }
96                    continue;
97                }
98            }
99            merged.push(range);
100        }
101
102        TimecodeRangeList { ranges: merged }
103    }
104
105    /// Iterate over the stored ranges.
106    pub fn iter(&self) -> std::slice::Iter<'_, TimecodeRange> {
107        self.ranges.iter()
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_new_valid() {
117        let r = TimecodeRange::new(10, 20).expect("valid timecode range");
118        assert_eq!(r.start, 10);
119        assert_eq!(r.end, 20);
120    }
121
122    #[test]
123    fn test_new_invalid_returns_none() {
124        assert!(TimecodeRange::new(20, 10).is_none());
125    }
126
127    #[test]
128    fn test_new_same_start_end() {
129        let r = TimecodeRange::new(5, 5).expect("valid timecode range");
130        assert_eq!(r.duration_frames(), 1);
131    }
132
133    #[test]
134    fn test_duration_frames() {
135        let r = TimecodeRange::new(0, 24).expect("valid timecode range");
136        assert_eq!(r.duration_frames(), 25);
137    }
138
139    #[test]
140    fn test_contains_frame_inside() {
141        let r = TimecodeRange::new(10, 20).expect("valid timecode range");
142        assert!(r.contains_frame(10));
143        assert!(r.contains_frame(15));
144        assert!(r.contains_frame(20));
145    }
146
147    #[test]
148    fn test_contains_frame_outside() {
149        let r = TimecodeRange::new(10, 20).expect("valid timecode range");
150        assert!(!r.contains_frame(9));
151        assert!(!r.contains_frame(21));
152    }
153
154    #[test]
155    fn test_overlaps_true() {
156        let a = TimecodeRange::new(0, 10).expect("valid timecode range");
157        let b = TimecodeRange::new(5, 15).expect("valid timecode range");
158        assert!(a.overlaps(&b));
159        assert!(b.overlaps(&a));
160    }
161
162    #[test]
163    fn test_overlaps_adjacent_no_overlap() {
164        let a = TimecodeRange::new(0, 9).expect("valid timecode range");
165        let b = TimecodeRange::new(10, 20).expect("valid timecode range");
166        // Adjacent but not overlapping (end of a == start of b - 1)
167        assert!(!a.overlaps(&b));
168    }
169
170    #[test]
171    fn test_overlaps_touching() {
172        let a = TimecodeRange::new(0, 10).expect("valid timecode range");
173        let b = TimecodeRange::new(10, 20).expect("valid timecode range");
174        // They share frame 10 → overlapping
175        assert!(a.overlaps(&b));
176    }
177
178    #[test]
179    fn test_split_at_valid() {
180        let r = TimecodeRange::new(0, 9).expect("valid timecode range");
181        let (left, right) = r.split_at(4).expect("split should succeed");
182        assert_eq!(left.start, 0);
183        assert_eq!(left.end, 4);
184        assert_eq!(right.start, 5);
185        assert_eq!(right.end, 9);
186    }
187
188    #[test]
189    fn test_split_at_boundary_invalid() {
190        let r = TimecodeRange::new(0, 9).expect("valid timecode range");
191        // Cannot split at the last frame (end = 9)
192        assert!(r.split_at(9).is_none());
193        // Cannot split before start
194        assert!(r.split_at(u64::MAX).is_none());
195    }
196
197    #[test]
198    fn test_list_total_frames() {
199        let mut list = TimecodeRangeList::new();
200        list.add(TimecodeRange::new(0, 9).expect("valid timecode range")); // 10 frames
201        list.add(TimecodeRange::new(20, 24).expect("valid timecode range")); // 5 frames
202        assert_eq!(list.total_frames(), 15);
203    }
204
205    #[test]
206    fn test_list_merge_adjacent() {
207        let mut list = TimecodeRangeList::new();
208        list.add(TimecodeRange::new(10, 20).expect("valid timecode range"));
209        list.add(TimecodeRange::new(21, 30).expect("valid timecode range")); // adjacent → merge
210        list.add(TimecodeRange::new(50, 60).expect("valid timecode range")); // gap → separate
211        let merged = list.merge_adjacent();
212        let ranges: Vec<_> = merged.iter().cloned().collect();
213        assert_eq!(ranges.len(), 2);
214        assert_eq!(ranges[0].start, 10);
215        assert_eq!(ranges[0].end, 30);
216        assert_eq!(ranges[1].start, 50);
217        assert_eq!(ranges[1].end, 60);
218    }
219
220    #[test]
221    fn test_list_merge_overlapping() {
222        let mut list = TimecodeRangeList::new();
223        list.add(TimecodeRange::new(0, 15).expect("valid timecode range"));
224        list.add(TimecodeRange::new(10, 25).expect("valid timecode range")); // overlapping → merge
225        let merged = list.merge_adjacent();
226        let ranges: Vec<_> = merged.iter().cloned().collect();
227        assert_eq!(ranges.len(), 1);
228        assert_eq!(ranges[0].end, 25);
229    }
230}