use caseless::default_case_fold_str;
use unicode_normalization::UnicodeNormalization;
pub const UNICODE_FOLD_V1_MAXIMUM_EXPANSION: usize = 18;
pub const UNICODE_FOLD_V1_UNICODE_VERSION: (u8, u8, u8) = (17, 0, 0);
#[must_use]
pub fn unicode_fold_v1(value: &str) -> String {
default_case_fold_str(&value.nfkc().collect::<String>())
}
#[cfg(test)]
mod tests {
use super::{
UNICODE_FOLD_V1_MAXIMUM_EXPANSION, UNICODE_FOLD_V1_UNICODE_VERSION, unicode_fold_v1,
};
#[test]
fn the_pinned_unicode_version_is_exact() {
assert_eq!(
unicode_normalization::UNICODE_VERSION,
UNICODE_FOLD_V1_UNICODE_VERSION,
"the linked Unicode tables moved; this is a new fold profile, not a bump"
);
}
#[test]
fn the_transform_is_idempotent() {
for probe in [
"Straße",
"file",
"İstanbul",
"ΣΟΦΟΣ",
"Ⅻ",
"ExP",
"café",
"cafe\u{0301}",
"",
"ß\u{FB03}Σ",
] {
let once = unicode_fold_v1(probe);
assert_eq!(unicode_fold_v1(&once), once, "not idempotent for {probe:?}");
}
}
#[test]
fn the_frozen_conformance_pairs_hold() {
for (input, expected) in [
("Straße", "strasse"),
("file", "file"),
("İstanbul", "i\u{307}stanbul"),
("ΣΟΦΟΣ", "σοφοσ"),
("Σοφος", "σοφοσ"),
("Ⅻ", "xii"),
("ExP", "exp"),
("café", "café"),
("cafe\u{0301}", "café"),
("plain ascii", "plain ascii"),
("", ""),
] {
assert_eq!(
unicode_fold_v1(input),
expected,
"fold pair failed for {input:?}"
);
}
}
#[test]
fn equivalent_spellings_fold_together() {
for (left, right) in [
("café", "cafe\u{0301}"),
("fi", "fi"),
("Ⅻ", "XII"),
("ß", "SS"),
("ΣΟΦΟΣ", "σοφος"),
] {
assert_eq!(
unicode_fold_v1(left),
unicode_fold_v1(right),
"{left:?} and {right:?} must fold together"
);
}
}
#[test]
fn the_expansion_bound_holds_for_every_scalar_value() {
use unicode_normalization::UnicodeNormalization;
let mut worst_ratio = 0.0_f64;
let mut worst_point = '\u{0}';
for point in (0..=0x10_FFFF_u32).filter_map(char::from_u32) {
let mut buffer = [0_u8; 4];
let source = point.encode_utf8(&mut buffer);
let bound = source.len() * UNICODE_FOLD_V1_MAXIMUM_EXPANSION;
assert!(source.nfkd().map(char::len_utf8).sum::<usize>() <= bound);
assert!(source.nfkc().map(char::len_utf8).sum::<usize>() <= bound);
let folded = unicode_fold_v1(source);
#[allow(clippy::cast_precision_loss)]
let ratio = folded.len() as f64 / source.len() as f64;
if ratio > worst_ratio {
worst_ratio = ratio;
worst_point = point;
}
}
#[allow(clippy::cast_precision_loss)]
let charged = UNICODE_FOLD_V1_MAXIMUM_EXPANSION as f64;
assert!(
worst_ratio <= charged,
"U+{:04X} expands {worst_ratio:.2}x, over the charged {charged:.0}x",
worst_point as u32
);
}
}