use super::fold_table::{FOLD_BLOB, FOLD_KEYS, FOLD_OFFSETS, UNICODE_VERSION};
pub(crate) const FOLDING_TABLE_VERSION: u16 = 2;
pub(crate) const FOLDING_UNICODE_VERSION: &str = UNICODE_VERSION;
const PUNCTUATION_EQUIVALENTS: [(char, char); 8] = [
('\u{2018}', '\''), ('\u{2019}', '\''), ('\u{02BC}', '\''), ('\u{FF07}', '\''), ('\u{2010}', '-'), ('\u{2011}', '-'), ('\u{FE63}', '-'), ('\u{FF0D}', '-'), ];
pub(crate) fn punctuation_equivalent(value: char) -> char {
let mut index = 0;
while index < PUNCTUATION_EQUIVALENTS.len() {
match PUNCTUATION_EQUIVALENTS.get(index) {
Some((from, to)) if *from == value => return *to,
_ => index += 1,
}
}
value
}
fn table_fold(value: char) -> Option<&'static str> {
let code = value as u32;
let position = FOLD_KEYS.binary_search(&code).ok()?;
let start = usize::try_from(*FOLD_OFFSETS.get(position)?).ok()?;
let end = usize::try_from(*FOLD_OFFSETS.get(position + 1)?).ok()?;
FOLD_BLOB.get(start..end)
}
pub(crate) fn fold_char(value: char, output: &mut String) {
let value = punctuation_equivalent(value);
if value.is_ascii() {
output.push(value.to_ascii_lowercase());
return;
}
match table_fold(value) {
Some(folded) => output.push_str(folded),
None => output.push(value),
}
}
#[must_use]
pub(crate) fn fold(input: &str) -> String {
let mut output = String::with_capacity(input.len());
for value in input.chars() {
fold_char(value, &mut output);
}
output
}
pub(crate) fn segmentation_equivalent(value: char) -> char {
let value = punctuation_equivalent(value);
if value.is_ascii() {
return value;
}
match table_fold(value) {
Some(folded) => {
let mut characters = folded.chars();
match (characters.next(), characters.next()) {
(Some(single), None) => single,
_ => value,
}
}
None => value,
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn case_folds_to_lowercase() {
assert_eq!(fold("HELLO World"), "hello world");
assert_eq!(fold("\u{0410}\u{0411}"), "\u{0430}\u{0431}");
}
#[test]
fn diacritics_fold_to_their_ascii_base() {
assert_eq!(fold("Café"), "cafe");
assert_eq!(fold("naïve"), "naive");
assert_eq!(fold("ÄÖÜ"), "aou");
assert_eq!(fold("straße"), "strasse");
assert_eq!(fold("Ångström"), "angstrom");
}
#[test]
fn decomposed_and_precomposed_diacritics_fold_alike() {
assert_eq!(fold("cafe\u{0301}"), fold("caf\u{00E9}"));
assert_eq!(fold("cafe\u{0301}"), "cafe");
}
#[test]
fn compatibility_forms_fold_to_ascii() {
assert_eq!(fold("\u{FB01}nd"), "find");
assert_eq!(fold("\u{FF21}\u{FF22}"), "ab");
assert_eq!(fold("x\u{00B2}"), "x2");
assert_eq!(fold("\u{2168}"), "ix");
}
#[test]
fn punctuation_equivalents_collapse_beyond_nfkc() {
assert_eq!(fold("don\u{2019}t"), fold("don't"));
assert_eq!(fold("i\u{2011}485"), fold("i-485"));
assert_eq!(segmentation_equivalent('\u{2019}'), '\'');
assert_eq!(segmentation_equivalent('\u{2011}'), '-');
}
#[test]
fn fullwidth_digits_stay_alphanumeric_for_segmentation() {
assert!(segmentation_equivalent('\u{FF15}').is_alphanumeric());
assert_eq!(fold("\u{FF15}"), "5");
}
#[test]
fn combining_marks_fold_away() {
assert_eq!(fold("\u{0301}"), "");
assert_eq!(fold("\u{0300}\u{0302}"), "");
}
#[test]
fn ideographs_are_left_alone() {
assert_eq!(fold("\u{4E2D}\u{6587}"), "\u{4E2D}\u{6587}");
}
#[test]
fn folding_is_idempotent() {
for sample in [
"Café",
"straße",
"\u{FB01}nd",
"HELLO",
"\u{FF21}\u{FF22}",
"don\u{2019}t",
"\u{4E2D}\u{6587}",
"cafe\u{0301}",
"\u{2168}",
] {
let once = fold(sample);
assert_eq!(fold(&once), once, "fold is not stable on {sample:?}");
}
}
#[test]
fn the_generated_table_is_sorted_and_addressable() {
assert!(
FOLD_KEYS
.windows(2)
.all(|pair| match (pair.first(), pair.get(1)) {
(Some(left), Some(right)) => left < right,
_ => true,
}),
"FOLD_KEYS must be strictly ascending for binary search"
);
assert_eq!(FOLD_OFFSETS.len(), FOLD_KEYS.len() + 1);
for index in 0..FOLD_KEYS.len() {
let key = *FOLD_KEYS.get(index).expect("in range");
let value = char::from_u32(key).expect("table keys are scalar values");
assert!(
table_fold(value).is_some(),
"entry {key:#X} is unaddressable"
);
}
}
#[test]
fn the_unicode_version_is_pinned() {
assert_eq!(FOLDING_UNICODE_VERSION, "14.0.0");
}
}