metrale_grammar/schema/
range_regex.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// Integer range -> regex generation — port of
4// `JSONSchemaConverter::{MakePatternForDigitRange,GenerateNumberPatterns,
5// GenerateSubRangeRegex,GenerateRangeRegex}` from
6// `cpp/json_schema_converter.cc`.
7//
8// Produces a regex (anchored with `^...$`) matching all integers in a
9// closed `[start, end]` range, where `None` means unbounded.
10
11/// A digit-range pattern for one fixed-length slot: either a literal
12/// digit or a `[a-b]` class, optionally followed by `\d{n}`.
13fn make_pattern_for_digit_range(start: char, end: char, remaining: i32) -> String {
14    let mut out = String::new();
15    if start == end {
16        out.push(start);
17    } else {
18        out.push('[');
19        out.push(start);
20        out.push('-');
21        out.push(end);
22        out.push(']');
23    }
24    if remaining > 0 {
25        out.push_str(&format!("\\d{{{remaining}}}"));
26    }
27    out
28}
29
30/// Generate the alternation of fixed-length digit patterns covering
31/// the positive range `[lower, upper]`. Port of `GenerateNumberPatterns`.
32fn generate_number_patterns(lower: i64, upper: i64) -> Vec<String> {
33    let mut patterns = Vec::new();
34    let lower_len = lower.to_string().len() as i32;
35    let upper_len = upper.to_string().len() as i32;
36
37    for len in lower_len..=upper_len {
38        let digit_min = 10i64.pow((len - 1) as u32);
39        let digit_max = 10i64.pow(len as u32) - 1;
40        let start = if len == lower_len { lower } else { digit_min };
41        let end = if len == upper_len { upper } else { digit_max };
42        let start_str = start.to_string();
43        let end_str = end.to_string();
44        let sb: Vec<char> = start_str.chars().collect();
45        let eb: Vec<char> = end_str.chars().collect();
46
47        if len == 1 {
48            patterns.push(make_pattern_for_digit_range(sb[0], eb[0], 0));
49            continue;
50        }
51
52        let mut prefix = 0usize;
53        while prefix < len as usize && sb[prefix] == eb[prefix] {
54            prefix += 1;
55        }
56        if prefix == len as usize {
57            patterns.push(start_str.clone());
58            continue;
59        }
60        if prefix > 0 && prefix >= (len as usize).saturating_sub(2) {
61            let common: String = sb[..prefix].iter().collect();
62            patterns.push(format!(
63                "{common}{}",
64                make_pattern_for_digit_range(sb[prefix], eb[prefix], len - prefix as i32 - 1)
65            ));
66            continue;
67        }
68
69        if len == lower_len && len == upper_len {
70            push_full_len_patterns(
71                &mut patterns,
72                start,
73                end,
74                digit_min,
75                digit_max,
76                &sb,
77                &eb,
78                len,
79            );
80        } else if len == lower_len && len != upper_len {
81            if start == digit_min {
82                patterns.push(format!("[1-9]\\d{{{}}}", len - 1));
83            } else {
84                push_start_open(&mut patterns, &sb, len);
85                patterns.push(start_str.clone());
86            }
87        } else if len != lower_len && len == upper_len {
88            if end == digit_max {
89                patterns.push(format!("[1-9]\\d{{{}}}", len - 1));
90            } else {
91                push_end_open(&mut patterns, &eb, len);
92                patterns.push(end_str.clone());
93            }
94        } else {
95            patterns.push(format!("[1-9]\\d{{{}}}", len - 1));
96        }
97    }
98    patterns
99}
100
101/// Append the patterns generated when the length covers both the
102/// lower and upper bound exactly.
103#[allow(clippy::too_many_arguments)]
104fn push_full_len_patterns(
105    patterns: &mut Vec<String>,
106    start: i64,
107    end: i64,
108    digit_min: i64,
109    digit_max: i64,
110    sb: &[char],
111    eb: &[char],
112    len: i32,
113) {
114    let start_str: String = sb.iter().collect();
115    let end_str: String = eb.iter().collect();
116    if start == digit_max {
117        patterns.push(start_str);
118    } else if start == digit_min {
119        if end == digit_max {
120            patterns.push(format!("[1-9]\\d{{{}}}", len - 1));
121        } else {
122            for (i, &ec) in eb.iter().enumerate() {
123                if i == 0 {
124                    if ec > '1' {
125                        patterns.push(make_pattern_for_digit_range('1', prev_char(ec), len - 1));
126                    }
127                } else {
128                    let pref: String = eb[..i].iter().collect();
129                    if ec > '0' {
130                        patterns.push(format!(
131                            "{pref}{}",
132                            make_pattern_for_digit_range('0', prev_char(ec), len - i as i32 - 1)
133                        ));
134                    }
135                }
136            }
137            patterns.push(end_str);
138        }
139    } else if end == digit_max {
140        for (i, &sc) in sb.iter().enumerate() {
141            if i == 0 {
142                if sc < '9' {
143                    patterns.push(make_pattern_for_digit_range(next_char(sc), '9', len - 1));
144                }
145            } else {
146                let pref: String = sb[..i].iter().collect();
147                if sc < '9' {
148                    patterns.push(format!(
149                        "{pref}{}",
150                        make_pattern_for_digit_range(next_char(sc), '9', len - i as i32 - 1)
151                    ));
152                }
153            }
154        }
155        patterns.push(start_str);
156    } else {
157        push_full_distinct_first(patterns, sb, eb, len);
158    }
159}
160
161/// The hardest sub-case: first digits differ and both bounds are
162/// strictly inside the length band.
163fn push_full_distinct_first(patterns: &mut Vec<String>, sb: &[char], eb: &[char], len: i32) {
164    let start_str: String = sb.iter().collect();
165    let end_str: String = eb.iter().collect();
166    let sf = sb[0];
167    let ef = eb[0];
168    if (ef as i32) - (sf as i32) > 1 {
169        patterns.push(make_pattern_for_digit_range(
170            next_char(sf),
171            prev_char(ef),
172            len - 1,
173        ));
174    }
175    for (i, &sc) in sb.iter().enumerate() {
176        if i == 0 {
177            let pref: String = sb[..1].iter().collect();
178            if sb[1] < '9' {
179                patterns.push(format!(
180                    "{pref}{}",
181                    make_pattern_for_digit_range(next_char(sb[1]), '9', len - 2)
182                ));
183            }
184        } else {
185            let pref: String = sb[..i].iter().collect();
186            if sc < '9' {
187                patterns.push(format!(
188                    "{pref}{}",
189                    make_pattern_for_digit_range(next_char(sc), '9', len - i as i32 - 1)
190                ));
191            }
192        }
193    }
194    patterns.push(start_str);
195    for (i, &ec) in eb.iter().enumerate() {
196        if i == 0 {
197            let pref: String = eb[..1].iter().collect();
198            if eb[1] > '0' {
199                patterns.push(format!(
200                    "{pref}{}",
201                    make_pattern_for_digit_range('0', prev_char(eb[1]), len - 2)
202                ));
203            }
204        } else {
205            let pref: String = eb[..i].iter().collect();
206            if ec > '0' {
207                patterns.push(format!(
208                    "{pref}{}",
209                    make_pattern_for_digit_range('0', prev_char(ec), len - i as i32 - 1)
210                ));
211            }
212        }
213    }
214    patterns.push(end_str);
215}
216
217/// Open-ended-above patterns starting from `sb` of length `len`.
218fn push_start_open(patterns: &mut Vec<String>, sb: &[char], len: i32) {
219    for (i, &sc) in sb.iter().enumerate() {
220        if i == 0 {
221            if sc < '9' {
222                patterns.push(make_pattern_for_digit_range(next_char(sc), '9', len - 1));
223            }
224        } else {
225            let pref: String = sb[..i].iter().collect();
226            if sc < '9' {
227                patterns.push(format!(
228                    "{pref}{}",
229                    make_pattern_for_digit_range(next_char(sc), '9', len - i as i32 - 1)
230                ));
231            }
232        }
233    }
234}
235
236/// Open-ended-below patterns ending at `eb` of length `len`.
237fn push_end_open(patterns: &mut Vec<String>, eb: &[char], len: i32) {
238    for (i, &ec) in eb.iter().enumerate() {
239        if i == 0 {
240            if ec > '1' {
241                patterns.push(make_pattern_for_digit_range('1', prev_char(ec), len - 1));
242            }
243        } else {
244            let pref: String = eb[..i].iter().collect();
245            if ec > '0' {
246                patterns.push(format!(
247                    "{pref}{}",
248                    make_pattern_for_digit_range('0', prev_char(ec), len - i as i32 - 1)
249                ));
250            }
251        }
252    }
253}
254
255fn next_char(c: char) -> char {
256    char::from(c as u8 + 1)
257}
258fn prev_char(c: char) -> char {
259    char::from(c as u8 - 1)
260}
261
262/// Generate the parenthesized alternation for the positive range
263/// `[lower, upper]`. Port of `GenerateSubRangeRegex`.
264fn generate_sub_range_regex(lower: i64, upper: i64) -> String {
265    let patterns = generate_number_patterns(lower, upper);
266    format!("({})", patterns.join("|"))
267}
268
269/// Generate a regex matching integers in `[start, end]` (inclusive),
270/// each bound `None` meaning unbounded. Port of `GenerateRangeRegex`.
271pub fn generate_range_regex(start: Option<i64>, end: Option<i64>) -> String {
272    let mut parts: Vec<String> = Vec::new();
273
274    match (start, end) {
275        (None, None) => return "^-?\\d+$".to_string(),
276        (Some(s), None) => {
277            if s <= 0 {
278                if s < 0 {
279                    parts.push(format!("-{}", generate_sub_range_regex(s.abs(), 1)));
280                }
281                parts.push("0".to_string());
282                parts.push("[1-9]\\d*".to_string());
283            } else {
284                let start_str = s.to_string();
285                let sb: Vec<char> = start_str.chars().collect();
286                let len = sb.len() as i32;
287                if len == 1 {
288                    parts.push(make_pattern_for_digit_range(sb[0], '9', 0));
289                    parts.push("[1-9]\\d*".to_string());
290                } else {
291                    parts.push(start_str.clone());
292                    push_start_open(&mut parts, &sb, len);
293                    parts.push(format!("[1-9]\\d{{{len},}}"));
294                }
295            }
296        }
297        (None, Some(e)) => {
298            if e >= 0 {
299                parts.push("-[1-9]\\d*".to_string());
300                parts.push("0".to_string());
301                if e > 0 {
302                    parts.push(generate_sub_range_regex(1, e));
303                }
304            } else {
305                let end_str = (-e).to_string();
306                let eb: Vec<char> = end_str.chars().collect();
307                let len = eb.len() as i32;
308                if len == 1 {
309                    parts.push(format!("-{}", make_pattern_for_digit_range(eb[0], '9', 0)));
310                    parts.push("-[1-9]\\d*".to_string());
311                } else {
312                    parts.push(e.to_string());
313                    for (i, &ec) in eb.iter().enumerate() {
314                        if i == 0 {
315                            if ec > '1' {
316                                parts.push(format!(
317                                    "-{}",
318                                    make_pattern_for_digit_range('1', prev_char(ec), len - 1)
319                                ));
320                            }
321                        } else {
322                            let pref: String = eb[..i].iter().collect();
323                            if ec > '0' {
324                                parts.push(format!(
325                                    "-{pref}{}",
326                                    make_pattern_for_digit_range(
327                                        '0',
328                                        prev_char(ec),
329                                        len - i as i32 - 1
330                                    )
331                                ));
332                            }
333                        }
334                    }
335                    parts.push(format!("-[1-9]\\d{{{len},}}"));
336                }
337            }
338        }
339        (Some(s), Some(e)) => {
340            if s > e {
341                return "^()$".to_string();
342            }
343            if s < 0 {
344                let neg_end = (-1i64).min(e);
345                parts.push(format!("-{}", generate_sub_range_regex(-neg_end, -s)));
346            }
347            if s <= 0 && e >= 0 {
348                parts.push("0".to_string());
349            }
350            if e > 0 {
351                let pos_start = 1i64.max(s);
352                parts.push(generate_sub_range_regex(pos_start, e));
353            }
354        }
355    }
356
357    format!("^({})$", parts.join("|"))
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn matches_cpp_examples() {
366        assert_eq!(generate_range_regex(Some(12), Some(16)), r"^((1[2-6]))$");
367        assert_eq!(generate_range_regex(Some(1), Some(10)), r"^(([1-9]|10))$");
368        assert_eq!(
369            generate_range_regex(Some(-5), Some(10)),
370            r"^(-([1-5])|0|([1-9]|10))$"
371        );
372        assert_eq!(generate_range_regex(Some(-15), Some(-10)), r"^(-(1[0-5]))$");
373        assert_eq!(generate_range_regex(None, None), r"^-?\d+$");
374        assert_eq!(generate_range_regex(Some(5), None), r"^([5-9]|[1-9]\d*)$");
375        assert_eq!(generate_range_regex(None, Some(0)), r"^(-[1-9]\d*|0)$");
376        assert_eq!(generate_range_regex(Some(5), Some(5)), r"^((5))$");
377        assert_eq!(
378            generate_range_regex(Some(-10), Some(0)),
379            r"^(-([1-9]|10)|0)$"
380        );
381        assert_eq!(generate_range_regex(Some(0), Some(10)), r"^(0|([1-9]|10))$");
382    }
383
384    #[test]
385    fn matches_multi_length_examples() {
386        assert_eq!(
387            generate_range_regex(Some(1), Some(9999)),
388            r"^(([1-9]|[1-9]\d{1}|[1-9]\d{2}|[1-9]\d{3}))$"
389        );
390        assert_eq!(
391            generate_range_regex(Some(s_int()), None),
392            r"^([5-9]|[1-9]\d*)$"
393        );
394    }
395
396    fn s_int() -> i64 {
397        5
398    }
399}