#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
macro_rules! impl_dowild {
( $type:ty: $for:ty ) => {
impl DoWild<$type> for $for {
fn dowild(&self, haystack: Self) -> bool {
dowild(self, haystack)
}
fn dowild_with(&self, haystack: Self, options: Options<$type>) -> bool {
dowild_with(self, haystack, options)
}
}
};
( $type:ty: $for:ty => $( $tail:tt )* ) => {
impl DoWild<$type> for $for {
fn dowild(&self, haystack: Self) -> bool {
dowild(self $( $tail )*, haystack $( $tail )* )
}
fn dowild_with(&self, haystack: Self, options: Options<$type>) -> bool {
dowild_with(self $( $tail )*, haystack $( $tail )*, options)
}
}
};
}
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::collections::VecDeque;
#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use core::cmp::Ordering;
use core::fmt::Display;
use core::ops::Deref;
#[cfg(feature = "std")]
use std::collections::VecDeque;
#[cfg(feature = "std")]
use std::error::Error;
#[cfg(feature = "std")]
use std::string::String;
#[cfg(feature = "std")]
use std::vec::Vec;
pub trait DoWild<T>
where
T: Wildcard,
{
#[must_use]
fn dowild(&self, haystack: Self) -> bool;
#[must_use]
fn dowild_with(&self, haystack: Self, options: Options<T>) -> bool;
}
pub trait Wildcard: Eq + Copy + Clone {
const DEFAULT_ANY: Self;
const DEFAULT_CLASS_CLOSE: Self;
const DEFAULT_CLASS_HYPHEN: Self;
const DEFAULT_CLASS_NEGATE: Self;
const DEFAULT_CLASS_OPEN: Self;
const DEFAULT_ESCAPE: Self;
const DEFAULT_ONE: Self;
fn match_one_case_insensitive(first: Self, second: Self) -> bool;
fn match_one_case_sensitive(first: Self, second: Self) -> bool;
fn match_range_case_insensitive(token: Self, low: Self, high: Self) -> bool;
fn match_range_case_sensitive(token: Self, low: Self, high: Self) -> bool;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum BorrowedOrOwned<'a, T> {
Borrowed(&'a T),
Owned(T),
}
#[derive(Debug, Clone)]
enum Class<T> {
Positive(Vec<ClassKind<T>>),
Negative(Vec<ClassKind<T>>),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum ClassKind<T> {
Range(T, T),
One(T),
RangeOne(T),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SimpleMatchError {
DuplicateCharacterAssignment,
}
#[derive(Debug, Clone)]
struct CharacterClass<T> {
class: Option<Class<T>>,
end: usize,
start: usize,
}
#[derive(Debug, Clone)]
struct CharacterClasses<T>(VecDeque<CharacterClass<T>>);
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub struct Options<T>
where
T: Wildcard,
{
pub case_sensitive: bool,
pub class_negate: T,
pub is_classes_enabled: bool,
pub is_escape_enabled: bool,
pub wildcard_any: T,
pub wildcard_escape: T,
pub wildcard_one: T,
}
impl<T> Deref for BorrowedOrOwned<'_, T> {
type Target = T;
#[inline]
fn deref(&self) -> &Self::Target {
match self {
BorrowedOrOwned::Borrowed(value) => value,
BorrowedOrOwned::Owned(value) => value,
}
}
}
impl<T> AsRef<T> for BorrowedOrOwned<'_, T> {
#[inline]
fn as_ref(&self) -> &T {
self
}
}
impl<T> CharacterClass<T>
where
T: Wildcard + Ord,
{
#[inline]
const fn new(class: Option<Class<T>>, start: usize, end: usize) -> Self {
Self { class, end, start }
}
#[inline]
const fn new_invalid(start: usize, end: usize) -> Self {
Self::new(None, start, end)
}
#[inline]
const fn len(&self) -> usize {
self.end - self.start + 1
}
fn parse(start: usize, pattern: &[T], class_negate: T) -> Self {
let mut p_idx = start + 1;
if p_idx + 2 > pattern.len() {
return Self::new_invalid(start, p_idx + 1);
}
let mut class = if pattern[p_idx] == class_negate {
p_idx += 1;
Class::new_negative()
} else {
Class::new_positive()
};
if pattern[p_idx] == T::DEFAULT_CLASS_CLOSE {
let kind = ClassKind::parse_first(p_idx, pattern);
p_idx += kind.len();
class.push(kind);
}
if p_idx < pattern.len() {
while let Some(kind) = ClassKind::parse(p_idx, pattern) {
p_idx += kind.len();
if p_idx >= pattern.len() {
return Self::new_invalid(start, p_idx);
}
class.push(kind);
}
Self::new(Some(class), start, p_idx)
} else {
Self::new_invalid(start, p_idx)
}
}
#[inline]
fn try_match<F, G>(&self, token: T, match_one: F, match_range: G) -> Option<bool>
where
F: Fn(T, T) -> bool + Copy,
G: Fn(T, T, T) -> bool + Copy,
{
self.class
.as_ref()
.map(|class| class.is_match(token, match_one, match_range))
}
}
impl<T> CharacterClasses<T>
where
T: Wildcard + Ord,
{
#[inline]
fn new() -> Self {
Self(VecDeque::new())
}
#[inline]
fn get(&self, index: usize) -> Option<&CharacterClass<T>> {
self.0.iter().find(|r| r.start == index)
}
#[inline]
fn parse(start: usize, pattern: &[T], class_negate: T) -> CharacterClass<T> {
CharacterClass::parse(start, pattern, class_negate)
}
fn get_or_add(&mut self, start: usize, pattern: &[T], class_negate: T) -> &CharacterClass<T> {
if let Some(last) = self.0.back() {
#[allow(clippy::else_if_without_else)]
if last.start == start {
return unsafe { &*(last as *const CharacterClass<T>) };
} else if last.start > start {
return self.get(start).unwrap();
}
}
let class = Self::parse(start, pattern, class_negate);
self.0.push_back(class);
unsafe { self.0.back().unwrap_unchecked() }
}
#[inline]
fn prune(&mut self, index: usize) {
while let Some(first) = self.0.front() {
if first.start < index {
self.0.pop_front();
} else {
break;
}
}
}
}
impl<T> Class<T>
where
T: Wildcard + Ord,
{
#[inline]
const fn new_positive() -> Self {
Self::Positive(Vec::new())
}
#[inline]
const fn new_negative() -> Self {
Self::Negative(Vec::new())
}
#[inline]
fn push(&mut self, kind: ClassKind<T>) {
match self {
Self::Positive(kinds) | Self::Negative(kinds) => {
if kinds.last() != Some(&kind) {
kinds.push(kind);
}
}
}
}
#[inline]
fn is_match<F, G>(&self, token: T, match_one: F, match_range: G) -> bool
where
F: Fn(T, T) -> bool + Copy,
G: Fn(T, T, T) -> bool + Copy,
{
match self {
Self::Positive(kinds) => kinds
.iter()
.any(|r| r.contains(&token, match_one, match_range)),
Self::Negative(kinds) => !kinds
.iter()
.any(|r| r.contains(&token, match_one, match_range)),
}
}
}
impl<T> ClassKind<T>
where
T: Wildcard + Ord,
{
#[inline]
fn contains<F, G>(&self, token: &T, match_one: F, match_range: G) -> bool
where
F: Fn(T, T) -> bool,
G: Fn(T, T, T) -> bool,
{
match self {
Self::Range(low, high) => match_range(*token, *low, *high),
Self::One(c) | Self::RangeOne(c) => match_one(*c, *token),
}
}
#[inline]
fn parse(index: usize, pattern: &[T]) -> Option<Self> {
if pattern[index] == T::DEFAULT_CLASS_CLOSE {
None
} else {
Some(Self::parse_first(index, pattern))
}
}
fn parse_first(index: usize, pattern: &[T]) -> Self {
let first = pattern[index];
if index + 2 < pattern.len() && pattern[index + 1] == T::DEFAULT_CLASS_HYPHEN {
let second = pattern[index + 2];
if second == T::DEFAULT_CLASS_CLOSE {
Self::One(first)
} else {
match first.cmp(&second) {
Ordering::Less => Self::Range(first, second),
Ordering::Equal => Self::RangeOne(first),
Ordering::Greater => Self::Range(second, first),
}
}
} else {
Self::One(first)
}
}
#[inline]
const fn len(&self) -> usize {
match self {
Self::Range(_, _) | Self::RangeOne(_) => 3,
Self::One(_) => 1,
}
}
}
impl<T> Default for Options<T>
where
T: Wildcard,
{
fn default() -> Self {
Self::new()
}
}
impl<T> Options<T>
where
T: Wildcard,
{
#[must_use]
pub const fn new() -> Self {
Self {
case_sensitive: true,
wildcard_escape: T::DEFAULT_ESCAPE,
is_classes_enabled: false,
class_negate: T::DEFAULT_CLASS_NEGATE,
wildcard_any: T::DEFAULT_ANY,
wildcard_one: T::DEFAULT_ONE,
is_escape_enabled: false,
}
}
#[must_use]
pub const fn case_insensitive(mut self, yes: bool) -> Self {
self.case_sensitive = !yes;
self
}
#[must_use]
pub const fn enable_escape(mut self, yes: bool) -> Self {
self.is_escape_enabled = yes;
self
}
#[must_use]
pub const fn enable_escape_with(mut self, token: T) -> Self {
self.is_escape_enabled = true;
self.wildcard_escape = token;
self
}
#[must_use]
pub const fn enable_classes(mut self, yes: bool) -> Self {
self.is_classes_enabled = yes;
self
}
#[must_use]
pub const fn enable_classes_with(mut self, negation: T) -> Self {
self.is_classes_enabled = true;
self.class_negate = negation;
self
}
#[must_use]
pub const fn wildcard_any_with(mut self, token: T) -> Self {
self.wildcard_any = token;
self
}
#[must_use]
pub const fn wildcard_one_with(mut self, token: T) -> Self {
self.wildcard_one = token;
self
}
pub fn verify(&self) -> Result<(), SimpleMatchError> {
if self.wildcard_any == self.wildcard_one
|| self.wildcard_any == self.wildcard_escape
|| self.wildcard_any == self.class_negate
|| self.wildcard_one == self.wildcard_escape
|| self.wildcard_one == self.class_negate
|| self.wildcard_escape == self.class_negate
{
return Err(SimpleMatchError::DuplicateCharacterAssignment);
}
Ok(())
}
pub fn verified(self) -> Result<Self, SimpleMatchError> {
self.verify().map(|()| self)
}
}
#[cfg(feature = "std")]
impl Error for SimpleMatchError {}
impl Display for SimpleMatchError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::DuplicateCharacterAssignment => {
write!(
f,
"Verifying options failed: The options contain a duplicate character \
assignment."
)
}
}
}
}
impl_dowild!(u8: &[u8]);
impl_dowild!(u8: &str => .as_bytes());
impl_dowild!(u8: String => .as_bytes());
impl_dowild!(u8: Vec<u8> => .as_slice());
impl_dowild!(char: &[char]);
impl_dowild!(char: Vec<char> => .as_slice());
impl Wildcard for u8 {
const DEFAULT_ANY: Self = b'*';
const DEFAULT_ESCAPE: Self = b'\\';
const DEFAULT_ONE: Self = b'?';
const DEFAULT_CLASS_CLOSE: Self = b']';
const DEFAULT_CLASS_HYPHEN: Self = b'-';
const DEFAULT_CLASS_NEGATE: Self = b'!';
const DEFAULT_CLASS_OPEN: Self = b'[';
#[inline]
fn match_one_case_sensitive(first: Self, second: Self) -> bool {
first == second
}
#[inline]
fn match_one_case_insensitive(first: Self, second: Self) -> bool {
first.eq_ignore_ascii_case(&second)
}
#[inline]
fn match_range_case_sensitive(token: Self, low: Self, high: Self) -> bool {
low <= token && token <= high
}
#[inline]
fn match_range_case_insensitive(token: Self, low: Self, high: Self) -> bool {
if low <= token && token <= high {
true
} else if !token.is_ascii_alphabetic() {
false
} else {
is_in_ascii_range_case_insensitive(token, low, high)
}
}
}
impl Wildcard for char {
const DEFAULT_ANY: Self = '*';
const DEFAULT_ESCAPE: Self = '\\';
const DEFAULT_ONE: Self = '?';
const DEFAULT_CLASS_CLOSE: Self = ']';
const DEFAULT_CLASS_HYPHEN: Self = '-';
const DEFAULT_CLASS_NEGATE: Self = '!';
const DEFAULT_CLASS_OPEN: Self = '[';
#[inline]
fn match_one_case_insensitive(first: Self, second: Self) -> bool {
first.eq_ignore_ascii_case(&second)
}
#[inline]
fn match_one_case_sensitive(first: Self, second: Self) -> bool {
first == second
}
#[inline]
fn match_range_case_sensitive(token: Self, low: Self, high: Self) -> bool {
low <= token && token <= high
}
#[inline]
fn match_range_case_insensitive(token: Self, low: Self, high: Self) -> bool {
if low <= token && token <= high {
true
} else if !(low.is_ascii() && high.is_ascii() && token.is_ascii_alphabetic()) {
false
} else {
is_in_ascii_range_case_insensitive(token as u8, low as u8, high as u8)
}
}
}
#[must_use]
pub fn dowild<T>(pattern: &[T], haystack: &[T]) -> bool
where
T: Wildcard,
{
let mut p_idx = 0;
let mut h_idx = 0;
let mut next_p_idx = 0;
let mut next_h_idx = 0;
let wildcard_any = T::DEFAULT_ANY;
let wildcard_one = T::DEFAULT_ONE;
let mut has_seen_wildcard_any = false;
while p_idx < pattern.len() || h_idx < haystack.len() {
if p_idx < pattern.len() {
match pattern[p_idx] {
c if c == wildcard_any => {
has_seen_wildcard_any = true;
p_idx += 1;
while p_idx < pattern.len() && pattern[p_idx] == wildcard_any {
p_idx += 1;
}
if p_idx >= pattern.len() {
return true;
}
let next_c = pattern[p_idx];
if next_c == wildcard_one {
while h_idx < haystack.len() {
p_idx += 1;
h_idx += 1;
if !(p_idx < pattern.len() && pattern[p_idx] == next_c) {
break;
}
}
if p_idx >= pattern.len() {
return true;
}
} else {
while h_idx < haystack.len() && haystack[h_idx] != next_c {
h_idx += 1;
}
if h_idx >= haystack.len() {
return false;
}
}
next_p_idx = p_idx;
next_h_idx = h_idx;
continue;
}
c if c == wildcard_one => {
if h_idx < haystack.len() {
p_idx += 1;
h_idx += 1;
continue;
}
}
c => {
if h_idx < haystack.len() && haystack[h_idx] == c {
p_idx += 1;
h_idx += 1;
continue;
}
}
}
}
if has_seen_wildcard_any && next_h_idx < haystack.len() {
p_idx = next_p_idx;
next_h_idx += 1;
if p_idx < pattern.len() {
while next_h_idx < haystack.len() && haystack[next_h_idx] != pattern[p_idx] {
next_h_idx += 1;
}
}
h_idx = next_h_idx;
continue;
}
return false;
}
true
}
#[must_use]
pub fn dowild_with<T>(pattern: &[T], haystack: &[T], options: Options<T>) -> bool
where
T: Wildcard + Ord,
{
if options.case_sensitive {
dowild_with_worker(
pattern,
haystack,
options,
T::match_one_case_sensitive,
T::match_range_case_sensitive,
)
} else {
dowild_with_worker(
pattern,
haystack,
options,
T::match_one_case_insensitive,
T::match_range_case_insensitive,
)
}
}
#[inline]
#[allow(clippy::too_many_lines)]
fn dowild_with_worker<F, G, T>(
pattern: &[T],
haystack: &[T],
options: Options<T>,
match_one: F,
match_range: G,
) -> bool
where
T: Wildcard + Ord,
F: Fn(T, T) -> bool + Copy,
G: Fn(T, T, T) -> bool + Copy,
{
let Options {
class_negate,
is_classes_enabled,
is_escape_enabled,
wildcard_any,
wildcard_escape,
wildcard_one,
..
} = options;
let is_wildcard_any = |token: T| token == wildcard_any;
let is_wildcard_one = |token: T| token == wildcard_one;
let is_escape = |token: T| is_escape_enabled && token == wildcard_escape;
let is_class_open = |token: T| is_classes_enabled && token == T::DEFAULT_CLASS_OPEN;
let is_special = |token: T| {
token == wildcard_any
|| token == wildcard_one
|| token == wildcard_escape
|| (is_classes_enabled && token == T::DEFAULT_CLASS_OPEN)
};
let is_valid_class_or_escape = |token: T, p_idx: usize, invalid_class_idx: usize| {
(is_classes_enabled && token == T::DEFAULT_CLASS_OPEN && p_idx < invalid_class_idx)
|| (is_escape_enabled && token == wildcard_escape)
};
let mut p_idx = 0;
let mut h_idx = 0;
let mut next_p_idx = 0;
let mut next_h_idx = 0;
let mut classes = CharacterClasses::new();
let mut has_seen_wildcard_any = false;
let mut invalid_class_idx = usize::MAX;
while p_idx < pattern.len() || h_idx < haystack.len() {
if p_idx < pattern.len() {
match pattern[p_idx] {
c if is_wildcard_any(c) => {
has_seen_wildcard_any = true;
p_idx += 1;
while p_idx < pattern.len() && is_wildcard_any(pattern[p_idx]) {
p_idx += 1;
}
if p_idx >= pattern.len() {
return true;
}
let next_c = pattern[p_idx];
#[allow(clippy::else_if_without_else)]
if is_wildcard_one(next_c) {
while h_idx < haystack.len() {
p_idx += 1;
h_idx += 1;
if !(p_idx < pattern.len() && is_wildcard_one(pattern[p_idx])) {
break;
}
}
if p_idx >= pattern.len() {
return true;
}
} else if !is_valid_class_or_escape(next_c, p_idx, invalid_class_idx) {
while h_idx < haystack.len() && !match_one(haystack[h_idx], next_c) {
h_idx += 1;
}
if h_idx >= haystack.len() {
return false;
}
}
next_p_idx = p_idx;
next_h_idx = h_idx;
continue;
}
c if is_wildcard_one(c) => {
if h_idx < haystack.len() {
p_idx += 1;
h_idx += 1;
continue;
}
}
c if is_escape(c) && p_idx + 1 < pattern.len() => {
if h_idx < haystack.len() {
let next_c = pattern[p_idx + 1];
let h = haystack[h_idx];
#[allow(clippy::else_if_without_else)]
if is_special(next_c) && h == next_c {
p_idx += 2;
h_idx += 1;
continue;
} else if !is_special(next_c) && h == wildcard_escape {
p_idx += 1;
h_idx += 1;
continue;
}
}
}
c if is_class_open(c) && p_idx < invalid_class_idx && p_idx + 1 < pattern.len() => {
if h_idx < haystack.len() {
let class = if has_seen_wildcard_any {
classes.prune(next_p_idx);
BorrowedOrOwned::Borrowed(classes.get_or_add(
p_idx,
pattern,
class_negate,
))
} else {
BorrowedOrOwned::Owned(CharacterClasses::parse(
p_idx,
pattern,
class_negate,
))
};
#[allow(clippy::else_if_without_else)]
if let Some(is_match) =
class.try_match(haystack[h_idx], match_one, match_range)
{
p_idx += class.len();
if is_match {
h_idx += 1;
continue;
}
} else {
invalid_class_idx = class.as_ref().start;
if match_one(haystack[h_idx], T::DEFAULT_CLASS_OPEN) {
p_idx += 1;
h_idx += 1;
continue;
}
}
}
}
c => {
if h_idx < haystack.len() && match_one(haystack[h_idx], c) {
p_idx += 1;
h_idx += 1;
continue;
}
}
}
}
if has_seen_wildcard_any && next_h_idx < haystack.len() {
p_idx = next_p_idx;
next_h_idx += 1;
if p_idx < pattern.len()
&& !is_valid_class_or_escape(pattern[p_idx], p_idx, invalid_class_idx)
{
while next_h_idx < haystack.len() && !match_one(haystack[next_h_idx], pattern[p_idx])
{
next_h_idx += 1;
}
}
h_idx = next_h_idx;
continue;
}
return false;
}
true
}
#[inline]
const fn is_in_ascii_range_case_insensitive(token: u8, low: u8, high: u8) -> bool {
const ASCII_CASE_MASK: u8 = 0b0010_0000;
if token.is_ascii_lowercase() {
let token_uppercase = token ^ ASCII_CASE_MASK;
low <= token_uppercase && token_uppercase <= high
} else {
let token_lowercase = token | ASCII_CASE_MASK;
low <= token_lowercase && token_lowercase <= high
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::same_case_sensitive(b'j', b'j', true, true)]
#[case::different_case_case_sensitive(b'j', b'J', true, false)]
#[case::different_char_case_sensitive(b'a', b'b', true, false)]
#[case::same_case_insensitive(b'j', b'j', false, true)]
#[case::different_case_insensitive(b'j', b'J', false, true)]
#[case::different_char_case_insensitive(b'a', b'B', false, false)]
fn impl_wildcard_match_one(
#[case] first: u8,
#[case] second: u8,
#[case] case_sensitive: bool,
#[case] expected: bool,
) {
if case_sensitive {
assert_eq!(Wildcard::match_one_case_sensitive(first, second), expected);
assert_eq!(
Wildcard::match_one_case_sensitive(first as char, second as char),
expected
);
} else {
assert_eq!(
Wildcard::match_one_case_insensitive(first, second),
expected
);
assert_eq!(
Wildcard::match_one_case_insensitive(first as char, second as char),
expected
);
}
}
#[rstest]
#[case::all_the_same(b'j', b'j', b'j', true)]
#[case::low_is_higher_high_is_same(b'j', b'k', b'j', false)]
#[case::low_is_lower_high_is_same(b'j', b'i', b'j', true)]
#[case::high_is_lower_low_is_same(b'j', b'k', b'i', false)]
#[case::high_is_higher_low_is_same(b'j', b'j', b'k', true)]
#[case::non_alpha_when_false(b'#', b'*', b']', false)]
#[case::non_alpha_when_true(b'+', b'*', b']', true)]
#[case::only_token_alpha(b'a', b'*', b']', false)]
#[case::only_token_big_alpha(b'A', b'*', b']', true)]
#[case::between_alphabetic(b']', b'*', b'B', false)]
fn impl_wildcard_match_range_when_case_sensitive(
#[case] token: u8,
#[case] low: u8,
#[case] high: u8,
#[case] expected: bool,
) {
assert_eq!(
Wildcard::match_range_case_sensitive(token, low, high),
expected
);
assert_eq!(
Wildcard::match_range_case_sensitive(token as char, low as char, high as char),
expected
);
}
#[rstest]
#[case::all_the_same_small(b'j', b'j', b'j', true)]
#[case::all_the_same_big(b'J', b'J', b'J', true)]
#[case::no_alpha_low_is_big(b'[', b'A', b'z', true)]
#[case::no_alpha_both_big(b'[', b'A', b'Z', false)]
#[case::no_alpha_low_is_small(b'[', b'a', b'z', false)]
#[case::no_alpha_both_small(b'[', b'a', b'z', false)]
#[case::all_small_middle(b'j', b'a', b'z', true)]
#[case::all_small_low_is_higher(b'j', b'k', b'z', false)]
#[case::all_small_high_is_lower(b'j', b'a', b'i', false)]
#[case::all_big_middle(b'J', b'A', b'Z', true)]
#[case::all_big_low_is_higher(b'J', b'K', b'Z', false)]
#[case::all_big_high_is_lower(b'J', b'A', b'I', false)]
#[case::big_a_to_j(b'z', b'A', b'j', true)]
#[case::non_alpha_when_false(b'#', b'*', b']', false)]
#[case::control_when_false(b'\x1f', b'*', b']', false)]
#[case::non_alpha_when_true(b'+', b'*', b']', true)]
#[case::only_token_alpha(b'a', b'*', b']', true)]
#[case::only_token_big_alpha(b'A', b'*', b']', true)]
#[case::between_alphabetic(b']', b'*', b'B', false)]
fn impl_wildcard_match_range_when_case_insensitive(
#[case] token: u8,
#[case] low: u8,
#[case] high: u8,
#[case] expected: bool,
) {
assert_eq!(
Wildcard::match_range_case_insensitive(token, low, high),
expected
);
assert_eq!(
Wildcard::match_range_case_insensitive(token as char, low as char, high as char),
expected
);
}
}