1#![no_std]
24#![doc(html_root_url = "https://docs.rs/to-regex-range/0.1.0")]
25
26extern crate alloc;
27
28use alloc::collections::BTreeSet;
29use alloc::format;
30use alloc::string::{String, ToString};
31use alloc::vec;
32use alloc::vec::Vec;
33
34#[cfg(doctest)]
36#[doc = include_str!("../README.md")]
37struct ReadmeDoctests;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct Options {
48 pub capture: bool,
50 pub shorthand: bool,
52 pub wrap: bool,
54}
55
56impl Default for Options {
57 fn default() -> Self {
58 Self {
59 capture: false,
60 shorthand: false,
61 wrap: true,
62 }
63 }
64}
65
66impl Options {
67 #[must_use]
69 pub fn capture(mut self, value: bool) -> Self {
70 self.capture = value;
71 self
72 }
73 #[must_use]
75 pub fn shorthand(mut self, value: bool) -> Self {
76 self.shorthand = value;
77 self
78 }
79 #[must_use]
81 pub fn wrap(mut self, value: bool) -> Self {
82 self.wrap = value;
83 self
84 }
85}
86
87#[must_use]
94pub fn to_regex_range(min: i64, max: i64) -> String {
95 to_regex_range_with_options(min, max, &Options::default())
96}
97
98#[must_use]
100pub fn to_regex_range_with_options(min: i64, max: i64, options: &Options) -> String {
101 if min == max {
102 return min.to_string();
103 }
104
105 let a = min.min(max);
106 let b = min.max(max);
107
108 if a.abs_diff(b) == 1 {
109 let result = format!("{min}|{max}");
110 if options.capture {
111 return format!("({result})");
112 }
113 if !options.wrap {
114 return result;
115 }
116 return format!("(?:{result})");
117 }
118
119 let mut a = a;
120 let mut negatives: Vec<Token> = Vec::new();
121 let mut positives: Vec<Token> = Vec::new();
122
123 if a < 0 {
124 let new_min = if b < 0 { b.saturating_abs() } else { 1 };
127 negatives = split_to_patterns(new_min, a.saturating_abs(), *options);
128 a = 0;
129 }
130
131 if b >= 0 {
132 positives = split_to_patterns(a, b, *options);
133 }
134
135 let mut result = collate_patterns(&negatives, &positives);
136
137 if options.capture {
138 result = format!("({result})");
139 } else if options.wrap && (positives.len() + negatives.len()) > 1 {
140 result = format!("(?:{result})");
141 }
142
143 result
144}
145
146struct Token {
147 pattern: String,
148 count: Vec<i64>,
149 string: String,
150}
151
152fn collate_patterns(neg: &[Token], pos: &[Token]) -> String {
153 let mut subpatterns = filter_patterns(neg, pos, "-", false);
154 subpatterns.extend(filter_patterns(neg, pos, "-?", true));
155 subpatterns.extend(filter_patterns(pos, neg, "", false));
156 subpatterns.join("|")
157}
158
159fn filter_patterns(
160 arr: &[Token],
161 comparison: &[Token],
162 prefix: &str,
163 intersection: bool,
164) -> Vec<String> {
165 let mut result = Vec::new();
166 for ele in arr {
167 let present = comparison.iter().any(|e| e.string == ele.string);
168 if intersection == present {
169 result.push(format!("{prefix}{}", ele.string));
170 }
171 }
172 result
173}
174
175#[allow(clippy::cast_possible_truncation)] fn split_to_ranges(min: i64, max: i64) -> Vec<i64> {
177 let (min128, max128) = (i128::from(min), i128::from(max));
178 let mut stops: BTreeSet<i64> = BTreeSet::new();
179 stops.insert(max);
180
181 let mut nines = 1;
184 let mut stop = count_nines(min, nines);
185 while min128 <= stop && stop <= max128 {
186 stops.insert(stop as i64);
187 nines += 1;
188 stop = count_nines(min, nines);
189 }
190
191 let mut zeros = 1;
192 let mut stop = count_zeros(max.saturating_add(1), zeros) - 1;
193 while min < stop && stop <= max {
194 stops.insert(stop);
195 zeros += 1;
196 stop = count_zeros(max.saturating_add(1), zeros) - 1;
197 }
198
199 stops.into_iter().collect()
200}
201
202fn split_to_patterns(min: i64, max: i64, options: Options) -> Vec<Token> {
203 let ranges = split_to_ranges(min, max);
204 let mut tokens: Vec<Token> = Vec::new();
205 let mut start = min;
206 let mut prev: Option<usize> = None;
207
208 for &stop in &ranges {
209 let obj = range_to_pattern(start, stop, options);
210
211 if let Some(pi) = prev {
212 if tokens[pi].pattern == obj.pattern {
213 if tokens[pi].count.len() > 1 {
214 tokens[pi].count.pop();
215 }
216 tokens[pi].count.push(obj.count[0]);
217 let quant = to_quantifier(&tokens[pi].count);
218 tokens[pi].string = format!("{}{quant}", tokens[pi].pattern);
219 start = stop.saturating_add(1);
220 continue;
221 }
222 }
223
224 let quant = to_quantifier(&obj.count);
225 let string = format!("{}{quant}", obj.pattern);
226 tokens.push(Token {
227 pattern: obj.pattern,
228 count: obj.count,
229 string,
230 });
231 start = stop.saturating_add(1);
232 prev = Some(tokens.len() - 1);
233 }
234
235 tokens
236}
237
238struct RangePattern {
239 pattern: String,
240 count: Vec<i64>,
241}
242
243fn range_to_pattern(start: i64, stop: i64, options: Options) -> RangePattern {
244 if start == stop {
245 return RangePattern {
246 pattern: start.to_string(),
247 count: vec![0],
248 };
249 }
250
251 let s = start.to_string();
252 let t = stop.to_string();
253 let mut pattern = String::new();
254 let mut count = 0;
255
256 for (sd, td) in s.chars().zip(t.chars()) {
257 if sd == td {
258 pattern.push(sd);
259 } else if sd != '0' || td != '9' {
260 pattern.push_str(&to_character_class(sd, td));
261 } else {
262 count += 1;
263 }
264 }
265
266 if count > 0 {
267 pattern.push_str(if options.shorthand { "\\d" } else { "[0-9]" });
268 }
269
270 RangePattern {
271 pattern,
272 count: vec![count],
273 }
274}
275
276fn to_quantifier(digits: &[i64]) -> String {
277 let start = digits.first().copied().unwrap_or(0);
278 let stop = digits.get(1).copied();
279 let stop_truthy = stop.is_some_and(|s| s != 0);
280 if stop_truthy {
281 format!("{{{start},{}}}", stop.unwrap())
282 } else if start > 1 {
283 format!("{{{start}}}")
284 } else {
285 String::new()
286 }
287}
288
289fn to_character_class(a: char, b: char) -> String {
290 let dash = if (b as i64 - a as i64) == 1 { "" } else { "-" };
291 format!("[{a}{dash}{b}]")
292}
293
294fn count_nines(min: i64, len: usize) -> i128 {
297 let s = min.to_string();
298 let keep = s.len().saturating_sub(len);
299 let mut out = String::from(&s[..keep]);
300 for _ in 0..len {
301 out.push('9');
302 }
303 out.parse::<i128>().unwrap_or(i128::MAX)
304}
305
306#[allow(clippy::cast_possible_truncation)] fn count_zeros(integer: i64, zeros: u32) -> i64 {
309 let pow = 10i128.pow(zeros);
310 let i = i128::from(integer);
311 (i - (i % pow)) as i64
312}