use std::ops::Range;
use num_traits::{CheckedAdd, SaturatingAdd, SaturatingSub, Unsigned};
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Span<Idx: Unsigned + Copy> {
pub start: Idx,
pub len: Idx,
}
impl<Idx: Unsigned + Copy> Span<Idx> {
#[inline]
pub const fn from_start_len(start: Idx, len: Idx) -> Self {
Self { start, len }
}
#[inline]
pub fn from_start_end(start: Idx, end: Idx) -> Self
where
Idx: PartialOrd,
{
assert!(start <= end, "Span start must be less than or equal to end");
Self {
start,
len: end - start,
}
}
#[inline]
pub fn try_from_start_end(start: Idx, end: Idx) -> Option<Self>
where
Idx: PartialOrd,
{
(start <= end).then(|| Self {
start,
len: end - start,
})
}
#[inline]
pub fn end(&self) -> Idx {
self.start + self.len
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len.is_zero()
}
#[inline]
pub fn contains(&self, idx: Idx) -> bool
where
Idx: PartialOrd,
{
self.start <= idx && idx < self.end()
}
#[inline]
pub fn range(self) -> Range<Idx> {
let Self { start, len } = self;
Range {
start,
end: start + len,
}
}
pub fn try_cast<Narrow>(self) -> Option<Span<Narrow>>
where
Narrow: TryFrom<Idx> + Unsigned + Copy,
{
Some(Span {
start: self.start.try_into().ok()?,
len: self.len.try_into().ok()?,
})
}
#[inline]
pub fn intersects(self, other: Self) -> bool
where
Idx: PartialOrd,
{
!self.is_empty()
&& !other.is_empty()
&& self.start < other.end()
&& other.start < self.end()
}
#[inline]
pub fn union(self, other: Self) -> Self
where
Idx: Ord,
{
let start = self.start.min(other.start);
let end = self.end().max(other.end());
Self {
start,
len: end - start,
}
}
#[inline]
pub fn clamped_to(self, len: Idx) -> Self
where
Idx: Ord,
{
let start = self.start.min(len);
Self {
start,
len: self.len.min(len - start),
}
}
#[inline]
#[must_use]
#[expect(clippy::should_implement_trait)]
pub fn add(self, rhs: Idx) -> Self {
let Self { start, len } = self;
Self {
start: start + rhs,
len,
}
}
#[inline]
#[must_use]
#[expect(clippy::should_implement_trait)]
pub fn sub(self, rhs: Idx) -> Self {
let Self { start, len } = self;
Self {
start: start - rhs,
len,
}
}
#[inline]
#[must_use]
pub fn scale(self, scale: Idx) -> Self {
let Self { start, len } = self;
Self {
start: scale * start,
len: scale * len,
}
}
#[inline]
pub fn saturating_add(self, rhs: Idx) -> Self
where
Idx: SaturatingAdd,
{
let start = self.start.saturating_add(&rhs);
let end = self.start.saturating_add(&self.len).saturating_add(&rhs);
Self {
start,
len: end - start,
}
}
#[inline]
pub fn saturating_sub(self, rhs: Idx) -> Self
where
Idx: SaturatingSub,
{
let start = self.start.saturating_sub(&rhs);
let end = self.end().saturating_sub(&rhs);
Self {
start,
len: end - start,
}
}
}
impl Span<u32> {
#[inline]
pub const fn range_usize(self) -> Range<usize> {
let Self { start, len } = self;
Range {
start: start as usize,
end: start as usize + len as usize,
}
}
}
impl Span<usize> {
#[inline]
pub const fn cast_u64(self) -> Span<u64> {
let Self { start, len } = self;
Span {
start: start as u64,
len: len as u64,
}
}
}
impl Span<u64> {
#[inline]
pub const fn range_usize(self) -> Range<usize> {
let Self { start, len } = self;
Range {
start: start as usize,
end: start as usize + len as usize,
}
}
}
macro_rules! impl_into_iterator {
($($idx:ty),*) => {
$(
impl IntoIterator for Span<$idx> {
type Item = $idx;
type IntoIter = Range<$idx>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.range()
}
}
)*
};
}
impl_into_iterator!(u8, u16, u32, u64, usize);
impl<Idx: Unsigned + Copy + CheckedAdd + std::fmt::Debug> std::fmt::Debug for Span<Idx> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self { start, len } = *self;
match start.checked_add(&len) {
Some(end) => write!(f, "{start:?}..{end:?}"),
None => write!(f, "{start:?}..{start:?}+{len:?} (overflow)"),
}
}
}
impl<Idx: Unsigned + Copy> From<Span<Idx>> for Range<Idx> {
#[inline]
fn from(value: Span<Idx>) -> Self {
value.range()
}
}
impl<Idx: Unsigned + Copy> From<Span<Idx>> for core::range::Range<Idx> {
#[inline]
fn from(value: Span<Idx>) -> Self {
let Range { start, end } = value.range();
Self { start, end }
}
}
#[cfg(test)]
mod tests {
use super::Span;
#[test]
fn try_from_start_end_rejects_inverted_ranges() {
assert_eq!(
Span::try_from_start_end(3_u64, 7),
Some(Span::from_start_len(3, 4))
);
assert_eq!(
Span::try_from_start_end(5_u64, 5),
Some(Span::from_start_len(5, 0))
);
assert_eq!(Span::try_from_start_end(7_u64, 3), None);
}
#[test]
fn intersects_is_half_open_and_empty_spans_intersect_nothing() {
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));
}
#[test]
fn union_covers_both_spans_and_the_gap() {
assert_eq!(
Span::from_start_len(2_u64, 3).union(Span::from_start_len(10, 2)),
Span::from_start_len(2, 10)
);
assert_eq!(
Span::from_start_len(2_u64, 10).union(Span::from_start_len(4, 2)),
Span::from_start_len(2, 10)
);
assert_eq!(
Span::from_start_len(5_u64, 0).union(Span::from_start_len(5, 0)),
Span::from_start_len(5, 0)
);
}
#[test]
fn clamped_to_caps_both_endpoints() {
assert_eq!(
Span::from_start_len(2_u64, 3).clamped_to(10),
Span::from_start_len(2, 3)
);
assert_eq!(
Span::from_start_len(2_u64, 30).clamped_to(10),
Span::from_start_len(2, 8)
);
assert_eq!(
Span::from_start_len(10_u64, 3).clamped_to(10),
Span::from_start_len(10, 0)
);
assert_eq!(
Span::from_start_len(20_u64, 3).clamped_to(10),
Span::from_start_len(10, 0)
);
}
#[test]
fn saturating_add_clamps_at_the_maximum() {
assert_eq!(
Span::from_start_len(2_u8, 3).saturating_add(1),
Span::from_start_len(3, 3)
);
assert_eq!(
Span::from_start_len(250_u8, 4).saturating_add(3),
Span::from_start_len(253, 2)
);
assert_eq!(
Span::from_start_len(250_u8, 4).saturating_add(200),
Span::from_start_len(255, 0)
);
}
#[test]
fn debug_does_not_panic_on_overflowing_spans() {
assert_eq!(format!("{:?}", Span::from_start_len(3_u8, 4)), "3..7");
assert_eq!(
format!("{:?}", Span::from_start_len(200_u8, 100)),
"200..200+100 (overflow)"
);
}
}