1use crate::ast::Regex;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Width {
14 pub min: usize,
15 pub max: Option<usize>,
16}
17
18impl Width {
19 pub fn fixed(n: usize) -> Self {
21 Self {
22 min: n,
23 max: Some(n),
24 }
25 }
26
27 pub fn unbounded(min: usize) -> Self {
29 Self { min, max: None }
30 }
31
32 pub fn is_fixed(&self) -> bool {
34 self.max == Some(self.min)
35 }
36
37 pub fn is_nullable(&self) -> bool {
39 self.min == 0
40 }
41
42 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
58pub 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}