Skip to main content

re_parser/
width.rs

1//! The [`Width`] type and the [`node_width`] convenience constructor.
2//!
3//! The actual computation lives on [`crate::ast::Regex`] via
4//! [`Regex::min_width`] and [`Regex::max_width`].
5
6use crate::ast::Regex;
7
8/// The range of character widths a pattern can match.
9///
10/// - `min` — fewest characters consumed; always finite.
11/// - `max` — most characters consumed; `None` means unbounded (`*`, `+`, `{n,}`).
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Width {
14    pub min: usize,
15    pub max: Option<usize>,
16}
17
18impl Width {
19    /// Fixed width: `min == max`.
20    pub fn fixed(n: usize) -> Self {
21        Self {
22            min: n,
23            max: Some(n),
24        }
25    }
26
27    /// Unbounded: at least `min` characters, no upper limit.
28    pub fn unbounded(min: usize) -> Self {
29        Self { min, max: None }
30    }
31
32    /// `true` when the pattern always matches the same number of characters.
33    pub fn is_fixed(&self) -> bool {
34        self.max == Some(self.min)
35    }
36
37    /// `true` when the pattern can match the empty string.
38    pub fn is_nullable(&self) -> bool {
39        self.min == 0
40    }
41
42    /// `true` when there is no upper bound on the match length.
43    pub fn is_unbounded(&self) -> bool {
44        self.max.is_none()
45    }
46}
47
48impl std::fmt::Display for Width {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self.max {
51            Some(max) if max == self.min => write!(f, "exactly {}", self.min),
52            Some(max) => write!(f, "{}..={}", self.min, max),
53            None => write!(f, "{}..", self.min),
54        }
55    }
56}
57
58/// Build a [`Width`] from a parsed [`Regex`] node.
59///
60/// This is a thin wrapper — the computation is delegated to
61/// [`Regex::min_width`] and [`Regex::max_width`].
62pub fn node_width(node: &Regex) -> Width {
63    Width {
64        min: node.min_width(),
65        max: node.max_width(),
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::parse;
73
74    fn ast(pattern: &str) -> Regex {
75        parse(pattern).unwrap()
76    }
77
78    // ── single atoms ──────────────────────────────────────────────────────────
79
80    #[test]
81    fn test_literal() {
82        assert_eq!(ast("a").min_width(), 1);
83        assert_eq!(ast("a").max_width(), Some(1));
84    }
85
86    #[test]
87    fn test_any_char() {
88        assert_eq!(ast(".").min_width(), 1);
89        assert_eq!(ast(".").max_width(), Some(1));
90    }
91
92    #[test]
93    fn test_anchor_zero_width() {
94        for p in ["^", "$", r"\b"] {
95            assert_eq!(ast(p).min_width(), 0, "min_width of {p}");
96            assert_eq!(ast(p).max_width(), Some(0), "max_width of {p}");
97        }
98    }
99
100    #[test]
101    fn test_escape_class() {
102        assert_eq!(ast(r"\d").min_width(), 1);
103        assert_eq!(ast(r"\w").max_width(), Some(1));
104    }
105
106    #[test]
107    fn test_char_class() {
108        assert_eq!(ast("[abc]").min_width(), 1);
109        assert_eq!(ast("[^0-9]").max_width(), Some(1));
110    }
111
112    // ── quantifiers ───────────────────────────────────────────────────────────
113
114    #[test]
115    fn test_star() {
116        assert_eq!(ast("a*").min_width(), 0);
117        assert_eq!(ast("a*").max_width(), None);
118    }
119
120    #[test]
121    fn test_plus() {
122        assert_eq!(ast("a+").min_width(), 1);
123        assert_eq!(ast("a+").max_width(), None);
124    }
125
126    #[test]
127    fn test_question() {
128        assert_eq!(ast("a?").min_width(), 0);
129        assert_eq!(ast("a?").max_width(), Some(1));
130    }
131
132    #[test]
133    fn test_exact() {
134        assert_eq!(ast("a{4}").min_width(), 4);
135        assert_eq!(ast("a{4}").max_width(), Some(4));
136    }
137
138    #[test]
139    fn test_at_least() {
140        assert_eq!(ast("a{3,}").min_width(), 3);
141        assert_eq!(ast("a{3,}").max_width(), None);
142    }
143
144    #[test]
145    fn test_between() {
146        assert_eq!(ast("a{2,5}").min_width(), 2);
147        assert_eq!(ast("a{2,5}").max_width(), Some(5));
148    }
149
150    #[test]
151    fn test_lazy_same_width_as_greedy() {
152        assert_eq!(ast("a*?").min_width(), ast("a*").min_width());
153        assert_eq!(ast("a*?").max_width(), ast("a*").max_width());
154        assert_eq!(ast("a+?").min_width(), ast("a+").min_width());
155        assert_eq!(ast("a{1,3}?").max_width(), ast("a{1,3}").max_width());
156    }
157
158    // ── concat ────────────────────────────────────────────────────────────────
159
160    #[test]
161    fn test_concat_fixed() {
162        assert_eq!(ast("abc").min_width(), 3);
163        assert_eq!(ast("abc").max_width(), Some(3));
164    }
165
166    #[test]
167    fn test_concat_with_quantifier() {
168        assert_eq!(ast("ab+").min_width(), 2);
169        assert_eq!(ast("ab+").max_width(), None);
170    }
171
172    #[test]
173    fn test_concat_optional_suffix() {
174        assert_eq!(ast("ab?").min_width(), 1);
175        assert_eq!(ast("ab?").max_width(), Some(2));
176    }
177
178    #[test]
179    fn test_anchored_pattern() {
180        assert_eq!(ast("^abc$").min_width(), 3);
181        assert_eq!(ast("^abc$").max_width(), Some(3));
182    }
183
184    // ── alternation ───────────────────────────────────────────────────────────
185
186    #[test]
187    fn test_alternation_same_width() {
188        assert_eq!(ast("cat|dog").min_width(), 3);
189        assert_eq!(ast("cat|dog").max_width(), Some(3));
190    }
191
192    #[test]
193    fn test_alternation_different_width() {
194        assert_eq!(ast("a|bb|ccc").min_width(), 1);
195        assert_eq!(ast("a|bb|ccc").max_width(), Some(3));
196    }
197
198    #[test]
199    fn test_alternation_unbounded_branch() {
200        assert_eq!(ast("a|b+").min_width(), 1);
201        assert_eq!(ast("a|b+").max_width(), None);
202    }
203
204    // ── groups ────────────────────────────────────────────────────────────────
205
206    #[test]
207    fn test_capturing_group() {
208        assert_eq!(ast("(abc)").min_width(), 3);
209        assert_eq!(ast("(abc)").max_width(), Some(3));
210    }
211
212    #[test]
213    fn test_non_capturing_group() {
214        assert_eq!(ast("(?:ab)+").min_width(), 2);
215        assert_eq!(ast("(?:ab)+").max_width(), None);
216    }
217
218    #[test]
219    fn test_lookahead_zero_width() {
220        assert_eq!(ast("foo(?=bar)").min_width(), 3);
221        assert_eq!(ast("foo(?=bar)").max_width(), Some(3));
222    }
223
224    #[test]
225    fn test_lookbehind_zero_width() {
226        assert_eq!(ast(r"(?<=\d)px").min_width(), 2);
227        assert_eq!(ast(r"(?<=\d)px").max_width(), Some(2));
228    }
229
230    // ── real-world ────────────────────────────────────────────────────────────
231
232    #[test]
233    fn test_ipv4_octet() {
234        assert_eq!(ast(r"\d{1,3}").min_width(), 1);
235        assert_eq!(ast(r"\d{1,3}").max_width(), Some(3));
236    }
237
238    #[test]
239    fn test_iso_date() {
240        assert_eq!(ast(r"\d{4}-\d{2}-\d{2}").min_width(), 10);
241        assert_eq!(ast(r"\d{4}-\d{2}-\d{2}").max_width(), Some(10));
242    }
243
244    #[test]
245    fn test_hex_colour() {
246        assert_eq!(ast(r"#[0-9a-fA-F]{6}").min_width(), 7);
247        assert_eq!(ast(r"#[0-9a-fA-F]{6}").max_width(), Some(7));
248    }
249
250    // ── node_width wrapper ────────────────────────────────────────────────────
251
252    #[test]
253    fn test_node_width_wrapper() {
254        let w = node_width(&ast(r"\d{2,4}"));
255        assert_eq!(w.min, 2);
256        assert_eq!(w.max, Some(4));
257    }
258
259    // ── Width helpers ─────────────────────────────────────────────────────────
260
261    #[test]
262    fn test_is_fixed() {
263        assert!(Width::fixed(5).is_fixed());
264        assert!(!Width::unbounded(1).is_fixed());
265        assert!(!Width { min: 1, max: Some(3) }.is_fixed());
266    }
267
268    #[test]
269    fn test_is_nullable() {
270        assert!(Width::fixed(0).is_nullable());
271        assert!(Width { min: 0, max: Some(3) }.is_nullable());
272        assert!(!Width::fixed(1).is_nullable());
273    }
274
275    #[test]
276    fn test_display() {
277        assert_eq!(Width::fixed(3).to_string(), "exactly 3");
278        assert_eq!(Width { min: 1, max: Some(5) }.to_string(), "1..=5");
279        assert_eq!(Width::unbounded(2).to_string(), "2..");
280    }
281}