use std::sync::Arc;
#[derive(Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum PatternElem {
Char(char),
Wildcard,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
pub struct Pattern {
elems: Arc<Vec<PatternElem>>,
}
impl Pattern {
fn new(elems: Arc<Vec<PatternElem>>) -> Self {
Self { elems }
}
pub fn get_elems(&self) -> &[PatternElem] {
&self.elems
}
pub fn iter(&self) -> impl Iterator<Item = &PatternElem> {
self.elems.iter()
}
pub fn len(&self) -> usize {
self.elems.len()
}
pub fn is_empty(&self) -> bool {
self.elems.is_empty()
}
}
impl From<Arc<Vec<PatternElem>>> for Pattern {
fn from(value: Arc<Vec<PatternElem>>) -> Self {
Self::new(value)
}
}
impl From<Vec<PatternElem>> for Pattern {
fn from(value: Vec<PatternElem>) -> Self {
Self::new(Arc::new(value))
}
}
impl FromIterator<PatternElem> for Pattern {
fn from_iter<T: IntoIterator<Item = PatternElem>>(iter: T) -> Self {
Self::new(Arc::new(iter.into_iter().collect()))
}
}
impl std::fmt::Display for Pattern {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for pc in self.elems.as_ref() {
match pc {
PatternElem::Char('*') => write!(f, r#"\*"#)?,
PatternElem::Char(c) => write!(f, "{}", c.escape_debug())?,
PatternElem::Wildcard => write!(f, r#"*"#)?,
}
}
Ok(())
}
}
impl PatternElem {
fn match_char(self, text_char: char) -> bool {
match self {
PatternElem::Char(c) => text_char == c,
PatternElem::Wildcard => true,
}
}
fn is_wildcard(self) -> bool {
matches!(self, PatternElem::Wildcard)
}
}
impl Pattern {
pub fn wildcard_match(&self, text: &str) -> bool {
let pattern = self.get_elems();
if pattern.is_empty() {
return text.is_empty();
}
let text: Vec<char> = text.chars().collect();
let mut i: usize = 0; let mut j: usize = 0; let mut star_idx: usize = 0; let mut tmp_idx: usize = 0; let mut contains_star: bool = false;
let text_len = text.len();
let pattern_len = pattern.len();
while i < text_len && (!contains_star || star_idx != pattern_len - 1) {
#[expect(
clippy::indexing_slicing,
reason = "`j` is checked to be less than length"
)]
if j < pattern_len && pattern[j].is_wildcard() {
contains_star = true;
star_idx = j;
tmp_idx = i;
j += 1;
} else if j < pattern_len && pattern[j].match_char(text[i]) {
i += 1;
j += 1;
} else if contains_star {
j = star_idx + 1;
i = tmp_idx + 1;
tmp_idx = i;
} else {
return false;
}
}
#[expect(
clippy::indexing_slicing,
reason = "`j` is checked to be less than length"
)]
while j < pattern_len && pattern[j].is_wildcard() {
j += 1;
}
j == pattern_len
}
}
#[cfg(test)]
mod test {
use super::*;
impl std::ops::Add for Pattern {
type Output = Pattern;
fn add(self, rhs: Self) -> Self::Output {
let elems = [self.get_elems(), rhs.get_elems()].concat();
Pattern::from(elems)
}
}
fn string_map(text: &str) -> Pattern {
text.chars().map(PatternElem::Char).collect()
}
fn star() -> Pattern {
Pattern::from(vec![PatternElem::Wildcard])
}
fn empty() -> Pattern {
Pattern::from(vec![])
}
#[test]
fn test_wildcard_match_basic() {
assert!((string_map("foo") + star()).wildcard_match("foo bar"));
assert!((star() + string_map("bar")).wildcard_match("foo bar"));
assert!((star() + string_map("o b") + star()).wildcard_match("foo bar"));
assert!((string_map("f") + star() + string_map(" bar")).wildcard_match("foo bar"));
assert!((string_map("f") + star() + star() + string_map("r")).wildcard_match("foo bar"));
assert!((star() + string_map("f") + star() + star() + star()).wildcard_match("foo bar"));
assert!(!(star() + string_map("foo")).wildcard_match("foo bar"));
assert!(!(string_map("bar") + star()).wildcard_match("foo bar"));
assert!(!(star() + string_map("bo") + star()).wildcard_match("foo bar"));
assert!(!(string_map("f") + star() + string_map("br")).wildcard_match("foo bar"));
assert!(!(star() + string_map("x") + star() + star() + star()).wildcard_match("foo bar"));
assert!(!empty().wildcard_match("foo bar"));
assert!(empty().wildcard_match(""));
assert!(star().wildcard_match(""));
assert!(!string_map("foo bar").wildcard_match(""));
assert!(string_map("*").wildcard_match("*"));
assert!(star().wildcard_match("*"));
assert!(!string_map("\u{0000}").wildcard_match("*"));
assert!(!string_map(r"\u{0000}").wildcard_match("*"));
}
#[test]
fn test_wildcard_match_unicode() {
assert!((string_map("y") + star()).wildcard_match("y̆"));
assert!(string_map("y̆").wildcard_match("y̆"));
assert!(!(star() + string_map("p") + star()).wildcard_match("y̆"));
assert!((star() + string_map("p") + star()).wildcard_match("ḛ̶͑͝x̶͔͛a̵̰̯͛m̴͉̋́p̷̠͂l̵͇̍̔ȩ̶̣͝"));
assert!((star() + string_map("a̵̰̯͛m̴͉̋́") + star()).wildcard_match("ḛ̶͑͝x̶͔͛a̵̰̯͛m̴͉̋́p̷̠͂l̵͇̍̔ȩ̶̣͝"));
assert!(!(string_map("y") + star()).wildcard_match("ḛ̶͑͝x̶͔͛a̵̰̯͛m̴͉̋́p̷̠͂l̵͇̍̔ȩ̶̣͝"));
}
}