1use std::ops::Range;
9
10use num_traits::{CheckedAdd, SaturatingAdd, SaturatingSub, Unsigned};
11
12#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct Span<Idx: Unsigned + Copy> {
15 pub start: Idx,
17
18 pub len: Idx,
20}
21
22impl<Idx: Unsigned + Copy> Span<Idx> {
23 #[inline]
25 pub const fn from_start_len(start: Idx, len: Idx) -> Self {
26 Self { start, len }
27 }
28
29 #[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 #[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 #[inline]
64 pub fn end(&self) -> Idx {
65 self.start + self.len
66 }
67
68 #[inline]
70 pub fn is_empty(&self) -> bool {
71 self.len.is_zero()
72 }
73
74 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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
261macro_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
283impl<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); assert!(span.intersects(Span::from_start_len(6, 1))); assert!(span.intersects(Span::from_start_len(0, 4))); assert!(!span.intersects(Span::from_start_len(7, 1))); assert!(!span.intersects(Span::from_start_len(0, 3))); assert!(!span.intersects(Span::from_start_len(5, 0))); 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}