#![allow(clippy::unwrap_used, clippy::expect_used)]
use unicode_lang::{Form, normalize};
struct Vectors {
c: [String; 5],
}
fn parse_seq(field: &str) -> String {
field
.split_whitespace()
.map(|h| char::from_u32(u32::from_str_radix(h, 16).unwrap()).unwrap())
.collect()
}
fn check_record(v: &Vectors) {
let [c1, c2, c3, c4, c5] = &v.c;
for src in [c1, c2, c3] {
assert_eq!(&normalize(src, Form::Nfc), c2, "NFC({src:?})");
}
for src in [c4, c5] {
assert_eq!(&normalize(src, Form::Nfc), c4, "NFC({src:?})");
}
for src in [c1, c2, c3] {
assert_eq!(&normalize(src, Form::Nfd), c3, "NFD({src:?})");
}
for src in [c4, c5] {
assert_eq!(&normalize(src, Form::Nfd), c5, "NFD({src:?})");
}
for src in [c1, c2, c3, c4, c5] {
assert_eq!(&normalize(src, Form::Nfkc), c4, "NFKC({src:?})");
}
for src in [c1, c2, c3, c4, c5] {
assert_eq!(&normalize(src, Form::Nfkd), c5, "NFKD({src:?})");
}
}
#[test]
fn curated_cases() {
let raw = [
("00C0", "00C0", "0041 0300", "00C0", "0041 0300"),
(
"0044 0307 0323",
"1E0C 0307",
"0044 0323 0307",
"1E0C 0307",
"0044 0323 0307",
),
("AC00", "AC00", "1100 1161", "AC00", "1100 1161"),
("AC01", "AC01", "1100 1161 11A8", "AC01", "1100 1161 11A8"),
("FB01", "FB01", "FB01", "0066 0069", "0066 0069"),
("FF21", "FF21", "FF21", "0041", "0041"),
("2126", "03A9", "03A9", "03A9", "03A9"),
("00BD", "00BD", "00BD", "0031 2044 0032", "0031 2044 0032"),
(
"0041 0301 0323",
"1EA0 0301",
"0041 0323 0301",
"1EA0 0301",
"0041 0323 0301",
),
];
for (c1, c2, c3, c4, c5) in raw {
let v = Vectors {
c: [
parse_seq(c1),
parse_seq(c2),
parse_seq(c3),
parse_seq(c4),
parse_seq(c5),
],
};
check_record(&v);
}
}
#[test]
fn full_conformance_suite() {
let path = std::path::Path::new("dev/ucd/NormalizationTest.txt");
let Ok(text) = std::fs::read_to_string(path) else {
eprintln!("skipping full conformance: {} not present", path.display());
return;
};
let mut part1_singletons: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut in_part1 = false;
let mut records = 0usize;
for line in text.lines() {
if let Some(rest) = line.strip_prefix('@') {
in_part1 = rest.starts_with("Part1");
continue;
}
let data = line.split('#').next().unwrap_or("").trim();
if data.is_empty() {
continue;
}
let cols: Vec<&str> = data.split(';').collect();
if cols.len() < 5 {
continue;
}
let v = Vectors {
c: [
parse_seq(cols[0]),
parse_seq(cols[1]),
parse_seq(cols[2]),
parse_seq(cols[3]),
parse_seq(cols[4]),
],
};
if in_part1 {
let mut chars = v.c[0].chars();
if let (Some(only), None) = (chars.next(), chars.next()) {
let _ = part1_singletons.insert(only as u32);
}
}
check_record(&v);
records += 1;
}
assert!(records > 15_000, "unexpectedly few records: {records}");
for cp in 0u32..=0x10_FFFF {
if part1_singletons.contains(&cp) {
continue;
}
let Some(c) = char::from_u32(cp) else {
continue;
};
let s = c.to_string();
for form in [Form::Nfc, Form::Nfd, Form::Nfkc, Form::Nfkd] {
assert_eq!(normalize(&s, form), s, "identity {cp:#06X} {form:?}");
}
}
eprintln!("full conformance: {records} records + whole-codespace identity OK");
}