use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SlotRange {
start: u64,
end: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("invalid range: start_slot {start} > end_slot {end}")]
pub struct InvertedSlotRange {
pub start: u64,
pub end: u64,
}
impl SlotRange {
pub fn new(start: u64, end: u64) -> Result<Self, InvertedSlotRange> {
if start > end {
return Err(InvertedSlotRange { start, end });
}
Ok(Self { start, end })
}
pub const fn new_unchecked(start: u64, end: u64) -> Self {
Self { start, end }
}
pub const fn from_count(start: u64, count: u64) -> Self {
Self {
start,
end: start.saturating_add(count.saturating_sub(1)),
}
}
pub const fn start(&self) -> u64 {
self.start
}
pub const fn end(&self) -> u64 {
self.end
}
pub const fn len(&self) -> u64 {
self.end - self.start + 1
}
pub const fn is_empty(&self) -> bool {
false
}
pub const fn contains(&self, slot: u64) -> bool {
slot >= self.start && slot <= self.end
}
pub const fn overlaps(&self, other: SlotRange) -> bool {
self.start <= other.end && other.start <= self.end
}
}
impl fmt::Display for SlotRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}..={}", self.start, self.end)
}
}
impl From<SlotRange> for std::ops::RangeInclusive<u64> {
fn from(range: SlotRange) -> Self {
range.start..=range.end
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[test]
fn an_inverted_range_is_rejected() {
assert_eq!(
SlotRange::new(10, 9),
Err(InvertedSlotRange { start: 10, end: 9 })
);
assert!(SlotRange::new(10, 10).is_ok());
}
#[test]
fn length_is_inclusive_of_both_bounds() {
assert_eq!(SlotRange::new(10, 10).expect("range").len(), 1);
assert_eq!(SlotRange::new(10, 19).expect("range").len(), 10);
}
#[test]
fn from_count_matches_the_start_plus_count_convention() {
assert_eq!(
SlotRange::from_count(100, 1),
SlotRange::new_unchecked(100, 100)
);
assert_eq!(
SlotRange::from_count(100, 10),
SlotRange::new_unchecked(100, 109)
);
assert_eq!(
SlotRange::from_count(100, 0),
SlotRange::new_unchecked(100, 100)
);
}
#[test]
fn contains_is_inclusive() {
let range = SlotRange::new(10, 12).expect("range");
assert!(range.contains(10) && range.contains(12));
assert!(!range.contains(9) && !range.contains(13));
}
#[rstest]
#[case::shares_the_lower_bound(5, 10, true)]
#[case::shares_the_upper_bound(12, 20, true)]
#[case::lies_inside(11, 11, true)]
#[case::encloses(0, 20, true)]
#[case::ends_just_below(5, 9, false)]
#[case::starts_just_above(13, 20, false)]
fn overlaps_is_inclusive(#[case] start: u64, #[case] end: u64, #[case] expected: bool) {
let range = SlotRange::new_unchecked(10, 12);
let other = SlotRange::new_unchecked(start, end);
assert_eq!(range.overlaps(other), expected);
assert_eq!(other.overlaps(range), expected);
}
}