Skip to main content

commonware_utils/
range.rs

1//! Non-empty [`Range`] type that guarantees at least one element.
2
3use bytes::{Buf, BufMut};
4use commonware_codec::{EncodeSize, Error as CodecError, Read, Write};
5use core::{fmt, ops::Range};
6
7/// Error returned when attempting to create a non-empty range from an empty range.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
9#[error("range is empty")]
10pub struct EmptyRange;
11
12/// A non-empty [`Range`] (`start..end`) where `start < end` is guaranteed.
13#[derive(Clone, PartialEq, Eq, Hash)]
14pub struct NonEmptyRange<Idx>(Range<Idx>);
15
16impl<Idx: fmt::Debug> fmt::Debug for NonEmptyRange<Idx> {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        self.0.fmt(f)
19    }
20}
21
22impl<Idx: PartialOrd> NonEmptyRange<Idx> {
23    /// Creates a `NonEmptyRange` if `start < end`.
24    pub fn new(range: Range<Idx>) -> Result<Self, EmptyRange> {
25        (range.start < range.end)
26            .then_some(Self(range))
27            .ok_or(EmptyRange)
28    }
29}
30
31impl<Idx: Copy> NonEmptyRange<Idx> {
32    /// Returns the start of the range.
33    pub const fn start(&self) -> Idx {
34        self.0.start
35    }
36
37    /// Returns the end of the range (exclusive).
38    pub const fn end(&self) -> Idx {
39        self.0.end
40    }
41}
42
43impl<Idx: PartialOrd> TryFrom<Range<Idx>> for NonEmptyRange<Idx> {
44    type Error = EmptyRange;
45
46    fn try_from(range: Range<Idx>) -> Result<Self, Self::Error> {
47        Self::new(range)
48    }
49}
50
51impl<Idx> From<NonEmptyRange<Idx>> for Range<Idx> {
52    fn from(r: NonEmptyRange<Idx>) -> Self {
53        r.0
54    }
55}
56
57impl<Idx> IntoIterator for NonEmptyRange<Idx>
58where
59    Range<Idx>: Iterator,
60{
61    type Item = <Range<Idx> as Iterator>::Item;
62    type IntoIter = Range<Idx>;
63
64    fn into_iter(self) -> Self::IntoIter {
65        self.0
66    }
67}
68
69impl<Idx: Write + PartialOrd> Write for NonEmptyRange<Idx> {
70    #[inline]
71    fn write(&self, buf: &mut impl BufMut) {
72        self.0.write(buf);
73    }
74}
75
76impl<Idx: EncodeSize> EncodeSize for NonEmptyRange<Idx> {
77    #[inline]
78    fn encode_size(&self) -> usize {
79        self.0.encode_size()
80    }
81}
82
83impl<Idx: Read + PartialOrd> Read for NonEmptyRange<Idx> {
84    type Cfg = Idx::Cfg;
85
86    #[inline]
87    fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, CodecError> {
88        let range = Range::<Idx>::read_cfg(buf, cfg)?;
89        if !range
90            .start
91            .partial_cmp(&range.end)
92            .is_some_and(|o| o.is_lt())
93        {
94            return Err(CodecError::Invalid("NonEmptyRange", "start must be < end"));
95        }
96        Ok(Self(range))
97    }
98}
99
100#[cfg(feature = "arbitrary")]
101impl<'a, Idx: arbitrary::Arbitrary<'a> + Ord> arbitrary::Arbitrary<'a> for NonEmptyRange<Idx> {
102    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
103        let a = Idx::arbitrary(u)?;
104        let b = Idx::arbitrary(u)?;
105        let (start, end) = if a < b {
106            (a, b)
107        } else if b < a {
108            (b, a)
109        } else {
110            return Err(arbitrary::Error::IncorrectFormat);
111        };
112        Ok(Self(start..end))
113    }
114}
115
116/// A macro to create a [`NonEmptyRange`] from a range expression, panicking if the range is empty.
117#[macro_export]
118macro_rules! non_empty_range {
119    ($start:expr, $end:expr) => {
120        $crate::range::NonEmptyRange::new($start..$end).expect("range must be non-empty")
121    };
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use commonware_codec::{DecodeExt, Encode};
128
129    #[test]
130    fn test_non_empty_range_valid() {
131        let r = NonEmptyRange::new(0u32..5).unwrap();
132        assert_eq!(r.start(), 0);
133        assert_eq!(r.end(), 5);
134        assert_eq!(Range::from(r), 0..5);
135    }
136
137    #[test]
138    fn test_non_empty_range_single_element() {
139        let r = NonEmptyRange::new(3u32..4).unwrap();
140        assert_eq!(r.start(), 3);
141        assert_eq!(r.end(), 4);
142    }
143
144    #[test]
145    fn test_non_empty_range_empty() {
146        assert_eq!(NonEmptyRange::new(5u32..5), Err(EmptyRange));
147        #[allow(clippy::reversed_empty_ranges)]
148        let reversed = NonEmptyRange::new(5u32..3);
149        assert_eq!(reversed, Err(EmptyRange));
150    }
151
152    #[test]
153    fn test_non_empty_range_into() {
154        let r = NonEmptyRange::new(1u32..10).unwrap();
155        let range: Range<u32> = r.into();
156        assert_eq!(range, 1..10);
157    }
158
159    #[test]
160    fn test_non_empty_range_debug() {
161        let r = NonEmptyRange::new(1u32..5).unwrap();
162        assert_eq!(format!("{r:?}"), "1..5");
163    }
164
165    #[test]
166    fn test_non_empty_range_iter() {
167        let r = NonEmptyRange::new(0u32..4).unwrap();
168        let items: Vec<_> = r.into_iter().collect();
169        assert_eq!(items, vec![0, 1, 2, 3]);
170    }
171
172    #[test]
173    fn test_non_empty_range_encode_decode() {
174        let r = NonEmptyRange::new(10u32..20).unwrap();
175        let encoded = r.encode();
176        let decoded = NonEmptyRange::<u32>::decode(encoded).unwrap();
177        assert_eq!(r, decoded);
178    }
179
180    #[test]
181    fn test_non_empty_range_decode_invalid() {
182        // Manually encode start=20, end=10 to bypass the Range write panic
183        let mut buf = Vec::new();
184        buf.extend_from_slice(&20u32.to_be_bytes());
185        buf.extend_from_slice(&10u32.to_be_bytes());
186        assert!(NonEmptyRange::<u32>::decode(bytes::Bytes::from(buf)).is_err());
187
188        // start == end is valid for Range but not for NonEmptyRange
189        let empty = Range {
190            start: 5u32,
191            end: 5u32,
192        };
193        let encoded = empty.encode();
194        assert!(NonEmptyRange::<u32>::decode(encoded).is_err());
195    }
196
197    #[cfg(feature = "arbitrary")]
198    mod conformance {
199        use super::*;
200        use commonware_codec::conformance::CodecConformance;
201
202        commonware_conformance::conformance_tests! {
203            CodecConformance<NonEmptyRange<u32>>,
204            CodecConformance<NonEmptyRange<u64>>,
205        }
206    }
207}