Skip to main content

re_span/
lib.rs

1//! An integer range that always has a non-negative length.
2//!
3//! The standard [`std::ops::Range`] can have `start > end`
4//! Taking a `Range` by argument thus means the callee must check for this eventuality and return an error.
5//!
6//! In contrast, [`Span`] always has a non-negative length, i.e. `len >= 0`.
7
8use std::ops::Range;
9
10use num_traits::{CheckedAdd, SaturatingAdd, SaturatingSub, Unsigned};
11
12/// An integer range who's length is always at least zero.
13#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct Span<Idx: Unsigned + Copy> {
15    /// The index of the first element.
16    pub start: Idx,
17
18    /// The number of elements in the range.
19    pub len: Idx,
20}
21
22impl<Idx: Unsigned + Copy> Span<Idx> {
23    /// Construct from `start` and `len`.
24    #[inline]
25    pub const fn from_start_len(start: Idx, len: Idx) -> Self {
26        Self { start, len }
27    }
28
29    /// Construct from `start` (inclusive) and `end` (exclusive).
30    ///
31    /// See also [`Self::try_from_start_end`].
32    ///
33    /// # Panics
34    /// Panics if `end < start`.
35    #[inline]
36    pub fn from_start_end(start: Idx, end: Idx) -> Self
37    where
38        Idx: PartialOrd,
39    {
40        assert!(start <= end, "Span start must be less than or equal to end");
41
42        Self {
43            start,
44            len: end - start,
45        }
46    }
47
48    /// Construct from `start` (inclusive) and `end` (exclusive).
49    ///
50    /// Returns `None` if `end < start`.
51    #[inline]
52    pub fn try_from_start_end(start: Idx, end: Idx) -> Option<Self>
53    where
54        Idx: PartialOrd,
55    {
56        (start <= end).then(|| Self {
57            start,
58            len: end - start,
59        })
60    }
61
62    /// The next element, just outside the range.
63    #[inline]
64    pub fn end(&self) -> Idx {
65        self.start + self.len
66    }
67
68    /// Is the span empty, i.e. has zero length?
69    #[inline]
70    pub fn is_empty(&self) -> bool {
71        self.len.is_zero()
72    }
73
74    /// Is the given index within the span?
75    #[inline]
76    pub fn contains(&self, idx: Idx) -> bool
77    where
78        Idx: PartialOrd,
79    {
80        self.start <= idx && idx < self.end()
81    }
82
83    /// Useful when slicing a slice
84    #[inline]
85    pub fn range(self) -> Range<Idx> {
86        let Self { start, len } = self;
87        Range {
88            start,
89            end: start + len,
90        }
91    }
92
93    pub fn try_cast<Narrow>(self) -> Option<Span<Narrow>>
94    where
95        Narrow: TryFrom<Idx> + Unsigned + Copy,
96    {
97        Some(Span {
98            start: self.start.try_into().ok()?,
99            len: self.len.try_into().ok()?,
100        })
101    }
102
103    /// Do the two spans share at least one index?
104    ///
105    /// An empty span intersects nothing.
106    #[inline]
107    pub fn intersects(self, other: Self) -> bool
108    where
109        Idx: PartialOrd,
110    {
111        !self.is_empty()
112            && !other.is_empty()
113            && self.start < other.end()
114            && other.start < self.end()
115    }
116
117    /// The smallest span covering both `self` and `other`, including any gap between them.
118    #[inline]
119    pub fn union(self, other: Self) -> Self
120    where
121        Idx: Ord,
122    {
123        let start = self.start.min(other.start);
124        let end = self.end().max(other.end());
125        Self {
126            start,
127            len: end - start,
128        }
129    }
130
131    /// Clamp the span so it fits inside a container of the given length, i.e. within `0..len`.
132    ///
133    /// The result is empty if the span starts at or beyond `len`.
134    #[inline]
135    pub fn clamped_to(self, len: Idx) -> Self
136    where
137        Idx: Ord,
138    {
139        let start = self.start.min(len);
140        Self {
141            start,
142            len: self.len.min(len - start),
143        }
144    }
145
146    /// Shift the span up by `rhs`, keeping its length.
147    ///
148    /// Overflows like normal unsigned addition if `self.end() + rhs` exceeds the maximum;
149    /// see [`Self::saturating_add`] for a clamping version.
150    #[inline]
151    #[must_use]
152    #[expect(clippy::should_implement_trait)]
153    pub fn add(self, rhs: Idx) -> Self {
154        let Self { start, len } = self;
155        Self {
156            start: start + rhs,
157            len,
158        }
159    }
160
161    /// Shift the span down by `rhs`, keeping its length.
162    ///
163    /// Underflows like normal unsigned subtraction if `rhs > start`;
164    /// see [`Self::saturating_sub`] for a clamping version.
165    #[inline]
166    #[must_use]
167    #[expect(clippy::should_implement_trait)]
168    pub fn sub(self, rhs: Idx) -> Self {
169        let Self { start, len } = self;
170        Self {
171            start: start - rhs,
172            len,
173        }
174    }
175
176    /// Multiply both `start` and `len` by `scale`.
177    ///
178    /// Useful for translating an element-span into a byte-span,
179    /// by scaling with `size_of::<T>()`.
180    #[inline]
181    #[must_use]
182    pub fn scale(self, scale: Idx) -> Self {
183        let Self { start, len } = self;
184        Self {
185            start: scale * start,
186            len: scale * len,
187        }
188    }
189
190    /// Shift the span up by `rhs`, clamping both endpoints at the maximum value.
191    ///
192    /// The length shrinks if the span crosses the maximum:
193    /// for `u8`, `(250..254).saturating_add(3) == 253..255`.
194    #[inline]
195    pub fn saturating_add(self, rhs: Idx) -> Self
196    where
197        Idx: SaturatingAdd,
198    {
199        let start = self.start.saturating_add(&rhs);
200        let end = self.start.saturating_add(&self.len).saturating_add(&rhs);
201        Self {
202            start,
203            len: end - start,
204        }
205    }
206
207    /// Shift the span down by `rhs`, clamping both endpoints at zero.
208    ///
209    /// The length shrinks if the span crosses zero:
210    /// `(2..5).saturating_sub(3) == 0..2`.
211    #[inline]
212    pub fn saturating_sub(self, rhs: Idx) -> Self
213    where
214        Idx: SaturatingSub,
215    {
216        let start = self.start.saturating_sub(&rhs);
217        let end = self.end().saturating_sub(&rhs);
218        Self {
219            start,
220            len: end - start,
221        }
222    }
223}
224
225impl Span<u32> {
226    /// Widening cast; useful for indexing.
227    #[inline]
228    pub const fn range_usize(self) -> Range<usize> {
229        let Self { start, len } = self;
230        Range {
231            start: start as usize,
232            end: start as usize + len as usize,
233        }
234    }
235}
236
237impl Span<usize> {
238    /// Widening cast.
239    #[inline]
240    pub const fn cast_u64(self) -> Span<u64> {
241        let Self { start, len } = self;
242        Span {
243            start: start as u64,
244            len: len as u64,
245        }
246    }
247}
248
249impl Span<u64> {
250    /// Cast to native pointer width; useful for indexing on native platforms.
251    #[inline]
252    pub const fn range_usize(self) -> Range<usize> {
253        let Self { start, len } = self;
254        Range {
255            start: start as usize,
256            end: start as usize + len as usize,
257        }
258    }
259}
260
261/// Iterate over the indices of the span.
262///
263/// Implemented per concrete index type because the underlying
264/// [`Range`] iterator requires the unstable `Step` trait.
265macro_rules! impl_into_iterator {
266    ($($idx:ty),*) => {
267        $(
268            impl IntoIterator for Span<$idx> {
269                type Item = $idx;
270                type IntoIter = Range<$idx>;
271
272                #[inline]
273                fn into_iter(self) -> Self::IntoIter {
274                    self.range()
275                }
276            }
277        )*
278    };
279}
280
281impl_into_iterator!(u8, u16, u32, u64, usize);
282
283/// Formats like the equivalent [`Range`], e.g. `3..7`.
284impl<Idx: Unsigned + Copy + CheckedAdd + std::fmt::Debug> std::fmt::Debug for Span<Idx> {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        let Self { start, len } = *self;
287        match start.checked_add(&len) {
288            Some(end) => write!(f, "{start:?}..{end:?}"),
289            None => write!(f, "{start:?}..{start:?}+{len:?} (overflow)"),
290        }
291    }
292}
293
294impl<Idx: Unsigned + Copy> From<Span<Idx>> for Range<Idx> {
295    #[inline]
296    fn from(value: Span<Idx>) -> Self {
297        value.range()
298    }
299}
300
301impl<Idx: Unsigned + Copy> From<Span<Idx>> for core::range::Range<Idx> {
302    #[inline]
303    fn from(value: Span<Idx>) -> Self {
304        let Range { start, end } = value.range();
305        Self { start, end }
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::Span;
312
313    #[test]
314    fn try_from_start_end_rejects_inverted_ranges() {
315        assert_eq!(
316            Span::try_from_start_end(3_u64, 7),
317            Some(Span::from_start_len(3, 4))
318        );
319        assert_eq!(
320            Span::try_from_start_end(5_u64, 5),
321            Some(Span::from_start_len(5, 0))
322        );
323        assert_eq!(Span::try_from_start_end(7_u64, 3), None);
324    }
325
326    #[test]
327    fn intersects_is_half_open_and_empty_spans_intersect_nothing() {
328        let span = Span::from_start_len(3_u64, 4); // 3..7
329        assert!(span.intersects(Span::from_start_len(6, 1))); // last index
330        assert!(span.intersects(Span::from_start_len(0, 4))); // overlaps the start
331        assert!(!span.intersects(Span::from_start_len(7, 1))); // just past the end
332        assert!(!span.intersects(Span::from_start_len(0, 3))); // ends where `span` starts
333        assert!(!span.intersects(Span::from_start_len(5, 0))); // empty, inside `span`
334        assert!(!Span::from_start_len(5_u64, 0).intersects(span));
335    }
336
337    #[test]
338    fn union_covers_both_spans_and_the_gap() {
339        assert_eq!(
340            Span::from_start_len(2_u64, 3).union(Span::from_start_len(10, 2)),
341            Span::from_start_len(2, 10)
342        );
343        assert_eq!(
344            Span::from_start_len(2_u64, 10).union(Span::from_start_len(4, 2)),
345            Span::from_start_len(2, 10)
346        );
347        assert_eq!(
348            Span::from_start_len(5_u64, 0).union(Span::from_start_len(5, 0)),
349            Span::from_start_len(5, 0)
350        );
351    }
352
353    #[test]
354    fn clamped_to_caps_both_endpoints() {
355        assert_eq!(
356            Span::from_start_len(2_u64, 3).clamped_to(10),
357            Span::from_start_len(2, 3)
358        );
359        assert_eq!(
360            Span::from_start_len(2_u64, 30).clamped_to(10),
361            Span::from_start_len(2, 8)
362        );
363        assert_eq!(
364            Span::from_start_len(10_u64, 3).clamped_to(10),
365            Span::from_start_len(10, 0)
366        );
367        assert_eq!(
368            Span::from_start_len(20_u64, 3).clamped_to(10),
369            Span::from_start_len(10, 0)
370        );
371    }
372
373    #[test]
374    fn saturating_add_clamps_at_the_maximum() {
375        assert_eq!(
376            Span::from_start_len(2_u8, 3).saturating_add(1),
377            Span::from_start_len(3, 3)
378        );
379        assert_eq!(
380            Span::from_start_len(250_u8, 4).saturating_add(3),
381            Span::from_start_len(253, 2)
382        );
383        assert_eq!(
384            Span::from_start_len(250_u8, 4).saturating_add(200),
385            Span::from_start_len(255, 0)
386        );
387    }
388
389    #[test]
390    fn debug_does_not_panic_on_overflowing_spans() {
391        assert_eq!(format!("{:?}", Span::from_start_len(3_u8, 4)), "3..7");
392        assert_eq!(
393            format!("{:?}", Span::from_start_len(200_u8, 100)),
394            "200..200+100 (overflow)"
395        );
396    }
397}