Skip to main content

provenant/license_detection/
position_set.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use bit_set::BitSet;
5
6use crate::license_detection::models::position_span::PositionSpan;
7
8/// A set of usize positions stored as a BitSet.
9/// Provides O(1) membership testing and efficient set operations.
10/// Caches bounds for cheap overlap pre-checks.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct PositionSet {
13    bitset: BitSet,
14    min_pos: usize,
15    max_pos: usize,
16}
17
18impl PositionSet {
19    /// Create a PositionSet from an iterator of usize positions.
20    pub fn from_usize_iter<I: IntoIterator<Item = usize>>(iter: I) -> Self {
21        let mut bitset = BitSet::new();
22        let mut min_pos = usize::MAX;
23        let mut max_pos = 0;
24
25        for pos in iter {
26            bitset.insert(pos);
27            min_pos = min_pos.min(pos);
28            max_pos = max_pos.max(pos);
29        }
30
31        Self {
32            bitset,
33            min_pos,
34            max_pos,
35        }
36    }
37
38    /// Create an empty PositionSet.
39    pub fn new() -> Self {
40        Self {
41            bitset: BitSet::new(),
42            min_pos: usize::MAX,
43            max_pos: 0,
44        }
45    }
46
47    /// Number of positions in the set.
48    pub fn len(&self) -> usize {
49        self.bitset.count()
50    }
51
52    /// Is the set empty?
53    pub fn is_empty(&self) -> bool {
54        self.bitset.is_empty()
55    }
56
57    /// Returns the minimum position in the set.
58    ///
59    /// Returns `usize::MAX` for an empty set.
60    pub fn min_pos(&self) -> usize {
61        self.min_pos
62    }
63
64    /// Returns the maximum position in the set.
65    ///
66    /// Returns `0` for an empty set.
67    pub fn max_pos(&self) -> usize {
68        self.max_pos
69    }
70
71    /// Insert a position.
72    pub fn insert(&mut self, pos: usize) -> bool {
73        let inserted = self.bitset.insert(pos);
74        if inserted {
75            self.min_pos = self.min_pos.min(pos);
76            self.max_pos = self.max_pos.max(pos);
77        }
78        inserted
79    }
80
81    /// Extend this set from a PositionSpan without allocating an intermediate set.
82    pub fn extend_from_span(&mut self, span: &PositionSpan) {
83        match span {
84            PositionSpan::Range { start, end } => {
85                for pos in *start..*end {
86                    self.insert(pos);
87                }
88            }
89            PositionSpan::Discrete(positions) => {
90                for &pos in positions {
91                    self.insert(pos);
92                }
93            }
94        }
95    }
96
97    /// Check if position is in the set.
98    pub fn contains(&self, pos: usize) -> bool {
99        self.bitset.contains(pos)
100    }
101
102    /// Remove a position from the set.
103    pub fn remove(&mut self, pos: usize) -> bool {
104        self.bitset.remove(pos)
105    }
106
107    /// Remove all positions in a span from the set.
108    pub fn remove_span(&mut self, span: &PositionSpan) {
109        for pos in span.iter() {
110            self.remove(pos);
111        }
112    }
113
114    /// Quick check if a range [range_start, range_end) might overlap with this set.
115    /// Returns true if the bounding boxes overlap, false if they definitely don't.
116    /// This is O(1) and used as a pre-filter before the expensive BitSet check.
117    #[inline]
118    pub fn may_overlap_range(&self, range_start: usize, range_end: usize) -> bool {
119        // min_pos == usize::MAX means empty set (see new())
120        if self.min_pos == usize::MAX {
121            return false;
122        }
123        range_end > self.min_pos && range_start <= self.max_pos
124    }
125
126    /// Build the subset of positions that fall within the half-open range
127    /// `[start, end)`.
128    ///
129    /// The naive form is `self.iter().filter(|p| start <= p < end)`, which
130    /// always walks the *entire* set. When this set is a whole-query matchables
131    /// set (hundreds of thousands of positions for a multi-MB file) but the
132    /// range is a single small query run (tens of tokens), that full scan is
133    /// repeated once per run and the per-file cost degrades to
134    /// `O(num_runs * total_positions)` — quadratic in file size. Translation
135    /// catalogs (`.po`) are the pathological case: thousands of small runs over
136    /// a single huge token stream.
137    ///
138    /// Instead, iterate whichever side is smaller. The clamped range can never
139    /// be longer than the set's own span, and for a small run it is far
140    /// shorter, so we probe `range.contains` against this set's O(1) membership
141    /// test. For a range that spans the whole set (the whole-query run) we fall
142    /// back to walking the set directly, which is the same work as before.
143    pub fn restricted_to_range(&self, start: usize, end: usize) -> PositionSet {
144        if self.min_pos == usize::MAX || end <= start {
145            return PositionSet::new();
146        }
147
148        // Clamp to the set's populated bounds; positions outside cannot be present.
149        let lo = start.max(self.min_pos);
150        let hi = end.min(self.max_pos + 1);
151        if hi <= lo {
152            return PositionSet::new();
153        }
154
155        // `iter()` walks the bit vector across the set's whole populated span,
156        // so use that span as its cost proxy. Probing the clamped range costs
157        // `range_len` O(1) membership tests. Probe only when the clamped range
158        // is strictly narrower than the set's span; when it already covers the
159        // span (the whole-query run, `range_len == set_span`) iterate the set
160        // directly, which is the original behavior and avoids probing every
161        // gap position. The clamp guarantees `range_len <= set_span`, so `<`
162        // (not `<=`) is what keeps the whole-query case on the set-iter path.
163        let range_len = hi - lo;
164        let set_span = self.max_pos - self.min_pos + 1;
165        if range_len < set_span {
166            (lo..hi).filter(|&pos| self.bitset.contains(pos)).collect()
167        } else {
168            self.bitset
169                .iter()
170                .filter(|&pos| pos >= lo && pos < hi)
171                .collect()
172        }
173    }
174
175    /// Compute the union of this set with another PositionSet.
176    ///
177    /// Returns a new PositionSet containing all positions from both sets.
178    pub fn union(&self, other: &PositionSet) -> PositionSet {
179        let mut result = self.clone();
180        for pos in other.iter() {
181            result.insert(pos);
182        }
183        result
184    }
185
186    /// Return the difference (elements in self but not in other).
187    pub fn difference(&self, other: &PositionSet) -> PositionSet {
188        let mut result = PositionSet::new();
189        for pos in self.bitset.iter() {
190            if !other.bitset.contains(pos) {
191                result.insert(pos);
192            }
193        }
194        result
195    }
196
197    /// Count elements in the intersection of self and other.
198    pub fn intersection_len(&self, other: &PositionSet) -> usize {
199        self.bitset
200            .iter()
201            .filter(|&p| other.bitset.contains(p))
202            .count()
203    }
204
205    /// Check if this set overlaps with a PositionSpan.
206    /// Uses O(1) bounds check before the O(n) element-wise check.
207    pub fn overlaps_span(&self, span: &PositionSpan) -> bool {
208        let (span_min, span_max) = span.bounds();
209        if span.is_empty() {
210            return false;
211        }
212        if !self.may_overlap_range(span_min, span_max) {
213            return false;
214        }
215        span.iter().any(|p| self.contains(p))
216    }
217
218    /// Check if this set contains all positions in a range.
219    /// Returns true for empty ranges.
220    pub fn contains_range(&self, range: std::ops::Range<usize>) -> bool {
221        if range.is_empty() {
222            return true;
223        }
224        let (start, end) = (range.start, range.end);
225        if !self.may_overlap_range(start, end) {
226            return false;
227        }
228        (start..end).all(|pos| self.contains(pos))
229    }
230
231    /// Iterate over positions.
232    pub fn iter(&self) -> impl Iterator<Item = usize> + '_ {
233        self.bitset.iter()
234    }
235
236    /// Convert this PositionSet to a PositionSpan.
237    ///
238    /// If positions are contiguous, returns a Range; otherwise returns Discrete.
239    pub fn to_position_span(&self) -> PositionSpan {
240        if self.is_empty() {
241            return PositionSpan::empty();
242        }
243
244        let positions: Vec<usize> = self.iter().collect();
245        let is_contiguous = positions.windows(2).all(|w| w[1] == w[0] + 1);
246
247        if is_contiguous {
248            PositionSpan::range(self.min_pos, self.max_pos + 1)
249        } else {
250            PositionSpan::from_positions(positions)
251        }
252    }
253}
254
255impl Default for PositionSet {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261impl std::iter::FromIterator<usize> for PositionSet {
262    fn from_iter<T: IntoIterator<Item = usize>>(iter: T) -> Self {
263        Self::from_usize_iter(iter)
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn test_new_empty() {
273        let set = PositionSet::new();
274        assert!(set.is_empty());
275        assert_eq!(set.len(), 0);
276    }
277
278    #[test]
279    fn test_from_usize_iter_sorted() {
280        let set = PositionSet::from_usize_iter(vec![1, 2, 3]);
281        assert_eq!(set.len(), 3);
282        assert_eq!(set.iter().collect::<Vec<_>>(), vec![1, 2, 3]);
283    }
284
285    #[test]
286    fn test_from_usize_iter_unsorted() {
287        let set = PositionSet::from_usize_iter(vec![3, 1, 2]);
288        assert_eq!(set.iter().collect::<Vec<_>>(), vec![1, 2, 3]);
289    }
290
291    #[test]
292    fn test_from_usize_iter_dedup() {
293        let set = PositionSet::from_usize_iter(vec![1, 2, 2, 3, 3, 3]);
294        assert_eq!(set.len(), 3);
295        assert_eq!(set.iter().collect::<Vec<_>>(), vec![1, 2, 3]);
296    }
297
298    // `restricted_to_range` must equal the naive `iter().filter(start <= p < end)`
299    // form for every range, regardless of which internal branch it picks (probe
300    // the range vs. walk the set). This pins the contract that the perf
301    // optimization is purely behavior-preserving.
302    #[test]
303    fn test_restricted_to_range_matches_naive_filter() {
304        let positions = vec![0usize, 1, 5, 6, 7, 100, 101, 5000, 5001, 9999];
305        let set = PositionSet::from_usize_iter(positions.iter().copied());
306
307        let naive = |start: usize, end: usize| -> Vec<usize> {
308            set.iter().filter(|&p| p >= start && p < end).collect()
309        };
310
311        let cases = [
312            (0, 0),         // empty range
313            (3, 3),         // empty range mid-set
314            (0, 10000),     // whole span (walk-the-set branch)
315            (5, 8),         // small interior run (probe-the-range branch)
316            (6, 7),         // single element
317            (0, 2),         // at the low bound
318            (5000, 6000),   // straddles populated and empty
319            (200, 5000),    // gap then boundary (exclusive end)
320            (12000, 99999), // entirely above max_pos
321        ];
322
323        for (start, end) in cases {
324            let got: Vec<usize> = set.restricted_to_range(start, end).iter().collect();
325            assert_eq!(
326                got,
327                naive(start, end),
328                "restricted_to_range({start}, {end}) diverged from naive filter"
329            );
330        }
331    }
332
333    #[test]
334    fn test_restricted_to_range_empty_set() {
335        let set = PositionSet::new();
336        assert!(set.restricted_to_range(0, 100).is_empty());
337    }
338
339    #[test]
340    fn test_restricted_to_range_unbounded_end() {
341        // The whole-query run uses end == usize::MAX; it must return every
342        // position at or after `start` without overflowing.
343        let set = PositionSet::from_usize_iter(vec![1usize, 50, 999]);
344        let got: Vec<usize> = set.restricted_to_range(0, usize::MAX).iter().collect();
345        assert_eq!(got, vec![1, 50, 999]);
346
347        let got: Vec<usize> = set.restricted_to_range(50, usize::MAX).iter().collect();
348        assert_eq!(got, vec![50, 999]);
349    }
350
351    #[test]
352    fn test_insert() {
353        let mut set = PositionSet::new();
354        assert!(set.insert(2));
355        assert!(set.insert(1));
356        assert!(set.insert(3));
357        assert!(!set.insert(2)); // Already present
358        assert_eq!(set.iter().collect::<Vec<_>>(), vec![1, 2, 3]);
359    }
360
361    #[test]
362    fn test_difference() {
363        let a = PositionSet::from_usize_iter(vec![1, 2, 3, 4]);
364        let b = PositionSet::from_usize_iter(vec![2, 4, 6]);
365        let diff = a.difference(&b);
366        assert_eq!(diff.iter().collect::<Vec<_>>(), vec![1, 3]);
367    }
368
369    #[test]
370    fn test_difference_empty() {
371        let a = PositionSet::from_usize_iter(vec![1, 2, 3]);
372        let b = PositionSet::new();
373        let diff = a.difference(&b);
374        assert_eq!(diff.iter().collect::<Vec<_>>(), vec![1, 2, 3]);
375    }
376
377    #[test]
378    fn test_difference_all_overlap() {
379        let a = PositionSet::from_usize_iter(vec![1, 2, 3]);
380        let b = PositionSet::from_usize_iter(vec![1, 2, 3]);
381        let diff = a.difference(&b);
382        assert!(diff.is_empty());
383    }
384
385    #[test]
386    fn test_contains() {
387        let set = PositionSet::from_usize_iter(vec![1, 3, 5]);
388        assert!(set.contains(1));
389        assert!(set.contains(3));
390        assert!(set.contains(5));
391        assert!(!set.contains(0));
392        assert!(!set.contains(2));
393        assert!(!set.contains(4));
394    }
395
396    #[test]
397    fn test_collect() {
398        let set: PositionSet = vec![3, 1, 2].into_iter().collect();
399        assert_eq!(set.iter().collect::<Vec<_>>(), vec![1, 2, 3]);
400    }
401
402    #[test]
403    fn test_extend_from_span_range() {
404        let mut set = PositionSet::new();
405        set.extend_from_span(&PositionSpan::range(5, 10));
406        assert_eq!(set.len(), 5);
407        assert!(set.contains(5));
408        assert!(set.contains(9));
409        assert!(!set.contains(4));
410        assert!(!set.contains(10));
411    }
412
413    #[test]
414    fn test_extend_from_span_discrete() {
415        let mut set = PositionSet::new();
416        set.extend_from_span(&PositionSpan::from_positions(vec![1, 3, 5]));
417        assert_eq!(set.len(), 3);
418        assert!(set.contains(1));
419        assert!(set.contains(3));
420        assert!(set.contains(5));
421        assert!(!set.contains(2));
422    }
423
424    #[test]
425    fn test_extend_from_span_merge() {
426        let mut set = PositionSet::from_usize_iter(vec![1, 2, 3]);
427        set.extend_from_span(&PositionSpan::range(2, 6));
428        assert_eq!(set.len(), 5);
429        assert_eq!(set.iter().collect::<Vec<_>>(), vec![1, 2, 3, 4, 5]);
430    }
431
432    #[test]
433    fn test_overlaps_span_range_yes() {
434        let set = PositionSet::from_usize_iter(vec![5, 6, 7]);
435        assert!(set.overlaps_span(&PositionSpan::range(6, 10)));
436        assert!(set.overlaps_span(&PositionSpan::range(0, 6)));
437    }
438
439    #[test]
440    fn test_overlaps_span_range_no() {
441        let set = PositionSet::from_usize_iter(vec![1, 2, 3]);
442        assert!(!set.overlaps_span(&PositionSpan::range(5, 10)));
443        assert!(!set.overlaps_span(&PositionSpan::range(10, 20)));
444    }
445
446    #[test]
447    fn test_overlaps_span_discrete_yes() {
448        let set = PositionSet::from_usize_iter(vec![1, 2, 3, 10, 11]);
449        assert!(set.overlaps_span(&PositionSpan::from_positions(vec![3, 4, 5])));
450        assert!(set.overlaps_span(&PositionSpan::from_positions(vec![0, 1])));
451    }
452
453    #[test]
454    fn test_overlaps_span_discrete_no() {
455        let set = PositionSet::from_usize_iter(vec![1, 2, 3]);
456        assert!(!set.overlaps_span(&PositionSpan::from_positions(vec![5, 6, 7])));
457    }
458
459    #[test]
460    fn test_overlaps_span_empty() {
461        let set = PositionSet::from_usize_iter(vec![1, 2, 3]);
462        assert!(!set.overlaps_span(&PositionSpan::empty()));
463    }
464
465    #[test]
466    fn test_contains_range_yes() {
467        let set = PositionSet::from_usize_iter(vec![1, 2, 3, 4, 5]);
468        assert!(set.contains_range(1..6));
469        assert!(set.contains_range(2..4));
470        assert!(set.contains_range(1..6));
471    }
472
473    #[test]
474    fn test_contains_range_no() {
475        let set = PositionSet::from_usize_iter(vec![1, 2, 3]);
476        assert!(!set.contains_range(0..4));
477        assert!(!set.contains_range(3..5));
478        assert!(!set.contains_range(5..10));
479    }
480
481    #[test]
482    fn test_contains_range_empty() {
483        let set = PositionSet::from_usize_iter(vec![1, 2, 3]);
484        assert!(set.contains_range(5..5));
485        assert!(set.contains_range(0..0));
486    }
487
488    #[test]
489    fn test_contains_range_disjoint() {
490        let set = PositionSet::from_usize_iter(vec![10, 11, 12]);
491        assert!(!set.contains_range(0..5));
492        assert!(!set.contains_range(15..20));
493    }
494
495    #[test]
496    fn test_to_position_span_empty() {
497        let set = PositionSet::new();
498        let span = set.to_position_span();
499        assert!(span.is_empty());
500    }
501
502    #[test]
503    fn test_to_position_span_contiguous() {
504        let set = PositionSet::from_usize_iter(vec![5, 6, 7, 8]);
505        let span = set.to_position_span();
506        assert_eq!(span, PositionSpan::range(5, 9));
507    }
508
509    #[test]
510    fn test_to_position_span_single() {
511        let set = PositionSet::from_usize_iter(vec![10]);
512        let span = set.to_position_span();
513        assert_eq!(span, PositionSpan::range(10, 11));
514    }
515
516    #[test]
517    fn test_to_position_span_discrete() {
518        let set = PositionSet::from_usize_iter(vec![1, 3, 5, 7]);
519        let span = set.to_position_span();
520        assert_eq!(span, PositionSpan::from_positions(vec![1, 3, 5, 7]));
521    }
522
523    #[test]
524    fn test_to_position_span_two_with_gap() {
525        let set = PositionSet::from_usize_iter(vec![1, 3]);
526        let span = set.to_position_span();
527        assert_eq!(span, PositionSpan::from_positions(vec![1, 3]));
528    }
529
530    #[test]
531    fn test_min_max_pos() {
532        let set = PositionSet::from_usize_iter(vec![5, 10, 15]);
533        assert_eq!(set.min_pos(), 5);
534        assert_eq!(set.max_pos(), 15);
535    }
536
537    #[test]
538    fn test_min_max_pos_empty() {
539        let set = PositionSet::new();
540        assert_eq!(set.min_pos(), usize::MAX);
541        assert_eq!(set.max_pos(), 0);
542    }
543}