pub(crate) fn casefold_char(c: char) -> impl DoubleEndedIterator<Item = char> + Clone {
c.to_lowercase()
.flat_map(char::to_uppercase)
.flat_map(char::to_lowercase)
}
pub(crate) fn casefold(s: &str) -> impl Iterator<Item = char> + Clone + '_ {
s.chars().flat_map(casefold_char)
}
pub(crate) fn casefold_rev(s: &str) -> impl Iterator<Item = char> + Clone + '_ {
s.chars().rev().flat_map(|c| casefold_char(c).rev())
}
pub(crate) fn caseless_eq(a: &str, b: &str) -> bool {
casefold(a).eq(casefold(b))
}
fn is_char_prefix(
mut haystack: impl Iterator<Item = char>,
prefix: impl Iterator<Item = char>,
) -> bool {
for c in prefix {
if haystack.next() != Some(c) {
return false;
}
}
true
}
pub(crate) fn caseless_contains(haystack: &str, needle: &str) -> bool {
let mut start = casefold(haystack);
loop {
if is_char_prefix(start.clone(), casefold(needle)) {
return true;
}
if start.next().is_none() {
return false;
}
}
}
pub(crate) fn caseless_starts_with(haystack: &str, needle: &str) -> bool {
is_char_prefix(casefold(haystack), casefold(needle))
}
pub(crate) fn caseless_ends_with(haystack: &str, needle: &str) -> bool {
is_char_prefix(casefold_rev(haystack), casefold_rev(needle))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("hello", "hello", true)]
#[case("Hello", "hELLO", true)]
#[case("hello", "hellos", false)]
#[case("", "", true)]
#[case("Jürgen", "JÜRGEN", true)]
#[case("ΛΟΓΟΣ", "λογος", true)]
#[case("λογοσ", "λογος", true)]
#[case("straße", "STRASSE", true)]
#[case("straße", "strasse", true)]
#[case("straẞe", "strasse", true)] #[case("İstanbul", "i\u{307}stanbul", true)]
#[case("İstanbul", "istanbul", false)] fn test_caseless_eq(#[case] a: &str, #[case] b: &str, #[case] expected: bool) {
assert_eq!(caseless_eq(a, b), expected);
assert_eq!(caseless_eq(b, a), expected);
}
#[rstest]
#[case("Hello World", "hello world")]
#[case("ΛΟΓΟΣ", "λογοσ")] #[case("λογος", "λογοσ")]
#[case("İstanbul", "i\u{307}stanbul")]
#[case("straße", "strasse")] #[case("STRASSE", "strasse")]
fn test_casefold(#[case] input: &str, #[case] expected: &str) {
assert_eq!(casefold(input).collect::<String>(), expected);
assert_eq!(
casefold_rev(input).collect::<String>(),
expected.chars().rev().collect::<String>()
);
}
#[rstest]
#[case("Hello World", "WORLD", true)]
#[case("Hello World", "mars", false)]
#[case("ΛΟΓΟΣ", "ς", true)]
fn test_caseless_contains(
#[case] haystack: &str,
#[case] needle: &str,
#[case] expected: bool,
) {
assert_eq!(caseless_contains(haystack, needle), expected);
}
#[rstest]
#[case("Hello World", "hello", true)]
#[case("Hello World", "world", false)]
fn test_caseless_starts_with(
#[case] haystack: &str,
#[case] needle: &str,
#[case] expected: bool,
) {
assert_eq!(caseless_starts_with(haystack, needle), expected);
}
#[rstest]
#[case("Hello World", "WORLD", true)]
#[case("Hello World", "hello", false)]
#[case("ΛΟΓΟΣ", "Σ", true)]
fn test_caseless_ends_with(
#[case] haystack: &str,
#[case] needle: &str,
#[case] expected: bool,
) {
assert_eq!(caseless_ends_with(haystack, needle), expected);
}
}