use super::generated::collation as tables;
use super::normalize::{canonical_combining_class as ccc, nfd};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::cmp::Ordering;
#[inline]
fn primary(ce: u64) -> u16 {
((ce >> 32) & 0xFFFF) as u16
}
#[inline]
fn secondary(ce: u64) -> u16 {
((ce >> 16) & 0xFFFF) as u16
}
#[inline]
fn tertiary(ce: u64) -> u16 {
(ce & 0xFFFF) as u16
}
#[inline]
fn is_variable(ce: u64) -> bool {
(ce >> 48) & 1 != 0
}
#[inline]
fn pack(p: u32, s: u32, t: u32) -> u64 {
(p as u64) << 32 | (s as u64) << 16 | t as u64
}
#[inline]
fn sub_weight(ce: u64) -> u16 {
((ce >> 49) & 0x7FFF) as u16
}
#[inline]
fn pack_tailored(base: u32, sub: u32, s: u32, t: u32) -> u64 {
((sub as u64) << 49) | (base as u64) << 32 | (s as u64) << 16 | t as u64
}
const SUB_MID: u16 = 0x4000;
const SUB_BEFORE: u16 = 0x2000;
#[cfg(feature = "collation-zh")]
const ZH_PINYIN: &[u8] = include_bytes!("collation_zh.bin");
#[cfg(feature = "collation-zh")]
const ZH_STROKE: &[u8] = include_bytes!("collation_zh_stroke.bin");
#[cfg(feature = "collation-zh")]
const ZH_ZHUYIN: &[u8] = include_bytes!("collation_zh_zhuyin.bin");
#[cfg(feature = "collation-zh")]
const ZH_PINYIN_RS: &[u8] = include_bytes!("collation_zh_rs.bin");
#[cfg(feature = "collation-zh")]
const ZH_RS_MARKER: u32 = 0x8000;
#[cfg(feature = "collation-zh")]
const ZH_HAN_BASE: u32 = 0x21F0;
#[cfg(feature = "collation-zh")]
fn zh_ranked(table: &[u8], cp: u32) -> Option<u16> {
let count = u32::from_le_bytes([table[0], table[1], table[2], table[3]]) as usize;
let cps = 4; let ranks = cps + count * 4; let (mut lo, mut hi) = (0usize, count);
while lo < hi {
let mid = (lo + hi) / 2;
let o = cps + mid * 4;
let v = u32::from_le_bytes([table[o], table[o + 1], table[o + 2], table[o + 3]]);
match v.cmp(&cp) {
Ordering::Less => lo = mid + 1,
Ordering::Greater => hi = mid,
Ordering::Equal => {
let r = ranks + mid * 2;
return Some(u16::from_le_bytes([table[r], table[r + 1]]));
}
}
}
None
}
#[cfg(feature = "collation-zh")]
fn zh_rs_key(cp: u32) -> Option<u16> {
let count = u32::from_le_bytes([
ZH_PINYIN_RS[0],
ZH_PINYIN_RS[1],
ZH_PINYIN_RS[2],
ZH_PINYIN_RS[3],
]) as usize;
let cps = 4; let keys = cps + count * 4; let (mut lo, mut hi) = (0usize, count);
while lo < hi {
let mid = (lo + hi) / 2;
let o = cps + mid * 4;
let v = u32::from_le_bytes([
ZH_PINYIN_RS[o],
ZH_PINYIN_RS[o + 1],
ZH_PINYIN_RS[o + 2],
ZH_PINYIN_RS[o + 3],
]);
match v.cmp(&cp) {
Ordering::Less => lo = mid + 1,
Ordering::Greater => hi = mid,
Ordering::Equal => {
let r = keys + mid * 2;
return Some(u16::from_le_bytes([ZH_PINYIN_RS[r], ZH_PINYIN_RS[r + 1]]));
}
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlternateHandling {
NonIgnorable,
Shifted,
}
const IMPLICIT_RANGES: &[(u32, u32, u32, u32)] = &[
(0x17000, 0x187FF, 0xFB00, 0x17000),
(0x18800, 0x18AFF, 0xFB01, 0x18800),
(0x18D00, 0x18D7F, 0xFB00, 0x17000),
(0x18D80, 0x18DFF, 0xFB01, 0x18800),
(0x1B170, 0x1B2FF, 0xFB02, 0x1B170),
(0x18B00, 0x18CFF, 0xFB03, 0x18B00),
];
fn push_implicit(cp: u32, out: &mut Vec<u64>) {
let (aaaa, bbbb) = implicit_primaries(cp);
out.push(pack(aaaa, 0x0020, 0x0002));
out.push(pack(bbbb, 0x0000, 0x0000));
}
fn implicit_primaries(cp: u32) -> (u32, u32) {
for &(first, last, base, origin) in IMPLICIT_RANGES {
if cp >= first && cp <= last {
return (base, (cp - origin) | 0x8000);
}
}
let base = if tables::unified_ideograph(cp) {
if (0x4E00..=0x9FFF).contains(&cp) || (0xF900..=0xFAFF).contains(&cp) {
0xFB40
} else {
0xFB80
}
} else {
0xFBC0
};
(base + (cp >> 15), (cp & 0x7FFF) | 0x8000)
}
fn lookup_contraction(first: u32, suffix: &[char]) -> Option<&'static [u64]> {
for (suf, ces) in tables::contractions(first)? {
if *suf == suffix {
return Some(ces);
}
}
None
}
fn collation_elements(cv: Vec<char>) -> Vec<u64> {
let mut cea = Vec::new();
each_collation_element(&cv, |ces, opt, _start| {
match ces {
Some(ces) => cea.extend_from_slice(ces),
None => push_implicit(opt, &mut cea),
}
Walk::Continue
});
cea
}
fn collation_elements_tagged(cv: Vec<char>) -> (Vec<u64>, Vec<usize>) {
let mut cea = Vec::new();
let mut src = Vec::new();
each_collation_element(&cv, |ces, opt, start| {
match ces {
Some(ces) => {
for &ce in ces {
cea.push(ce);
src.push(start);
}
}
None => {
let before = cea.len();
push_implicit(opt, &mut cea);
for _ in before..cea.len() {
src.push(start);
}
}
}
Walk::Continue
});
(cea, src)
}
fn each_collation_element<F: FnMut(Option<&'static [u64]>, u32, usize) -> Walk>(
cv: &[char],
mut emit: F,
) {
let mut consumed = alloc::vec![false; cv.len()];
let mut suffix: Vec<char> = Vec::new(); let mut i = 0;
while i < cv.len() {
if consumed[i] {
i += 1;
continue;
}
let s0 = cv[i] as u32;
let mut end = i + 1;
let mut matched: Option<&'static [u64]> = tables::ce_singles(s0);
suffix.clear();
let mut max_suf = 0usize;
if let Some(entries) = tables::contractions(s0) {
for (suf, ces) in entries {
if suf.len() > max_suf {
max_suf = suf.len();
}
let stop = i + 1 + suf.len();
if stop <= cv.len() && cv[i + 1..stop] == **suf {
matched = Some(ces);
suffix.clear();
suffix.extend_from_slice(suf);
end = stop;
break;
}
}
}
if max_suf > suffix.len() {
loop {
let mut last_ccc = 0u8;
let mut j = end;
let mut hit: Option<usize> = None;
while j < cv.len() {
if consumed[j] {
j += 1;
continue;
}
let cc = ccc(cv[j]);
if cc == 0 {
break; }
if last_ccc < cc {
suffix.push(cv[j]);
if let Some(ces) = lookup_contraction(s0, &suffix) {
matched = Some(ces);
hit = Some(j); break;
}
suffix.pop();
last_ccc = cc;
} else {
break; }
j += 1;
}
match hit {
Some(j) => {
consumed[j] = true;
if suffix.len() >= max_suf {
break; }
}
None => break,
}
}
}
let verdict = match matched {
Some(ces) => emit(Some(ces), 0, i),
None => emit(None, s0, i),
};
if verdict == Walk::Stop {
return;
}
i = end;
}
}
#[derive(PartialEq, Eq, Clone, Copy)]
enum Walk {
Continue,
Stop,
}
fn emit_number(digits: &[char], cea: &mut Vec<u64>) {
let marker = primary(collation_elements(alloc::vec!['0'])[0]) as u32;
let first_sig = digits
.iter()
.position(|&c| c != '0')
.unwrap_or(digits.len() - 1);
let sig = &digits[first_sig..];
cea.push(pack(marker, 0, 0));
cea.push(pack(sig.len() as u32 + 1, 0, 0));
for &d in sig {
cea.push(pack((d as u32 - '0' as u32) + 1, 0, 0));
}
}
fn collation_elements_numeric(cv: Vec<char>) -> Vec<u64> {
let mut cea = Vec::new();
let mut i = 0;
while i < cv.len() {
if cv[i].is_ascii_digit() {
let start = i;
while i < cv.len() && cv[i].is_ascii_digit() {
i += 1;
}
emit_number(&cv[start..i], &mut cea);
} else {
let start = i;
while i < cv.len() && !cv[i].is_ascii_digit() {
i += 1;
}
cea.extend(collation_elements(cv[start..i].to_vec()));
}
}
cea
}
fn primaries(s: &str) -> Vec<u16> {
collation_elements(nfd(s.chars()).collect())
.into_iter()
.map(primary)
.filter(|&p| p != 0)
.collect()
}
#[must_use]
pub fn find(text: &str, pattern: &str) -> Option<core::ops::Range<usize>> {
let pat = primaries(pattern);
if pat.is_empty() {
return Some(0..0);
}
let need = pat.len();
let mut nfd_buf: Vec<char> = Vec::new();
let mut start_of: Vec<usize> = Vec::new();
let mut end_of: Vec<usize> = Vec::new();
for (b, c) in text.char_indices() {
for d in nfd(core::iter::once(c)) {
nfd_buf.push(d);
start_of.push(b);
end_of.push(b + c.len_utf8());
}
}
let (cea, src) = collation_elements_tagged(nfd_buf);
let mut prim: Vec<u16> = Vec::new();
let mut prim_start: Vec<usize> = Vec::new();
let mut prim_end: Vec<usize> = Vec::new();
let mut group_starts: alloc::collections::BTreeSet<usize> = alloc::collections::BTreeSet::new();
for (idx, &ce) in cea.iter().enumerate() {
group_starts.insert(start_of[src[idx]]);
let p = primary(ce);
if p != 0 {
prim.push(p);
prim_start.push(start_of[src[idx]]);
prim_end.push(end_of[src[idx]]);
}
}
let mut k = 0usize; for (a, _) in text.char_indices() {
while k < prim_start.len() && prim_start[k] < a {
k += 1;
}
if group_starts.contains(&a) {
if k + need <= prim.len() && prim[k..k + need] == pat[..] {
let b = prim_end[k + need - 1];
if primaries(&text[a..b]) == pat {
return Some(a..b);
}
}
} else if let Some((win, b)) = window_decision(&text[a..], need) {
if win == pat {
return Some(a..a + b);
}
}
}
None
}
fn window_decision(s: &str, need: usize) -> Option<(Vec<u16>, usize)> {
debug_assert!(need > 0);
let cut = find_cut(s, need)?;
for (b, _) in s[..cut]
.char_indices()
.skip(1)
.chain(core::iter::once((cut, '\0')))
{
let pr = primaries(&s[..b]);
if pr.len() >= need {
return Some((pr, b));
}
}
None
}
fn find_cut(s: &str, need: usize) -> Option<usize> {
let mut cap = need.saturating_mul(8).max(16);
loop {
let mut end = cap.min(s.len());
while end < s.len() && !s.is_char_boundary(end) {
end += 1;
}
let prefix = &s[..end];
let mut nfd_buf: Vec<char> = Vec::new();
let mut byte_of: Vec<usize> = Vec::new();
for (b, c) in prefix.char_indices() {
for d in nfd(core::iter::once(c)) {
nfd_buf.push(d);
byte_of.push(b);
}
}
let mut produced = 0usize;
let mut reached = false; let mut cut: Option<usize> = None;
each_collation_element(&nfd_buf, |ces, opt, start| {
if reached {
cut = Some(byte_of[start]);
return Walk::Stop;
}
let nonzero = match ces {
Some(ces) => ces.iter().any(|&ce| primary(ce) != 0),
None => implicit_primaries(opt).0 != 0,
};
if nonzero {
produced += 1;
if produced >= need {
reached = true;
}
}
Walk::Continue
});
if reached {
if let Some(cut) = cut {
return Some(cut);
}
if end == s.len() {
return Some(s.len());
}
} else if end == s.len() {
return None; }
cap = cap.saturating_mul(2);
}
}
#[must_use]
pub fn contains(text: &str, pattern: &str) -> bool {
find(text, pattern).is_some()
}
fn first_primary(tail: &Tailoring, s: &str) -> u32 {
let key = tail.sort_key(s);
let base = key.first().copied().unwrap_or(0) as u32;
if base == 0 {
return 0;
}
let sub = key.get(1).copied().unwrap_or(0) as u32;
(base << 16) | sub
}
#[must_use]
pub fn index_labels(lang: &str) -> Vec<alloc::string::String> {
use alloc::string::ToString;
let extra: &[&str] = match lang.split(['-', '_']).next().unwrap_or(lang) {
"sv" | "fi" => &["Å", "Ä", "Ö"],
"da" | "nb" | "nn" | "no" => &["Æ", "Ø", "Å"],
"is" => &["Þ", "Æ", "Ö"],
"es" | "gl" => &["Ñ"],
"et" => &["Š", "Ž", "Õ", "Ä", "Ö", "Ü"],
"cs" | "sk" => &["Č", "Ch", "Ř", "Š", "Ž"],
"pl" => &["Ą", "Ć", "Ę", "Ł", "Ń", "Ó", "Ś", "Ź", "Ż"],
"hu" => &["Cs", "Dz", "Dzs", "Gy", "Ly", "Ny", "Sz", "Ty", "Zs"],
"tr" | "az" => &["Ç", "Ğ", "Ö", "Ş", "Ü"],
"ro" => &["Ă", "Â", "Î", "Ș", "Ț"],
"sq" => &[
"Ç", "Dh", "Ë", "Gj", "Ll", "Nj", "Rr", "Sh", "Th", "Xh", "Zh",
],
"cy" => &["Ch", "Dd", "Ff", "Ng", "Ll", "Ph", "Rh", "Th"],
_ => &[],
};
let tail = Tailoring::for_locale(lang).unwrap_or_else(Tailoring::identity);
let mut labels: Vec<alloc::string::String> = ('A'..='Z')
.map(|c| c.to_string())
.chain(extra.iter().map(|s| s.to_string()))
.collect();
labels.sort_by_key(|l| first_primary(&tail, l));
labels
}
#[must_use]
pub fn index_bucket(lang: &str, s: &str) -> alloc::string::String {
use alloc::string::ToString;
let tail = Tailoring::for_locale(lang).unwrap_or_else(Tailoring::identity);
let sp = first_primary(&tail, s);
if sp == 0 {
return "#".to_string(); }
let labels = index_labels(lang);
let mut chosen: Option<usize> = None;
for (i, label) in labels.iter().enumerate() {
if first_primary(&tail, label) <= sp {
chosen = Some(i);
} else {
break;
}
}
match chosen {
Some(i) if i == labels.len() - 1 && first_primary(&tail, &labels[i]) < sp => {
"#".to_string()
}
Some(i) => labels[i].clone(),
None => "#".to_string(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Strength {
Primary,
Secondary,
Tertiary,
Quaternary,
}
fn build_sort_key(cea: &[u64], alternate: AlternateHandling, strength: Strength) -> Vec<u16> {
let mut key = Vec::new();
match alternate {
AlternateHandling::NonIgnorable => {
for &ce in cea {
let p = primary(ce);
if p != 0 {
key.push(p);
}
}
if strength == Strength::Primary {
return key;
}
key.push(0);
for &ce in cea {
let s = secondary(ce);
if s != 0 {
key.push(s);
}
}
if strength == Strength::Secondary {
return key;
}
key.push(0);
for &ce in cea {
let t = tertiary(ce);
if t != 0 {
key.push(t);
}
}
}
AlternateHandling::Shifted => {
let mut rows: Vec<(u16, u16, u16, u16)> = Vec::with_capacity(cea.len());
let mut after_variable = false;
for &ce in cea {
let (p, s, t) = (primary(ce), secondary(ce), tertiary(ce));
if is_variable(ce) && p != 0 {
rows.push((0, 0, 0, p)); after_variable = true;
} else if p == 0 && s == 0 && t == 0 {
rows.push((0, 0, 0, 0)); } else if p == 0 {
if after_variable {
rows.push((0, 0, 0, 0));
} else {
rows.push((0, s, t, 0xFFFF));
}
} else {
rows.push((p, s, t, 0xFFFF));
after_variable = false;
}
}
for &(p, ..) in &rows {
if p != 0 {
key.push(p);
}
}
if strength == Strength::Primary {
return key;
}
key.push(0);
for &(_, s, ..) in &rows {
if s != 0 {
key.push(s);
}
}
if strength == Strength::Secondary {
return key;
}
key.push(0);
for &(_, _, t, _) in &rows {
if t != 0 {
key.push(t);
}
}
if strength == Strength::Tertiary {
return key;
}
key.push(0);
for &(.., q) in &rows {
if q != 0 {
key.push(q);
}
}
}
}
key
}
#[derive(Debug, Clone, Copy)]
pub struct Collator {
alternate: AlternateHandling,
strength: Strength,
numeric: bool,
}
impl Default for Collator {
fn default() -> Self {
Collator {
alternate: AlternateHandling::Shifted,
strength: Strength::Tertiary,
numeric: false,
}
}
}
impl Collator {
#[must_use]
pub fn new(alternate: AlternateHandling) -> Self {
Collator {
alternate,
strength: Strength::Tertiary,
numeric: false,
}
}
#[must_use]
pub fn with_strength(mut self, strength: Strength) -> Self {
self.strength = strength;
self
}
#[must_use]
pub fn with_numeric(mut self, numeric: bool) -> Self {
self.numeric = numeric;
self
}
#[must_use]
pub fn sort_key(&self, s: &str) -> Vec<u16> {
let cv: Vec<char> = nfd(s.chars()).collect();
let cea = if self.numeric {
collation_elements_numeric(cv)
} else {
collation_elements(cv)
};
build_sort_key(&cea, self.alternate, self.strength)
}
#[must_use]
pub fn compare(&self, a: &str, b: &str) -> Ordering {
self.sort_key(a).cmp(&self.sort_key(b))
}
}
#[must_use]
pub fn compare(a: &str, b: &str) -> Ordering {
Collator::default().compare(a, b)
}
#[must_use]
pub fn sort_key(s: &str) -> Vec<u16> {
Collator::default().sort_key(s)
}
pub struct Tailoring {
entries: Vec<(Vec<char>, Vec<u64>)>,
reorder: Option<Reorder>,
#[cfg(feature = "collation-zh")]
han: Option<&'static [u8]>,
#[cfg(feature = "collation-zh")]
han_unihan: bool,
}
impl Tailoring {
#[must_use]
pub fn for_locale(lang: &str) -> Option<Tailoring> {
let full = lang.replace('_', "-").to_ascii_lowercase();
let primary = full.split('-').next().unwrap_or(&full);
#[cfg(feature = "collation-zh")]
if primary == "zh" {
if full.find("-u-co-").map(|i| &full[i + 6..]) == Some("unihan") {
return Some(Tailoring::zh_unihan());
}
let table = match full.find("-u-co-").map(|i| &full[i + 6..]) {
Some("stroke") => ZH_STROKE,
Some("zhuyin") => ZH_ZHUYIN,
_ => ZH_PINYIN,
};
return Some(Tailoring::zh(table));
}
let inherited = match primary {
"nb" | "nn" => Some("no"),
"tl" => Some("fil"),
_ => None,
};
for key in [Some(full.as_str()), Some(primary), inherited]
.into_iter()
.flatten()
{
if let Some(rule) = crate::cldr::collation_rule(key)
&& let Some(t) = Tailoring::parse(rule)
{
return Some(t);
}
}
let rules = match primary {
"da" | "nb" | "nn" => "&z < æ < ø < å", "fo" => "&a < á &d < ð &i < í &o < ó &u < ú &y < ý &z < æ < ø", "lt" => "&c < č &s < š &z < ž", "vi" => "&a < ă < â &d < đ &e < ê &o < ô < ơ &u < ư", "hr" => "&c < č < ć &d < dž < đ &l < lj &n < nj &s < š &z < ž",
"hu" => {
"&c < cs &d < dz < dzs &g < gy &l < ly &n < ny &s < sz &t < ty &z < zs \
&O < ö <<< Ö << ő <<< Ő &U < ü <<< Ü << ű <<< Ű \
&cs <<< ccs / cs &dz <<< ddz / dz &dzs <<< ddzs / dzs &gy <<< ggy / gy \
&ly <<< lly / ly &ny <<< nny / ny &sz <<< ssz / sz &ty <<< tty / ty \
&zs <<< zzs / zs"
}
"ro" => "&a < ă < â &i < î &s < ș &t < ț", "ee" => {
"&D<dz<<<Dz<<<DZ<ɖ<<<Ɖ &E<ɛ<<<Ɛ &F<ƒ<<<Ƒ &G<gb<<<Gb<<<GB<ɣ<<<Ɣ &H<x<<<X \
&K<kp<<<Kp<<<KP &N<ny<<<Ny<<<NY<ŋ<<<Ŋ &O<ɔ<<<Ɔ &T<ts<<<Ts<<<TS &V<ʋ<<<Ʋ"
}
"af" => "&n<<<ʼn", "ja" => {
"&ぁ=ァ&あ=ア&ぃ=ィ&い=イ&ぅ=ゥ&う=ウ&ぇ=ェ&え=エ&ぉ=ォ&お=オ&か=カ&が=ガ&き=キ&ぎ=ギ\
&く=ク&ぐ=グ&け=ケ&げ=ゲ&こ=コ&ご=ゴ&さ=サ&ざ=ザ&し=シ&じ=ジ&す=ス&ず=ズ&せ=セ&ぜ=ゼ\
&そ=ソ&ぞ=ゾ&た=タ&だ=ダ&ち=チ&ぢ=ヂ&っ=ッ&つ=ツ&づ=ヅ&て=テ&で=デ&と=ト&ど=ド&な=ナ\
&に=ニ&ぬ=ヌ&ね=ネ&の=ノ&は=ハ&ば=バ&ぱ=パ&ひ=ヒ&び=ビ&ぴ=ピ&ふ=フ&ぶ=ブ&ぷ=プ&へ=ヘ\
&べ=ベ&ぺ=ペ&ほ=ホ&ぼ=ボ&ぽ=ポ&ま=マ&み=ミ&む=ム&め=メ&も=モ&ゃ=ャ&や=ヤ&ゅ=ュ&ゆ=ユ\
&ょ=ョ&よ=ヨ&ら=ラ&り=リ&る=ル&れ=レ&ろ=ロ&ゎ=ヮ&わ=ワ&ゐ=ヰ&ゑ=ヱ&を=ヲ&ん=ン&ゔ=ヴ\
&ゕ=ヵ&ゖ=ヶ"
}
_ => return None,
};
Tailoring::parse(rules)
}
#[must_use]
pub fn parse(rules: &str) -> Option<Tailoring> {
let mut entries: Vec<(Vec<char>, Vec<u64>)> = Vec::new();
let mut reorder_codes: Vec<ReorderCode> = Vec::new();
Self::parse_into(rules, &mut entries, &mut reorder_codes, 0)?;
let reorder = Reorder::build(&reorder_codes);
if entries.is_empty() && reorder.is_none() {
return None;
}
entries.sort_by_key(|e| core::cmp::Reverse(e.0.len()));
Some(Tailoring {
entries,
reorder,
#[cfg(feature = "collation-zh")]
han: None,
#[cfg(feature = "collation-zh")]
han_unihan: false,
})
}
fn parse_into(
rules: &str,
entries: &mut Vec<(Vec<char>, Vec<u64>)>,
reorder_codes: &mut Vec<ReorderCode>,
depth: u32,
) -> Option<()> {
if depth > 8 {
return Some(()); }
let toks = lex(rules)?;
let mut anchor: Vec<char> = Vec::new();
let mut anchor_primary = 0u32;
let mut anchor_ces: Vec<u64> = Vec::new();
let mut anchor_ce: Option<u64> = None;
let mut before = false; let (mut p_off, mut s_off, mut t_off) = (0u32, 0u32, 0u32);
let mut i = 0;
while i < toks.len() {
match &toks[i] {
Tok::Amp => {
i += 1;
before = false;
if let Some(Tok::Before(lvl)) = toks.get(i) {
before = *lvl >= 1;
i += 1;
}
let mut a = Vec::new();
while let Some(Tok::Lit(c)) = toks.get(i) {
a.push(*c);
i += 1;
}
anchor = a;
let anchor_nfd: Vec<char> = nfd(anchor.iter().copied()).collect();
anchor_ces = collation_elements(anchor_nfd.clone());
anchor_primary = primary(*anchor_ces.first()?) as u32;
anchor_ce = entries.iter().find_map(|(seq, ces)| {
(seq == &anchor_nfd && ces.len() == 1).then_some(ces[0])
});
(p_off, s_off, t_off) = (0, 0, 0);
}
Tok::Rel(level) => {
let level = *level;
i += 1;
let star = matches!(toks.get(i), Some(Tok::Star));
if star {
i += 1;
}
let mut target = Vec::new();
while let Some(Tok::Lit(c)) = toks.get(i) {
target.push(*c);
i += 1;
}
let mut expansion: Vec<char> = Vec::new();
if matches!(toks.get(i), Some(Tok::Slash)) {
i += 1;
while let Some(Tok::Lit(c)) = toks.get(i) {
expansion.push(*c);
i += 1;
}
}
if target.is_empty() || anchor_primary == 0 {
return None;
}
let groups: Vec<Vec<char>> = if star {
target.iter().map(|&c| alloc::vec![c]).collect()
} else {
alloc::vec![target]
};
let n_groups = groups.len();
for (gi, g) in groups.into_iter().enumerate() {
let exp = if gi + 1 == n_groups {
expansion.as_slice()
} else {
&[]
};
Self::apply_relation(
entries,
level,
&g,
&anchor,
anchor_primary,
anchor_ce,
&anchor_ces,
before,
exp,
&mut p_off,
&mut s_off,
&mut t_off,
);
}
}
Tok::Import(loc) => {
if let Some(rule) = import_rule(loc) {
Self::parse_into(rule, entries, reorder_codes, depth + 1)?;
} else if depth < 8
&& let Some(t) = import_tailoring(loc)
{
entries.extend(t.entries);
}
i += 1;
anchor_primary = 0;
}
Tok::Reorder(codes) => {
for c in codes {
if let Some(rc) = ReorderCode::parse(c) {
reorder_codes.push(rc);
}
}
i += 1;
}
_ => i += 1,
}
}
Some(())
}
#[allow(clippy::too_many_arguments)]
fn apply_relation(
entries: &mut Vec<(Vec<char>, Vec<u64>)>,
level: u8,
target: &[char],
anchor: &[char],
anchor_primary: u32,
anchor_ce: Option<u64>,
anchor_ces: &[u64],
before: bool,
expansion: &[char],
p_off: &mut u32,
s_off: &mut u32,
t_off: &mut u32,
) {
if level == 0 {
Self::push_expansion(entries, target, anchor);
return;
}
match level {
1 => (*p_off, *s_off, *t_off) = (*p_off + 1, 0, 0),
2 => (*s_off, *t_off) = (*s_off + 1, 0),
_ => *t_off += 1,
}
let exp_ces = Self::resolve_expansion(entries, expansion);
if anchor_ce.is_none()
&& level >= 2
&& anchor_ces.iter().filter(|&&ce| primary(ce) != 0).count() >= 2
{
Self::push_anchor_expansion(
entries, target, anchor_ces, level, *s_off, *t_off, &exp_ces,
);
return;
}
if let (Some(ce), true) = (anchor_ce, level >= 2) {
Self::push_variant(entries, target, ce, *s_off, *t_off, &exp_ces);
return;
}
let region = if before { SUB_BEFORE } else { SUB_MID };
let sub = region as u32 + *p_off;
Self::push_letter(
entries,
target,
anchor_primary,
sub,
*s_off,
*t_off,
&exp_ces,
);
}
fn resolve_expansion(entries: &[(Vec<char>, Vec<u64>)], seq: &[char]) -> Vec<u64> {
let nfd_seq: Vec<char> = nfd(seq.iter().copied()).collect();
let mut out = Vec::new();
let mut i = 0;
while i < nfd_seq.len() {
let mut best: Option<&Vec<u64>> = None;
let mut best_len = 0;
for (s, ces) in entries {
if !s.is_empty()
&& s.len() > best_len
&& nfd_seq[i..].len() >= s.len()
&& nfd_seq[i..i + s.len()] == s[..]
{
best = Some(ces);
best_len = s.len();
}
}
if let Some(ces) = best {
out.extend_from_slice(ces);
i += best_len;
} else {
out.extend(collation_elements(alloc::vec![nfd_seq[i]]));
i += 1;
}
}
out
}
fn case_variants(target: &[char]) -> Vec<(Vec<char>, u32)> {
let upper_all: Vec<char> = target.iter().map(|&c| upper(c)).collect();
let mut v = alloc::vec![(target.to_vec(), 0x0002u32), (upper_all, 0x0008u32)];
if target.len() > 1 {
let mut title = target.to_vec();
title[0] = upper(target[0]);
v.push((title, 0x0008));
}
v
}
fn push_letter(
entries: &mut Vec<(Vec<char>, Vec<u64>)>,
target: &[char],
base: u32,
sub: u32,
s_off: u32,
t_off: u32,
exp_ces: &[u64],
) {
for (form, case_t) in Self::case_variants(target) {
let all_upper = form.iter().all(|c| !c.is_lowercase());
let seq: Vec<char> = nfd(form.into_iter()).collect();
if !seq.is_empty() {
let mut ces = alloc::vec![pack_tailored(base, sub, 0x0020 + s_off, case_t + t_off)];
ces.extend(Self::cased_expansion(exp_ces, all_upper));
entries.push((seq, ces));
}
}
}
fn push_variant(
entries: &mut Vec<(Vec<char>, Vec<u64>)>,
target: &[char],
anchor_ce: u64,
s_off: u32,
t_off: u32,
exp_ces: &[u64],
) {
let base = primary(anchor_ce) as u32;
let sub = sub_weight(anchor_ce) as u32;
let sec = secondary(anchor_ce) as u32 + s_off;
for (form, case_t) in Self::case_variants(target) {
let all_upper = form.iter().all(|c| !c.is_lowercase());
let seq: Vec<char> = nfd(form.into_iter()).collect();
if !seq.is_empty() {
let mut ces = alloc::vec![pack_tailored(base, sub, sec, case_t + t_off)];
ces.extend(Self::cased_expansion(exp_ces, all_upper));
entries.push((seq, ces));
}
}
}
fn push_anchor_expansion(
entries: &mut Vec<(Vec<char>, Vec<u64>)>,
target: &[char],
anchor_ces: &[u64],
level: u8,
s_off: u32,
t_off: u32,
exp_ces: &[u64],
) {
let seq: Vec<char> = nfd(target.iter().copied()).collect();
if seq.is_empty() {
return;
}
let mut ces = anchor_ces.to_vec();
let dist = if level == 2 {
pack(0, 0x0020 + s_off, 0x0002)
} else {
pack(0, 0x0020, 0x0002 + t_off)
};
ces.push(dist);
ces.extend_from_slice(exp_ces);
entries.push((seq, ces));
}
fn cased_expansion(exp_ces: &[u64], all_upper: bool) -> Vec<u64> {
if !all_upper {
return exp_ces.to_vec();
}
exp_ces
.iter()
.map(|&ce| {
if tertiary(ce) == 0x0002 {
(ce & !0xFFFF) | 0x0008
} else {
ce
}
})
.collect()
}
fn push_expansion(entries: &mut Vec<(Vec<char>, Vec<u64>)>, target: &[char], anchor: &[char]) {
let forms = [
(target.to_vec(), anchor.to_vec()),
(
target.iter().map(|&c| upper(c)).collect::<Vec<_>>(),
anchor.iter().map(|&c| upper(c)).collect::<Vec<_>>(),
),
];
for (t_form, a_form) in forms {
let seq: Vec<char> = nfd(t_form.into_iter()).collect();
let ces = collation_elements(nfd(a_form.into_iter()).collect());
if !seq.is_empty() && !ces.is_empty() {
entries.push((seq, ces));
}
}
}
fn match_at(&self, rest: &[char]) -> Option<(usize, &[u64])> {
for (seq, ces) in &self.entries {
if rest.len() >= seq.len() && rest[..seq.len()] == seq[..] {
return Some((seq.len(), ces));
}
}
None
}
#[must_use]
pub fn sort_key(&self, s: &str) -> Vec<u16> {
let cv: Vec<char> = nfd(s.chars()).collect();
let mut cea = Vec::new();
let mut buf: Vec<char> = Vec::new();
let mut i = 0;
while i < cv.len() {
#[cfg(feature = "collation-zh")]
if let Some(table) = self.han
&& self.match_at(&cv[i..]).is_none()
{
let cp = cv[i] as u32;
let ranked = if self.han_unihan {
None
} else {
zh_ranked(table, cp)
};
if let Some(rank) = ranked {
if !buf.is_empty() {
cea.extend(collation_elements(core::mem::take(&mut buf)));
}
cea.push(pack(ZH_HAN_BASE, 0x0020, 0x0002));
cea.push(pack(rank as u32, 0x0000, 0x0000));
i += 1;
continue;
} else if let Some(packed) = zh_rs_key(cp) {
if !buf.is_empty() {
cea.extend(collation_elements(core::mem::take(&mut buf)));
}
let radical_key = (packed >> 8) as u32;
let resid = (packed & 0xFF) as u32;
cea.push(pack(ZH_HAN_BASE, 0x0020, 0x0002));
cea.push(pack(ZH_RS_MARKER, 0x0000, 0x0000));
cea.push(pack(radical_key, 0x0000, 0x0000));
cea.push(pack(resid, 0x0000, 0x0000));
let (aaaa, bbbb) = implicit_primaries(cp);
cea.push(pack(aaaa, 0x0000, 0x0000));
cea.push(pack(bbbb, 0x0000, 0x0000));
i += 1;
continue;
} else if tables::unified_ideograph(cp) {
if !buf.is_empty() {
cea.extend(collation_elements(core::mem::take(&mut buf)));
}
let (aaaa, bbbb) = implicit_primaries(cp);
cea.push(pack(ZH_HAN_BASE, 0x0020, 0x0002));
cea.push(pack(aaaa, 0x0000, 0x0000));
cea.push(pack(bbbb, 0x0000, 0x0000));
i += 1;
continue;
}
}
if let Some((len, ces)) = self.match_at(&cv[i..]) {
if !buf.is_empty() {
cea.extend(collation_elements(core::mem::take(&mut buf)));
}
cea.extend_from_slice(ces);
i += len;
} else {
buf.push(cv[i]);
i += 1;
}
}
if !buf.is_empty() {
cea.extend(collation_elements(buf));
}
build_tailored_sort_key(&cea, self.reorder.as_ref())
}
#[must_use]
pub fn compare(&self, a: &str, b: &str) -> Ordering {
self.sort_key(a).cmp(&self.sort_key(b))
}
#[must_use]
pub fn identity() -> Tailoring {
Tailoring {
entries: Vec::new(),
reorder: None,
#[cfg(feature = "collation-zh")]
han: None,
#[cfg(feature = "collation-zh")]
han_unihan: false,
}
}
#[cfg(feature = "collation-zh")]
fn zh(table: &'static [u8]) -> Tailoring {
const TONES: &str = "\
&[before 2]a<<ā<<<Ā<<á<<<Á<<ǎ<<<Ǎ<<à<<<À \
&[before 2]e<<ē<<<Ē<<é<<<É<<ě<<<Ě<<è<<<È \
&[before 2]i<<ī<<<Ī<<í<<<Í<<ǐ<<<Ǐ<<ì<<<Ì \
&[before 2]o<<ō<<<Ō<<ó<<<Ó<<ǒ<<<Ǒ<<ò<<<Ò \
&[before 2]u<<ū<<<Ū<<ú<<<Ú<<ǔ<<<Ǔ<<ù<<<Ù \
&U<<ǖ<<<Ǖ<<ǘ<<<Ǘ<<ǚ<<<Ǚ<<ǜ<<<Ǜ<<ü<<<Ü";
let mut t = Tailoring::parse(TONES).unwrap_or_else(Tailoring::identity);
t.han = Some(table);
t
}
#[cfg(feature = "collation-zh")]
fn zh_unihan() -> Tailoring {
let mut t = Tailoring::zh(ZH_PINYIN);
t.han_unihan = true;
t
}
}
fn build_tailored_sort_key(cea: &[u64], reorder: Option<&Reorder>) -> Vec<u16> {
let mut rows: Vec<(u16, u16, u16, u16, u16)> = Vec::with_capacity(cea.len());
let mut after_variable = false;
for &ce in cea {
let (p, sub, s, t) = (primary(ce), sub_weight(ce), secondary(ce), tertiary(ce));
if is_variable(ce) && p != 0 {
rows.push((0, 0, 0, 0, p)); after_variable = true;
} else if p == 0 && s == 0 && t == 0 {
rows.push((0, 0, 0, 0, 0)); } else if p == 0 {
if after_variable {
rows.push((0, 0, 0, 0, 0));
} else {
rows.push((0, 0, s, t, 0xFFFF));
}
} else {
rows.push((p, sub, s, t, 0xFFFF));
after_variable = false;
}
}
let mut key = Vec::new();
for &(p, sub, ..) in &rows {
if p != 0 {
if let Some(r) = reorder {
key.push(r.rank(p));
}
key.push(p);
key.push(if sub == 0 { SUB_MID } else { sub });
}
}
key.push(0);
for &(_, _, s, ..) in &rows {
if s != 0 {
key.push(s);
}
}
key.push(0);
for &(_, _, _, t, _) in &rows {
if t != 0 {
key.push(t);
}
}
key.push(0);
for &(.., q) in &rows {
if q != 0 {
key.push(q);
}
}
key
}
enum ReorderCode {
Script(super::script::Script),
Others,
Special,
}
impl ReorderCode {
fn parse(tag: &str) -> Option<ReorderCode> {
match tag.to_ascii_lowercase().as_str() {
"space" | "punct" | "symbol" | "currency" | "digit" => Some(ReorderCode::Special),
"others" | "zzzz" => Some(ReorderCode::Others),
_ => script_from_tag(tag).map(ReorderCode::Script),
}
}
}
fn script_from_tag(tag: &str) -> Option<super::script::Script> {
use super::script::Script::*;
let mut norm = String::new();
for (i, c) in tag.chars().enumerate() {
if i == 0 {
norm.push(c.to_ascii_uppercase());
} else {
norm.push(c.to_ascii_lowercase());
}
}
Some(match norm.as_str() {
"Cyrl" => Cyrillic,
"Latn" => Latin,
"Grek" => Greek,
"Arab" => Arabic,
"Hani" => Han,
"Hebr" => Hebrew,
"Armn" => Armenian,
"Geor" => Georgian,
"Ethi" => Ethiopic,
"Cher" => Cherokee,
"Deva" => Devanagari,
"Beng" => Bengali,
"Guru" => Gurmukhi,
"Gujr" => Gujarati,
"Orya" => Oriya,
"Taml" => Tamil,
"Telu" => Telugu,
"Knda" => Kannada,
"Mlym" => Malayalam,
"Sinh" => Sinhala,
"Tibt" => Tibetan,
"Thai" => Thai,
"Laoo" => Lao,
"Khmr" => Khmer,
"Mymr" => Myanmar,
"Hang" => Hangul,
"Bopo" => Bopomofo,
"Kana" => Katakana,
"Mong" => Mongolian,
_ => return None,
})
}
fn char_primary(cp: u32) -> u16 {
if let Some(ces) = tables::ce_singles(cp) {
for &ce in ces {
let p = primary(ce);
if p != 0 {
return p;
}
}
return 0;
}
let Some(c) = char::from_u32(cp) else {
return 0;
};
for ce in collation_elements(nfd(core::iter::once(c)).collect()) {
let p = primary(ce);
if p != 0 {
return p;
}
}
0
}
struct Reorder {
script_start: u16,
groups: Vec<(u16, u16, u16)>,
others_rank: u16,
}
impl Reorder {
fn build(codes: &[ReorderCode]) -> Option<Reorder> {
let mut scripts: Vec<(super::script::Script, u16)> = Vec::new();
let mut idx = 1u16;
let mut others_rank: Option<u16> = None;
for c in codes {
match c {
ReorderCode::Script(s) => {
scripts.push((*s, idx));
idx += 1;
}
ReorderCode::Others => {
if others_rank.is_none() {
others_rank = Some(idx);
idx += 1;
}
}
ReorderCode::Special => {}
}
}
if scripts.is_empty() {
return None;
}
let others_rank = others_rank.unwrap_or(idx);
let script_start = char_primary('a' as u32);
let mut groups: Vec<(u16, u16, u16)> =
scripts.iter().map(|&(_, r)| (u16::MAX, 0u16, r)).collect();
for cp in 0u32..0x1_0000 {
let sc = super::script::script_u32(cp);
let Some(pos) = scripts.iter().position(|&(s, _)| s == sc) else {
continue;
};
let pr = char_primary(cp);
if pr < script_start {
continue;
}
let g = &mut groups[pos];
if pr < g.0 {
g.0 = pr;
}
if pr > g.1 {
g.1 = pr;
}
}
groups.retain(|&(lo, hi, _)| lo <= hi);
if groups.is_empty() {
return None;
}
Some(Reorder {
script_start,
groups,
others_rank,
})
}
fn rank(&self, p: u16) -> u16 {
if p < self.script_start {
return 0;
}
for &(lo, hi, r) in &self.groups {
if p >= lo && p <= hi {
return r;
}
}
self.others_rank
}
}
fn upper(c: char) -> char {
super::case::to_uppercase(c).next().unwrap_or(c)
}
enum Tok {
Amp,
Rel(u8),
Star,
Slash,
Lit(char),
Before(u8),
Import(String),
Reorder(Vec<String>),
}
fn parse_hex(chars: &[char], start: usize, n: usize) -> Option<u32> {
if start + n > chars.len() {
return None;
}
let s: String = chars[start..start + n].iter().collect();
u32::from_str_radix(&s, 16).ok()
}
fn lex(rules: &str) -> Option<Vec<Tok>> {
let chars: Vec<char> = rules.chars().collect();
let mut out = Vec::new();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
match c {
'#' => {
while i < chars.len() && chars[i] != '\n' {
i += 1;
}
}
_ if c.is_whitespace() => i += 1,
'/' => {
out.push(Tok::Slash);
i += 1;
}
'|' => i += 1,
'\'' => {
i += 1;
if i < chars.len() && chars[i] == '\'' {
out.push(Tok::Lit('\'')); i += 1;
} else {
while i < chars.len() && chars[i] != '\'' {
out.push(Tok::Lit(chars[i]));
i += 1;
}
if i < chars.len() {
i += 1; }
}
}
'\\' => {
i += 1;
let Some(&e) = chars.get(i) else { break };
match e {
'u' => {
out.push(Tok::Lit(char::from_u32(parse_hex(&chars, i + 1, 4)?)?));
i += 5;
}
'U' => {
out.push(Tok::Lit(char::from_u32(parse_hex(&chars, i + 1, 8)?)?));
i += 9;
}
other => {
out.push(Tok::Lit(other)); i += 1;
}
}
}
'[' => {
let start = i + 1;
let mut depth = 1;
i += 1;
while i < chars.len() && depth > 0 {
match chars[i] {
'[' => depth += 1,
']' => depth -= 1,
_ => {}
}
if depth > 0 {
i += 1;
}
}
let content: String = chars[start..i].iter().collect();
if i < chars.len() {
i += 1; }
let content = content.trim();
if let Some(rest) = content.strip_prefix("before") {
let lvl = rest
.trim()
.chars()
.next()
.and_then(|d| d.to_digit(10))
.unwrap_or(1) as u8;
out.push(Tok::Before(lvl.clamp(1, 3)));
} else if let Some(rest) = content.strip_prefix("import") {
out.push(Tok::Import(rest.trim().to_string()));
} else if let Some(rest) = content.strip_prefix("reorder") {
let codes: Vec<String> =
rest.split_whitespace().map(ToString::to_string).collect();
if !codes.is_empty() {
out.push(Tok::Reorder(codes));
}
}
}
'&' => {
out.push(Tok::Amp);
i += 1;
}
'=' => {
out.push(Tok::Rel(0));
i += 1;
if i < chars.len() && chars[i] == '*' {
out.push(Tok::Star);
i += 1;
}
}
'<' => {
let mut n = 0u8;
while i < chars.len() && chars[i] == '<' {
n = n.saturating_add(1);
i += 1;
}
out.push(Tok::Rel(n.min(3)));
if i < chars.len() && chars[i] == '*' {
out.push(Tok::Star);
i += 1;
}
}
other => {
out.push(Tok::Lit(other));
i += 1;
}
}
}
Some(out)
}
fn import_rule(loc: &str) -> Option<&'static str> {
let full = loc.replace('_', "-").to_ascii_lowercase();
if let Some(idx) = full.find("-u-co-") {
if &full[idx + 6..] != "standard" {
return None;
}
return crate::cldr::collation_rule(&full[..idx]);
}
crate::cldr::collation_rule(&full)
}
fn import_tailoring(loc: &str) -> Option<Tailoring> {
let full = loc.replace('_', "-").to_ascii_lowercase();
let base = match full.find("-u-co-") {
Some(idx) if &full[idx + 6..] == "standard" => full[..idx].to_string(),
Some(_) => return None,
None => full,
};
Tailoring::for_locale(&base)
}
#[cfg(test)]
mod dos_fix_tests {
use super::*;
use alloc::string::String;
fn find_reference(text: &str, pattern: &str) -> Option<core::ops::Range<usize>> {
let pat = primaries(pattern);
if pat.is_empty() {
return Some(0..0);
}
let bounds: Vec<usize> = text
.char_indices()
.map(|(i, _)| i)
.chain(core::iter::once(text.len()))
.collect();
for a in 0..bounds.len() - 1 {
for b in a + 1..bounds.len() {
let pr = primaries(&text[bounds[a]..bounds[b]]);
if pr.len() < pat.len() {
continue;
}
if pr == pat {
return Some(bounds[a]..bounds[b]);
}
break;
}
}
None
}
fn collation_elements_reference(mut cv: Vec<char>) -> Vec<u64> {
let mut cea = Vec::new();
let mut i = 0;
while i < cv.len() {
let s0 = cv[i] as u32;
let mut end = i + 1;
let mut matched: Option<&'static [u64]> = tables::ce_singles(s0);
let mut suffix: Vec<char> = Vec::new();
if let Some(entries) = tables::contractions(s0) {
for (suf, ces) in entries {
let stop = i + 1 + suf.len();
if stop <= cv.len() && cv[i + 1..stop] == **suf {
matched = Some(ces);
suffix = suf.to_vec();
end = stop;
break;
}
}
}
loop {
let mut last_ccc = 0u8;
let mut j = end;
let mut hit = None;
while j < cv.len() {
let cc = ccc(cv[j]);
if cc == 0 {
break;
}
if last_ccc < cc {
let mut trial = suffix.clone();
trial.push(cv[j]);
if let Some(ces) = lookup_contraction(s0, &trial) {
hit = Some((j, ces, trial));
break;
}
last_ccc = cc;
} else {
break;
}
j += 1;
}
match hit {
Some((j, ces, trial)) => {
matched = Some(ces);
suffix = trial;
cv.remove(j);
}
None => break,
}
}
match matched {
Some(ces) => cea.extend_from_slice(ces),
None => push_implicit(s0, &mut cea),
}
i = end;
}
cea
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn pick<'a, T>(&mut self, xs: &'a [T]) -> &'a T {
&xs[(self.next() as usize) % xs.len()]
}
}
const ALPHABET: &[char] = &[
'a', 'b', 'c', 'e', 'h', 'l', 'z', 'A', 'C', 'H', 'L', 'ñ', 'Ñ', 'å', 'Ç', 'ç', '·',
'\u{0301}', '\u{0300}', '\u{0327}', '\u{0323}', '\u{0308}', 'é', 'É', '0', '1', '2', '中',
' ', '!', '\u{00C6}', '\u{0153}',
];
fn random_string(rng: &mut Rng, len: usize) -> String {
(0..len).map(|_| *rng.pick(ALPHABET)).collect()
}
#[test]
fn find_matches_reference_fuzz() {
let mut rng = Rng(0x9E3779B97F4A7C15);
for _ in 0..4000 {
let tlen = (rng.next() as usize) % 14;
let plen = 1 + (rng.next() as usize) % 4;
let text = random_string(&mut rng, tlen);
let pat = random_string(&mut rng, plen);
assert_eq!(
find(&text, &pat),
find_reference(&text, &pat),
"find mismatch: text={text:?} pat={pat:?}"
);
}
for (t, p) in [
("l·a", "la"),
("L·", "l"),
("e\u{0301}", "e"),
("\u{0301}e", "e"),
(" café", "cafe"),
("ñ", "n"),
("中文", "中"),
("aaa", "aa"),
("", "x"),
("x", ""),
] {
assert_eq!(
find(t, p),
find_reference(t, p),
"case text={t:?} pat={p:?}"
);
}
}
#[test]
fn collation_elements_matches_reference_fuzz() {
let mut rng = Rng(0xD1B54A32D192ED03);
for _ in 0..4000 {
let len = (rng.next() as usize) % 16;
let s = random_string(&mut rng, len);
let cv: Vec<char> = nfd(s.chars()).collect();
assert_eq!(
collation_elements(cv.clone()),
collation_elements_reference(cv),
"CE mismatch: s={s:?}"
);
}
}
#[test]
fn perf_smoke_large_nonmatching_find() {
let text: String = "a".repeat(300_000);
assert_eq!(find(&text, "qzx"), None);
assert_eq!(find(&text, "aaa"), Some(0..3));
}
#[test]
fn perf_smoke_long_combining_run() {
let mut s = String::from("e");
for _ in 0..200_000 {
s.push('\u{0301}');
}
let key = sort_key(&s);
assert!(!key.is_empty());
assert_eq!(find(&s, "z"), None);
}
#[test]
fn perf_smoke_unaligned_start_zero_primary_tail() {
let big: String = {
let mut s = String::from("l\u{00B7}");
for _ in 0..50_000 {
s.push('\u{0301}');
}
s
};
let small = "l\u{00B7}\u{0301}";
assert_eq!(find(&big, "zz"), None);
assert_eq!(find(small, "zz"), find_reference(small, "zz"));
assert_eq!(find(&big, "zz"), find_reference(small, "zz"));
assert_eq!(find(&big, "l"), Some(0..1));
}
#[test]
fn perf_smoke_repeated_contraction_nonmatching() {
let n = 50_000;
let big: String = "l\u{00B7}".repeat(n); assert_eq!(find(&big, "zz"), None);
assert!(!contains(&big, "zz"));
let small = "l\u{00B7}l\u{00B7}l\u{00B7}";
assert_eq!(find(small, "zz"), find_reference(small, "zz"));
assert_eq!(find(&big, "zz"), find_reference(small, "zz"));
assert_eq!(find(&big, "l"), Some(0..1));
assert!(contains(&big, "l"));
}
}