1use crate::ProgressEntry;
5
6#[derive(Debug)]
17pub struct InvertIter<I: Iterator<Item = ProgressEntry>> {
18 iter: I,
20 prev_end: u64,
22 total_size: u64,
24 window: u64,
26}
27
28impl<I> Iterator for InvertIter<I>
29where
30 I: Iterator<Item = ProgressEntry>,
31{
32 type Item = ProgressEntry;
33 fn next(&mut self) -> Option<Self::Item> {
34 let mut gap_start = self.prev_end;
35 let mut last_end = gap_start;
36 for range in self.iter.by_ref() {
37 debug_assert!(
42 range.start >= last_end,
43 "InvertIter requires sorted, non-overlapping ranges, but got {range:?} \
44 after a range ending at {last_end}; merge the input first"
45 );
46 last_end = range.end;
47 if range.start == gap_start {
48 gap_start = range.end;
49 continue;
50 }
51 let len = range.end.saturating_sub(range.start);
52 if len >= self.window {
53 self.prev_end = range.end;
54 return Some(gap_start..range.start);
55 }
56 }
57 if gap_start < self.total_size {
58 self.prev_end = self.total_size;
59 Some(gap_start..self.total_size)
60 } else {
61 None
62 }
63 }
64}
65
66pub const fn invert<I>(progress: I, total_size: u64, window: u64) -> InvertIter<I>
68where
69 I: Iterator<Item = ProgressEntry>,
70{
71 InvertIter {
72 iter: progress,
73 prev_end: 0,
74 total_size,
75 window,
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 #![allow(clippy::single_range_in_vec_init)]
82 use super::*;
83
84 fn invert_vec(progress: &[ProgressEntry], total_size: u64, window: u64) -> Vec<ProgressEntry> {
85 invert(progress.iter().cloned(), total_size, window).collect()
86 }
87
88 #[test]
89 fn test_windowed_invert() {
90 assert_eq!(invert_vec(&[10..20], 30, 1), [0..10, 20..30]);
91 assert_eq!(invert_vec(&[10..12], 30, 5), [0..30]);
92 assert_eq!(invert_vec(&[10..20, 25..27], 30, 5), [0..10, 20..30]);
93 assert_eq!(invert_vec(&[10..14, 25..27, 30..32], 50, 5), [0..50]);
94 assert_eq!(invert_vec(&[10..14, 25..49], 50, 5), [0..25, 49..50]);
95 assert_eq!(invert_vec(&[2..4, 6..8, 10..12], 15, 5), [0..15]);
96 assert_eq!(invert_vec(&[0..2, 10..20], 30, 5), [2..10, 20..30]);
97 }
98
99 #[test]
100 fn test_invert_empty_progress() {
101 assert_eq!(invert_vec(&[], 50, 1), [0..50]);
103 }
104
105 #[test]
106 fn test_invert_zero_total_size() {
107 assert_eq!(invert_vec(&[0..5], 0, 1), []);
109 }
110
111 #[test]
112 fn test_invert_full_cover_no_gaps() {
113 assert_eq!(invert_vec(&[0..30], 30, 1), []);
114 }
115
116 #[test]
117 fn test_invert_window_zero_keeps_small_entries() {
118 #![allow(clippy::single_range_in_vec_init)]
119 assert_eq!(invert_vec(&[10..12], 30, 0), [0..10, 12..30]);
122 }
123
124 #[test]
125 fn test_invert_trailing_gap_only() {
126 assert_eq!(invert_vec(&[0..20], 30, 1), [20..30]);
127 }
128
129 #[test]
130 fn test_invert_leading_gap_only() {
131 assert_eq!(invert_vec(&[10..30], 30, 1), [0..10]);
132 }
133
134 #[test]
135 fn test_invert_contiguous_then_gap() {
136 #![allow(clippy::single_range_in_vec_init)]
137 assert_eq!(invert_vec(&[0..10, 10..20], 30, 1), [20..30]);
138 }
139
140 #[test]
141 fn test_invert_reversed_range_is_safe_and_ignored() {
142 let reversed = core::ops::Range { start: 30, end: 5 };
146 assert_eq!(invert_vec(&[reversed], 50, 1), [0..50]);
147 }
148
149 #[test]
150 fn invert_window_boundary_len_equals_window_is_emitted() {
151 assert_eq!(invert_vec(&[0..5, 10..15], 20, 5), [5..10, 15..20]);
154 }
155
156 #[test]
157 fn invert_window_boundary_len_below_window_is_absorbed() {
158 assert_eq!(invert_vec(&[0..5, 10..14], 20, 5), [5..20]);
160 }
161
162 #[test]
163 fn invert_window_does_not_merge_small_gaps_between_large_ranges() {
164 assert_eq!(invert_vec(&[0..10, 12..22], 22, 5), [10..12]);
168 }
169
170 #[test]
171 fn invert_trailing_gap_emitted_regardless_of_window() {
172 assert_eq!(invert_vec(&[0..28], 30, 5), [28..30]);
175 assert_eq!(invert_vec(&[0..28], 30, 100), [28..30]);
176 }
177
178 #[test]
179 fn invert_multiple_gaps_across_next_calls() {
180 let mut it = invert([0..5, 10..20, 25..30].iter().cloned(), 30, 1);
183 assert_eq!(it.next(), Some(5..10));
184 assert_eq!(it.next(), Some(20..25));
185 assert_eq!(it.next(), None);
186 }
187
188 #[test]
189 fn invert_small_entry_contiguous_to_prior_is_not_a_gap() {
190 assert_eq!(invert_vec(&[0..10, 10..12], 30, 5), [12..30]);
193 }
194
195 #[test]
196 fn invert_reversed_entry_in_middle_is_ignored() {
197 let reversed = core::ops::Range { start: 30, end: 5 };
199 assert_eq!(invert_vec(&[0..10, reversed], 50, 1), [10..50]);
200 }
201
202 #[test]
203 #[cfg_attr(debug_assertions, should_panic)]
204 #[allow(clippy::should_panic_without_expect)]
205 fn invert_overlapping_input_fails_fast() {
206 let _ = invert_vec(&[0..10, 5..15], 20, 1);
209 }
210
211 #[test]
212 #[cfg_attr(debug_assertions, should_panic)]
213 #[allow(clippy::should_panic_without_expect)]
214 fn invert_unsorted_input_fails_fast() {
215 let _ = invert_vec(&[10..20, 0..5], 20, 1);
217 }
218}