#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod char;
mod tokens;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct Readme;
use std::ops::{Bound, RangeBounds};
use irange::integer::Bounded;
use tokens::identify_character;
pub use crate::char::Char;
pub use irange;
pub const UCD_VERSION: &str = tokens::UCD_VERSION;
pub use irange::{range::AnyRange, RangeSet};
pub trait CharacterClass: Sized {
fn new_from_range_u32<R: RangeBounds<u32>>(range: R) -> Option<Self>;
fn new_from_range_char<R: RangeBounds<char>>(range: R) -> Self;
fn get_cardinality(&self) -> u32;
fn to_regex(&self) -> String;
}
impl CharacterClass for RangeSet<Char> {
#[inline]
fn new_from_range_u32<R: RangeBounds<u32>>(range: R) -> Option<Self> {
let min = match range.start_bound() {
Bound::Included(&t) => Some(Char::from_u32(t)?),
Bound::Excluded(&t) => Char::from_u32(t)?.next(),
Bound::Unbounded => Some(Char::min_value()),
};
let max = match range.end_bound() {
Bound::Included(&t) => Some(Char::from_u32(t)?),
Bound::Excluded(&t) => Char::from_u32(t)?.previous(),
Bound::Unbounded => Some(Char::max_value()),
};
Some(new_from_bounds(min, max))
}
#[inline]
fn new_from_range_char<R: RangeBounds<char>>(range: R) -> Self {
let min = match range.start_bound() {
Bound::Included(&t) => Some(Char::new(t)),
Bound::Excluded(&t) => Char::new(t).next(),
Bound::Unbounded => Some(Char::min_value()),
};
let max = match range.end_bound() {
Bound::Included(&t) => Some(Char::new(t)),
Bound::Excluded(&t) => Char::new(t).previous(),
Bound::Unbounded => Some(Char::max_value()),
};
new_from_bounds(min, max)
}
#[inline]
fn get_cardinality(&self) -> u32 {
self.0
.chunks_exact(2)
.map(|bounds| {
bounds[1]
.index()
.checked_sub(bounds[0].index())
.map_or(0, |size| size + 1)
})
.fold(0, u32::saturating_add)
}
#[inline]
fn to_regex(&self) -> String {
if self.is_empty() {
String::from("[]")
} else if self.is_total() {
String::from(".")
} else if let Some(token) = tokens::identify_class(self) {
token
} else {
convert_to_regex(self)
}
}
}
#[inline]
fn new_from_bounds(min: Option<Char>, max: Option<Char>) -> RangeSet<Char> {
match (min, max) {
(Some(min), Some(max)) => RangeSet::new_from_range(min..=max),
_ => RangeSet::empty(),
}
}
fn convert_to_regex(range: &RangeSet<Char>) -> String {
let complement = range.complement();
let (range_to_use, is_complement) = if complement.0.len() < range.0.len() {
(&complement, true)
} else {
(range, false)
};
let mut sb = String::new();
let mut is_single_char = true;
for bounds in range_to_use.0.chunks_exact(2) {
let (min, max) = (bounds[0], bounds[1]);
if min == max {
is_single_char = sb.is_empty();
sb.push_str(&get_printable_char(min.to_char()));
continue;
}
is_single_char = false;
let separator = if min.next() == Some(max) { "" } else { "-" };
sb.push_str(&get_printable_char(min.to_char()));
sb.push_str(separator);
sb.push_str(&get_printable_char(max.to_char()));
}
if sb.is_empty() {
String::from("[]")
} else if is_complement {
format!("[^{sb}]")
} else if is_single_char {
sb
} else {
format!("[{sb}]")
}
}
fn get_printable_char(character: char) -> String {
if ('\u{20}'..='\u{7E}').contains(&character) {
if matches!(
character,
'*' | '+'
| '?'
| '('
| ')'
| '['
| ']'
| '{'
| '}'
| '|'
| '\\'
| '-'
| '^'
| '.'
| '$'
) {
format!("\\{character}")
} else {
String::from(character)
}
} else if let Some(c) = identify_character(character) {
c.to_owned()
} else {
format!("\\u{{{:04x}}}", character as u32)
}
}
#[cfg(test)]
mod tests {
use irange::range::AnyRange;
use super::*;
#[test]
fn test_empty_and_total() {
let range = RangeSet::<Char>::empty();
assert!(range.is_empty());
assert_eq!("[]", range.to_regex());
assert_eq!(0, range.get_cardinality());
let range = RangeSet::<Char>::total();
assert!(range.is_total());
assert_eq!(".", range.to_regex());
assert_eq!(Char::COUNT, range.get_cardinality());
assert_eq!(1_112_064, range.get_cardinality());
}
#[test]
fn test_operations() {
let range1 = RangeSet::new_from_range_char('a'..='z');
assert_eq!("[a-z]", range1.to_regex());
for char in range1.iter() {
assert!(range1.contains(char))
}
let range2 = RangeSet::<Char>::new_from_ranges(&[
AnyRange::from(Char::new('0')..Char::new('2')),
AnyRange::from(Char::new('A')..=Char::new('F')),
AnyRange::from(Char::new('a')..=Char::new('f')),
]);
assert_eq!("[01A-Fa-f]", range2.to_regex());
for char in range2.iter() {
assert!(range2.contains(char))
}
let intersection = range1.intersection(&range2);
assert_eq!("[a-f]", intersection.to_regex());
for char in intersection.iter() {
assert!(intersection.contains(char))
}
}
#[test]
fn test_to_regex() {
let range = RangeSet::<Char>::new_from_range_char('.'..='.');
assert_eq!("\\.", range.to_regex());
let range = RangeSet::<Char>::new_from_ranges(&[
AnyRange::from(Char::new('0')..=Char::new('9')),
AnyRange::from(Char::new('A')..=Char::new('F')),
AnyRange::from(Char::new('a')..=Char::new('f')),
]);
assert_eq!("\\p{ASCII_Hex_Digit}", range.to_regex());
}
#[test]
fn metacharacters_are_escaped() {
for character in [
'*', '+', '?', '(', ')', '[', ']', '{', '}', '|', '\\', '-', '^', '.', '$',
] {
let range = RangeSet::new_from_range_char(character..=character);
assert_eq!(format!("\\{character}"), range.to_regex());
}
}
#[test]
fn the_whole_printable_ascii_range_is_written_as_itself() {
assert_eq!("~", RangeSet::new_from_range_char('~'..='~').to_regex());
assert_eq!("[ -~]", RangeSet::new_from_range_char(' '..='~').to_regex());
}
#[test]
fn no_method_panics_on_a_malformed_set() {
let malformed = [
vec![Char::new('a')],
vec![Char::new('z'), Char::new('a')],
vec![
Char::new('x'),
Char::new('z'),
Char::new('a'),
Char::new('c'),
],
vec![
Char::new('a'),
Char::new('b'),
Char::new('c'),
Char::new('d'),
],
];
for bounds in malformed {
let range = RangeSet(bounds);
let _ = range.get_cardinality();
let _ = range.to_regex();
}
}
#[test]
#[cfg(feature = "serde")]
fn test_serde() {
let range = RangeSet::empty();
let serialized = serde_json::to_string(&range).unwrap();
let unserialized: RangeSet<Char> = serde_json::from_str(&serialized).unwrap();
assert_eq!(range, unserialized);
let range = RangeSet::<Char>::total();
let serialized = serde_json::to_string(&range).unwrap();
let unserialized: RangeSet<Char> = serde_json::from_str(&serialized).unwrap();
assert_eq!(range, unserialized);
let range = RangeSet::new_from_ranges(&[
AnyRange::from(Char::new('3')..=Char::new('4')),
AnyRange::from(Char::new('7')..Char::new('9')),
]);
let serialized = serde_json::to_string(&range).unwrap();
let unserialized: RangeSet<Char> = serde_json::from_str(&serialized).unwrap();
assert_eq!(range, unserialized);
}
}