use crate::charset::CharSet;
use rexact_unicode as uni;
type Extra = Vec<(u32, Vec<u32>)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaseModel {
EsLegacy,
SimpleFold,
Python,
PythonAscii,
}
pub struct CaseFold {
map: Vec<(u32, u32)>,
changed: CharSet,
extra: Vec<(u32, Vec<u32>)>,
model: CaseModel,
}
impl CaseFold {
pub fn new(model: CaseModel) -> Self {
let (map, extra): (Vec<(u32, u32)>, Extra) = match model {
CaseModel::EsLegacy => (
uni::upper_single_pairs()
.iter()
.copied()
.filter(|&(c, u)| !(c >= 128 && u < 128))
.collect(),
Vec::new(),
),
CaseModel::SimpleFold => (uni::simple_fold_pairs().to_vec(), Vec::new()),
CaseModel::Python => (
uni::lower_simple_pairs().to_vec(),
uni::py_lower_extra()
.iter()
.map(|(k, v)| (*k, v.to_vec()))
.collect(),
),
CaseModel::PythonAscii => ((0x41..=0x5A).map(|c| (c, c + 32)).collect(), Vec::new()),
};
let changed = CharSet::from_ranges(map.iter().map(|&(c, _)| (c, c)));
CaseFold {
map,
changed,
extra,
model,
}
}
pub fn canon(&self, c: u32) -> u32 {
self.map
.binary_search_by_key(&c, |&(k, _)| k)
.map(|i| self.map[i].1)
.unwrap_or(c)
}
fn images(&self, s: &CharSet) -> CharSet {
let mut out = s.difference(&self.changed);
let mapped: Vec<(u32, u32)> = self
.map
.iter()
.filter(|&&(c, _)| s.contains(c))
.map(|&(_, v)| (v, v))
.collect();
out = out.union(&CharSet::from_ranges(mapped));
if self.model == CaseModel::Python {
let more: Vec<(u32, u32)> = self
.extra
.iter()
.filter(|(k, _)| out.contains(*k))
.flat_map(|(_, v)| v.iter().map(|&x| (x, x)))
.collect();
out = out.union(&CharSet::from_ranges(more));
}
out
}
fn preimage(&self, t: &CharSet) -> CharSet {
let moved: Vec<(u32, u32)> = self
.map
.iter()
.filter(|&&(_, v)| t.contains(v))
.map(|&(c, _)| (c, c))
.collect();
t.difference(&self.changed)
.union(&CharSet::from_ranges(moved))
}
pub fn close(&self, s: &CharSet) -> CharSet {
self.preimage(&self.images(s))
}
pub fn close_python_class(&self, members: &CharSet, categories: &CharSet) -> CharSet {
if !self.has_case(members) {
return members.union(categories);
}
self.preimage(&self.images(members).union(categories))
}
pub fn has_case(&self, s: &CharSet) -> bool {
!s.intersect(&self.changed).is_empty()
|| self.map.iter().any(|&(_, v)| s.contains(v))
|| self.extra.iter().any(|(k, _)| s.contains(*k))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn set(cs: &[u32]) -> CharSet {
CharSet::from_ranges(cs.iter().map(|&c| (c, c)))
}
#[test]
fn simple_fold_orbits_include_kelvin_and_long_s() {
let f = CaseFold::new(CaseModel::SimpleFold);
let k = f.close(&set(&['k' as u32]));
assert!(k.contains('K' as u32) && k.contains(0x212A) && k.contains('k' as u32));
let s = f.close(&set(&['S' as u32]));
assert!(s.contains(0x17F) && s.contains('s' as u32));
assert!(!f.close(&set(&[0xDF])).contains('S' as u32)); }
#[test]
fn legacy_es_never_maps_non_ascii_to_ascii() {
let f = CaseFold::new(CaseModel::EsLegacy);
assert!(!f.close(&set(&['k' as u32])).contains(0x212A));
assert!(!f.close(&set(&['s' as u32])).contains(0x17F));
assert!(f.close(&set(&[0x3C3])).contains(0x3C2)); assert!(f.close(&set(&[0xE9])).contains(0xC9));
}
#[test]
fn python_has_dotless_i_equivalence_but_not_turkish_dotted() {
let f = CaseFold::new(CaseModel::Python);
let i = f.close(&set(&['i' as u32]));
assert!(i.contains('I' as u32) && i.contains(0x131) && i.contains(0x130));
let a = CaseFold::new(CaseModel::PythonAscii).close(&set(&['k' as u32]));
assert!(a.contains('K' as u32) && !a.contains(0x212A));
}
#[test]
fn python_class_without_cased_members_is_not_folded() {
let f = CaseFold::new(CaseModel::Python);
let digits = CharSet::range('0' as u32, '9' as u32);
assert_eq!(f.close_python_class(&digits, &CharSet::empty()), digits);
assert!(f
.close_python_class(&set(&['a' as u32]), &CharSet::empty())
.contains('A' as u32));
}
}