#[rustfmt::skip]
const CASE_PARTNERS: &[(char, char)] = &[
('\u{4B}', '\u{212A}'), ('\u{53}', '\u{17F}'), ('\u{6B}', '\u{212A}'), ('\u{73}', '\u{17F}'),
('\u{B5}', '\u{3BC}'), ('\u{C5}', '\u{212B}'), ('\u{DF}', '\u{1E9E}'), ('\u{E5}', '\u{212B}'),
('\u{17F}', '\u{73}'), ('\u{1C4}', '\u{1C5}'), ('\u{1C6}', '\u{1C5}'), ('\u{1C7}', '\u{1C8}'),
('\u{1C9}', '\u{1C8}'), ('\u{1CA}', '\u{1CB}'), ('\u{1CC}', '\u{1CB}'), ('\u{1F1}', '\u{1F2}'),
('\u{1F3}', '\u{1F2}'), ('\u{345}', '\u{3B9}'), ('\u{345}', '\u{1FBE}'),
('\u{390}', '\u{1FD3}'), ('\u{392}', '\u{3D0}'), ('\u{395}', '\u{3F5}'),
('\u{398}', '\u{3D1}'), ('\u{398}', '\u{3F4}'), ('\u{399}', '\u{345}'),
('\u{399}', '\u{1FBE}'), ('\u{39A}', '\u{3F0}'), ('\u{39C}', '\u{B5}'), ('\u{3A0}', '\u{3D6}'),
('\u{3A1}', '\u{3F1}'), ('\u{3A3}', '\u{3C2}'), ('\u{3A6}', '\u{3D5}'),
('\u{3A9}', '\u{2126}'), ('\u{3B0}', '\u{1FE3}'), ('\u{3B2}', '\u{3D0}'),
('\u{3B5}', '\u{3F5}'), ('\u{3B8}', '\u{3D1}'), ('\u{3B8}', '\u{3F4}'), ('\u{3B9}', '\u{345}'),
('\u{3B9}', '\u{1FBE}'), ('\u{3BA}', '\u{3F0}'), ('\u{3BC}', '\u{B5}'), ('\u{3C0}', '\u{3D6}'),
('\u{3C1}', '\u{3F1}'), ('\u{3C2}', '\u{3C3}'), ('\u{3C3}', '\u{3C2}'), ('\u{3C6}', '\u{3D5}'),
('\u{3C9}', '\u{2126}'), ('\u{3D0}', '\u{3B2}'), ('\u{3D1}', '\u{3B8}'),
('\u{3D5}', '\u{3C6}'), ('\u{3D6}', '\u{3C0}'), ('\u{3F0}', '\u{3BA}'), ('\u{3F1}', '\u{3C1}'),
('\u{3F4}', '\u{398}'), ('\u{3F5}', '\u{3B5}'), ('\u{412}', '\u{1C80}'),
('\u{414}', '\u{1C81}'), ('\u{41E}', '\u{1C82}'), ('\u{421}', '\u{1C83}'),
('\u{422}', '\u{1C84}'), ('\u{422}', '\u{1C85}'), ('\u{42A}', '\u{1C86}'),
('\u{432}', '\u{1C80}'), ('\u{434}', '\u{1C81}'), ('\u{43E}', '\u{1C82}'),
('\u{441}', '\u{1C83}'), ('\u{442}', '\u{1C84}'), ('\u{442}', '\u{1C85}'),
('\u{44A}', '\u{1C86}'), ('\u{462}', '\u{1C87}'), ('\u{463}', '\u{1C87}'),
('\u{1C80}', '\u{432}'), ('\u{1C81}', '\u{434}'), ('\u{1C82}', '\u{43E}'),
('\u{1C83}', '\u{441}'), ('\u{1C84}', '\u{442}'), ('\u{1C84}', '\u{1C85}'),
('\u{1C85}', '\u{442}'), ('\u{1C85}', '\u{1C84}'), ('\u{1C86}', '\u{44A}'),
('\u{1C87}', '\u{463}'), ('\u{1C88}', '\u{A64B}'), ('\u{1E60}', '\u{1E9B}'),
('\u{1E61}', '\u{1E9B}'), ('\u{1E9B}', '\u{1E61}'), ('\u{1F80}', '\u{1F88}'),
('\u{1F81}', '\u{1F89}'), ('\u{1F82}', '\u{1F8A}'), ('\u{1F83}', '\u{1F8B}'),
('\u{1F84}', '\u{1F8C}'), ('\u{1F85}', '\u{1F8D}'), ('\u{1F86}', '\u{1F8E}'),
('\u{1F87}', '\u{1F8F}'), ('\u{1F90}', '\u{1F98}'), ('\u{1F91}', '\u{1F99}'),
('\u{1F92}', '\u{1F9A}'), ('\u{1F93}', '\u{1F9B}'), ('\u{1F94}', '\u{1F9C}'),
('\u{1F95}', '\u{1F9D}'), ('\u{1F96}', '\u{1F9E}'), ('\u{1F97}', '\u{1F9F}'),
('\u{1FA0}', '\u{1FA8}'), ('\u{1FA1}', '\u{1FA9}'), ('\u{1FA2}', '\u{1FAA}'),
('\u{1FA3}', '\u{1FAB}'), ('\u{1FA4}', '\u{1FAC}'), ('\u{1FA5}', '\u{1FAD}'),
('\u{1FA6}', '\u{1FAE}'), ('\u{1FA7}', '\u{1FAF}'), ('\u{1FB3}', '\u{1FBC}'),
('\u{1FBE}', '\u{345}'), ('\u{1FBE}', '\u{3B9}'), ('\u{1FC3}', '\u{1FCC}'),
('\u{1FD3}', '\u{390}'), ('\u{1FE3}', '\u{3B0}'), ('\u{1FF3}', '\u{1FFC}'),
('\u{2126}', '\u{3A9}'), ('\u{212A}', '\u{4B}'), ('\u{212B}', '\u{C5}'),
('\u{A64A}', '\u{1C88}'), ('\u{A64B}', '\u{1C88}'), ('\u{FB05}', '\u{FB06}'),
('\u{FB06}', '\u{FB05}'),
];
#[derive(Debug, Clone, Copy)]
pub(crate) struct CaseVariants {
variants: [char; 5],
len: u8,
}
impl CaseVariants {
#[inline]
pub(crate) fn new(ch: char) -> Self {
if ch.is_ascii() {
let mut this = Self {
variants: [ch; 5],
len: 1,
};
if ch.is_ascii_alphabetic() {
this.push((ch as u8 ^ 0x20) as char);
match ch.to_ascii_lowercase() {
'k' => this.push('\u{212a}'),
's' => this.push('\u{17f}'),
_ => {}
}
}
return this;
}
Self::from_tables(ch)
}
fn from_tables(ch: char) -> Self {
let mut this = Self {
variants: [ch; 5],
len: 1,
};
if ch != '\u{131}' {
for mapped in [single(ch.to_lowercase()), single(ch.to_uppercase())]
.into_iter()
.flatten()
{
this.push(mapped);
}
}
let first = CASE_PARTNERS.partition_point(|(probe, _)| *probe < ch);
for (_, partner) in CASE_PARTNERS[first..]
.iter()
.take_while(|(probe, _)| *probe == ch)
{
this.push(*partner);
}
this
}
fn push(&mut self, variant: char) {
self.variants[usize::from(self.len)] = variant;
self.len += 1;
}
pub(crate) fn iter(&self) -> impl Iterator<Item = char> + '_ {
self.variants[..usize::from(self.len)].iter().copied()
}
}
fn single(mut mapped: impl Iterator<Item = char>) -> Option<char> {
let first = mapped.next()?;
mapped.next().is_none().then_some(first)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::regex::backtrack::unicode_case_eq;
use std::collections::HashMap;
#[test]
fn partner_table_is_complete() {
let mut groups: HashMap<(bool, String), Vec<char>> = HashMap::new();
for ch in (0..=0x10_ffff).filter_map(char::from_u32) {
groups
.entry((false, ch.to_lowercase().collect()))
.or_default()
.push(ch);
groups
.entry((true, ch.to_uppercase().collect()))
.or_default()
.push(ch);
}
for group in groups.values().filter(|group| group.len() > 1) {
for &probe in group {
let variants = CaseVariants::new(probe).iter().collect::<Vec<_>>();
for &other in group {
assert_eq!(
unicode_case_eq(probe, other),
variants.contains(&other),
"{probe:?} and {other:?}"
);
}
}
}
for ch in (0..=0x10_ffff).filter_map(char::from_u32) {
for variant in CaseVariants::new(ch).iter() {
assert!(unicode_case_eq(ch, variant), "{ch:?} and {variant:?}");
}
}
}
#[test]
fn ascii_fast_path_matches_the_tables() {
for ch in (0..=0x7f).map(char::from) {
let mut fast = CaseVariants::new(ch).iter().collect::<Vec<_>>();
let mut tables = CaseVariants::from_tables(ch).iter().collect::<Vec<_>>();
for variants in [&mut fast, &mut tables] {
variants.sort_unstable();
variants.dedup();
}
assert_eq!(fast, tables, "{ch:?}");
}
}
}