#![no_std]
#![doc(html_root_url = "https://docs.rs/to-regex-range/0.1.0")]
extern crate alloc;
use alloc::collections::BTreeSet;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Options {
pub capture: bool,
pub shorthand: bool,
pub wrap: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
capture: false,
shorthand: false,
wrap: true,
}
}
}
impl Options {
#[must_use]
pub fn capture(mut self, value: bool) -> Self {
self.capture = value;
self
}
#[must_use]
pub fn shorthand(mut self, value: bool) -> Self {
self.shorthand = value;
self
}
#[must_use]
pub fn wrap(mut self, value: bool) -> Self {
self.wrap = value;
self
}
}
#[must_use]
pub fn to_regex_range(min: i64, max: i64) -> String {
to_regex_range_with_options(min, max, &Options::default())
}
#[must_use]
pub fn to_regex_range_with_options(min: i64, max: i64, options: &Options) -> String {
if min == max {
return min.to_string();
}
let a = min.min(max);
let b = min.max(max);
if a.abs_diff(b) == 1 {
let result = format!("{min}|{max}");
if options.capture {
return format!("({result})");
}
if !options.wrap {
return result;
}
return format!("(?:{result})");
}
let mut a = a;
let mut negatives: Vec<Token> = Vec::new();
let mut positives: Vec<Token> = Vec::new();
if a < 0 {
let new_min = if b < 0 { b.saturating_abs() } else { 1 };
negatives = split_to_patterns(new_min, a.saturating_abs(), *options);
a = 0;
}
if b >= 0 {
positives = split_to_patterns(a, b, *options);
}
let mut result = collate_patterns(&negatives, &positives);
if options.capture {
result = format!("({result})");
} else if options.wrap && (positives.len() + negatives.len()) > 1 {
result = format!("(?:{result})");
}
result
}
struct Token {
pattern: String,
count: Vec<i64>,
string: String,
}
fn collate_patterns(neg: &[Token], pos: &[Token]) -> String {
let mut subpatterns = filter_patterns(neg, pos, "-", false);
subpatterns.extend(filter_patterns(neg, pos, "-?", true));
subpatterns.extend(filter_patterns(pos, neg, "", false));
subpatterns.join("|")
}
fn filter_patterns(
arr: &[Token],
comparison: &[Token],
prefix: &str,
intersection: bool,
) -> Vec<String> {
let mut result = Vec::new();
for ele in arr {
let present = comparison.iter().any(|e| e.string == ele.string);
if intersection == present {
result.push(format!("{prefix}{}", ele.string));
}
}
result
}
#[allow(clippy::cast_possible_truncation)] fn split_to_ranges(min: i64, max: i64) -> Vec<i64> {
let (min128, max128) = (i128::from(min), i128::from(max));
let mut stops: BTreeSet<i64> = BTreeSet::new();
stops.insert(max);
let mut nines = 1;
let mut stop = count_nines(min, nines);
while min128 <= stop && stop <= max128 {
stops.insert(stop as i64);
nines += 1;
stop = count_nines(min, nines);
}
let mut zeros = 1;
let mut stop = count_zeros(max.saturating_add(1), zeros) - 1;
while min < stop && stop <= max {
stops.insert(stop);
zeros += 1;
stop = count_zeros(max.saturating_add(1), zeros) - 1;
}
stops.into_iter().collect()
}
fn split_to_patterns(min: i64, max: i64, options: Options) -> Vec<Token> {
let ranges = split_to_ranges(min, max);
let mut tokens: Vec<Token> = Vec::new();
let mut start = min;
let mut prev: Option<usize> = None;
for &stop in &ranges {
let obj = range_to_pattern(start, stop, options);
if let Some(pi) = prev {
if tokens[pi].pattern == obj.pattern {
if tokens[pi].count.len() > 1 {
tokens[pi].count.pop();
}
tokens[pi].count.push(obj.count[0]);
let quant = to_quantifier(&tokens[pi].count);
tokens[pi].string = format!("{}{quant}", tokens[pi].pattern);
start = stop.saturating_add(1);
continue;
}
}
let quant = to_quantifier(&obj.count);
let string = format!("{}{quant}", obj.pattern);
tokens.push(Token {
pattern: obj.pattern,
count: obj.count,
string,
});
start = stop.saturating_add(1);
prev = Some(tokens.len() - 1);
}
tokens
}
struct RangePattern {
pattern: String,
count: Vec<i64>,
}
fn range_to_pattern(start: i64, stop: i64, options: Options) -> RangePattern {
if start == stop {
return RangePattern {
pattern: start.to_string(),
count: vec![0],
};
}
let s = start.to_string();
let t = stop.to_string();
let mut pattern = String::new();
let mut count = 0;
for (sd, td) in s.chars().zip(t.chars()) {
if sd == td {
pattern.push(sd);
} else if sd != '0' || td != '9' {
pattern.push_str(&to_character_class(sd, td));
} else {
count += 1;
}
}
if count > 0 {
pattern.push_str(if options.shorthand { "\\d" } else { "[0-9]" });
}
RangePattern {
pattern,
count: vec![count],
}
}
fn to_quantifier(digits: &[i64]) -> String {
let start = digits.first().copied().unwrap_or(0);
let stop = digits.get(1).copied();
let stop_truthy = stop.is_some_and(|s| s != 0);
if stop_truthy {
format!("{{{start},{}}}", stop.unwrap())
} else if start > 1 {
format!("{{{start}}}")
} else {
String::new()
}
}
fn to_character_class(a: char, b: char) -> String {
let dash = if (b as i64 - a as i64) == 1 { "" } else { "-" };
format!("[{a}{dash}{b}]")
}
fn count_nines(min: i64, len: usize) -> i128 {
let s = min.to_string();
let keep = s.len().saturating_sub(len);
let mut out = String::from(&s[..keep]);
for _ in 0..len {
out.push('9');
}
out.parse::<i128>().unwrap_or(i128::MAX)
}
#[allow(clippy::cast_possible_truncation)] fn count_zeros(integer: i64, zeros: u32) -> i64 {
let pow = 10i128.pow(zeros);
let i = i128::from(integer);
(i - (i % pow)) as i64
}