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}