dcbor_pattern/
interval.rs

1/// Provides an `Interval` type representing a range of usize values with a
2/// minimum and optional maximum.
3///
4/// This module is used in the context of pattern matching for dCBOR items
5/// to represent cardinality specifications like `{n}`, `{n,m}`, or `{n,}`
6/// in pattern expressions.
7use std::ops::{Bound, RangeBounds};
8
9/// Represents an inclusive interval with a minimum value and an optional
10/// maximum value.
11///
12/// When the maximum is `None`, the interval is considered unbounded above.
13#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
14pub struct Interval {
15    min: usize,
16    max: Option<usize>, // None == unbounded
17}
18
19impl Interval {
20    /// Creates a new `Interval` from any type that implements
21    /// `RangeBounds<usize>`.
22    ///
23    /// This allows creating intervals from Rust's range expressions like
24    /// `1..5`, `0..=10`, or `2..`.
25    pub fn new(range: impl RangeBounds<usize>) -> Self {
26        let min = match range.start_bound() {
27            Bound::Included(&start) => start,
28            Bound::Excluded(&start) => start + 1,
29            Bound::Unbounded => 0,
30        };
31        let max = match range.end_bound() {
32            Bound::Included(&end) => Some(end),
33            Bound::Excluded(&end) => Some(end - 1),
34            Bound::Unbounded => None,
35        };
36        Self { min, max }
37    }
38
39    /// Returns the minimum value of the interval.
40    pub fn min(&self) -> usize { self.min }
41
42    /// Returns the maximum value of the interval, or `None` if the interval is
43    /// unbounded.
44    pub fn max(&self) -> Option<usize> { self.max }
45
46    /// Checks if the given count falls within this interval.
47    pub fn contains(&self, count: usize) -> bool {
48        count >= self.min && (self.max.is_none() || count <= self.max.unwrap())
49    }
50
51    /// Checks if the interval represents a single value (i.e., min equals max).
52    pub fn is_single(&self) -> bool { Some(self.min) == self.max }
53
54    /// Checks if the interval is unbounded (i.e., has no maximum value).
55    pub fn is_unbounded(&self) -> bool { self.max.is_none() }
56
57    /// Returns a string representation of the interval using standard range
58    /// notation.
59    ///
60    /// Examples:
61    /// - `{3}` for the single value 3
62    /// - `{1,5}` for the range 1 to 5 inclusive
63    /// - `{2,}` for 2 or more
64    pub fn range_notation(&self) -> String {
65        match (self.min, self.max) {
66            (min, Some(max)) if min == max => format!("{{{}}}", min),
67            (min, Some(max)) => format!("{{{},{}}}", min, max),
68            (min, None) => format!("{{{},}}", min),
69        }
70    }
71
72    /// Returns a string representation of the interval using shorthand notation
73    /// where applicable.
74    ///
75    /// Examples:
76    /// - `?` for `{0,1}` (optional)
77    /// - `*` for `{0,}` (zero or more)
78    /// - `+` for `{1,}` (one or more)
79    pub fn shorthand_notation(&self) -> String {
80        match (self.min, self.max) {
81            (0, Some(1)) => "?".to_string(),
82            (min, Some(max)) if min == max => format!("{{{}}}", min),
83            (min, Some(max)) => format!("{{{},{}}}", min, max),
84            (0, None) => "*".to_string(),
85            (1, None) => "+".to_string(),
86            (min, None) => format!("{{{},}}", min),
87        }
88    }
89}
90
91/// Implementation of `RangeBounds<usize>` for `Interval`, allowing it to be
92/// used in contexts that expect range bounds, such as slice indexing.
93impl RangeBounds<usize> for Interval {
94    /// Returns the start bound of the interval.
95    ///
96    /// - Returns `Bound::Unbounded` if `min` is 0
97    /// - Returns `Bound::Included(&self.min)` otherwise
98    fn start_bound(&self) -> Bound<&usize> {
99        if self.min == 0 {
100            Bound::Unbounded
101        } else {
102            Bound::Included(&self.min)
103        }
104    }
105
106    /// Returns the end bound of the interval.
107    ///
108    /// - Returns `Bound::Included(max)` if `max` is `Some`
109    /// - Returns `Bound::Unbounded` if `max` is `None`
110    fn end_bound(&self) -> Bound<&usize> {
111        match self.max {
112            Some(ref max) => Bound::Included(max),
113            None => Bound::Unbounded,
114        }
115    }
116}
117
118/// Implements the `Display` trait for `Interval`, using the `range_notation`
119/// format.
120impl std::fmt::Display for Interval {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "{}", self.range_notation())
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    #[rustfmt::skip]
132    fn test_interval_display() {
133        assert_eq!(format!("{}", Interval::new(1..=5)), "{1,5}");
134        assert_eq!(format!("{}", Interval::new(3..=3)), "{3}");
135        assert_eq!(format!("{}", Interval::new(2..)), "{2,}");
136        assert_eq!(format!("{}", Interval::new(0..)), "{0,}");
137        assert_eq!(format!("{}", Interval::new(1..)), "{1,}");
138        assert_eq!(format!("{}", Interval::new(0..=1)), "{0,1}");
139    }
140}