extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use crate::lookup::range_value;
use crate::tables;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Form {
Nfc,
Nfd,
Nfkc,
Nfkd,
}
impl Form {
#[inline]
const fn composes(self) -> bool {
matches!(self, Form::Nfc | Form::Nfkc)
}
#[inline]
const fn compat(self) -> bool {
matches!(self, Form::Nfkc | Form::Nfkd)
}
}
const S_BASE: u32 = 0xAC00;
const L_BASE: u32 = 0x1100;
const V_BASE: u32 = 0x1161;
const T_BASE: u32 = 0x11A7;
const L_COUNT: u32 = 19;
const V_COUNT: u32 = 21;
const T_COUNT: u32 = 28;
const N_COUNT: u32 = V_COUNT * T_COUNT; const S_COUNT: u32 = L_COUNT * N_COUNT;
#[must_use]
pub fn normalize(s: &str, form: Form) -> String {
if let Quick::Yes = quick_check(s, form) {
return String::from(s);
}
let mut buf = decompose(s, form.compat());
if form.composes() {
compose(&mut buf);
}
buf.into_iter().collect()
}
#[must_use]
pub fn is_normalized(s: &str, form: Form) -> bool {
match quick_check(s, form) {
Quick::Yes => true,
Quick::No => false,
Quick::Maybe => {
let mut buf = decompose(s, form.compat());
if form.composes() {
compose(&mut buf);
}
buf.into_iter().eq(s.chars())
}
}
}
enum Quick {
Yes,
No,
Maybe,
}
fn quick_check(s: &str, form: Form) -> Quick {
let qc = match form {
Form::Nfc => tables::NFC_QC,
Form::Nfd => tables::NFD_QC,
Form::Nfkc => tables::NFKC_QC,
Form::Nfkd => tables::NFKD_QC,
};
let mut last_ccc = 0u8;
let mut result = Quick::Yes;
for c in s.chars() {
let cc = ccc(c);
if last_ccc > cc && cc != 0 {
return Quick::No;
}
match range_value(c as u32, qc) {
1 => return Quick::No,
2 => result = Quick::Maybe,
_ => {}
}
last_ccc = cc;
}
result
}
#[inline]
fn ccc(c: char) -> u8 {
range_value(c as u32, tables::CCC)
}
fn decompose(s: &str, compat: bool) -> Vec<char> {
let mut out: Vec<char> = Vec::with_capacity(s.len());
for c in s.chars() {
decompose_char(c, compat, &mut out);
}
canonical_order(&mut out);
out
}
fn decompose_char(c: char, compat: bool, out: &mut Vec<char>) {
let cp = c as u32;
if (S_BASE..S_BASE + S_COUNT).contains(&cp) {
hangul_decompose(cp, out);
return;
}
let (index, data): (&[(u32, u32, u32)], &[u32]) = if compat {
(tables::COMPAT_DECOMP, tables::COMPAT_DATA)
} else {
(tables::CANON_DECOMP, tables::CANON_DATA)
};
if let Ok(i) = index.binary_search_by_key(&cp, |&(key, _, _)| key) {
let (_, off, len) = index[i];
let (off, len) = (off as usize, len as usize);
out.extend(
data[off..off + len]
.iter()
.filter_map(|&d| char::from_u32(d)),
);
} else {
out.push(c);
}
}
fn hangul_decompose(cp: u32, out: &mut Vec<char>) {
let s = cp - S_BASE;
push_scalar(out, L_BASE + s / N_COUNT);
push_scalar(out, V_BASE + (s % N_COUNT) / T_COUNT);
let t = s % T_COUNT;
if t != 0 {
push_scalar(out, T_BASE + t);
}
}
#[inline]
fn push_scalar(out: &mut Vec<char>, cp: u32) {
if let Some(c) = char::from_u32(cp) {
out.push(c);
}
}
fn canonical_order(chars: &mut [char]) {
let n = chars.len();
let mut i = 1;
while i < n {
let cc = ccc(chars[i]);
if cc != 0 {
let mut j = i;
while j > 0 && ccc(chars[j - 1]) > cc {
chars.swap(j - 1, j);
j -= 1;
}
}
i += 1;
}
}
fn compose(chars: &mut Vec<char>) {
if chars.len() < 2 {
return;
}
let mut out: Vec<char> = Vec::with_capacity(chars.len());
let mut starter: Option<usize> = None;
let mut last_ccc = 0u8;
for &c in chars.iter() {
let cc = ccc(c);
if let Some(sp) = starter {
if last_ccc == 0 || last_ccc < cc {
if let Some(composite) = primary_composite(out[sp], c) {
out[sp] = composite;
continue;
}
}
}
out.push(c);
if cc == 0 {
starter = Some(out.len() - 1);
last_ccc = 0;
} else {
last_ccc = cc;
}
}
*chars = out;
}
fn primary_composite(a: char, b: char) -> Option<char> {
let (ca, cb) = (a as u32, b as u32);
if (L_BASE..L_BASE + L_COUNT).contains(&ca) && (V_BASE..V_BASE + V_COUNT).contains(&cb) {
let li = ca - L_BASE;
let vi = cb - V_BASE;
return char::from_u32(S_BASE + (li * V_COUNT + vi) * T_COUNT);
}
if (S_BASE..S_BASE + S_COUNT).contains(&ca)
&& (ca - S_BASE) % T_COUNT == 0
&& (T_BASE + 1..T_BASE + T_COUNT).contains(&cb)
{
return char::from_u32(ca + (cb - T_BASE));
}
let key = (u64::from(ca) << 32) | u64::from(cb);
match tables::COMPOSE.binary_search_by_key(&key, |&(k, _)| k) {
Ok(i) => char::from_u32(tables::COMPOSE[i].1),
Err(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nfc_composes_base_and_mark() {
assert_eq!(normalize("e\u{0301}", Form::Nfc), "é");
}
#[test]
fn test_nfd_decomposes_precomposed() {
assert_eq!(normalize("é", Form::Nfd), "e\u{0301}");
}
#[test]
fn test_nfd_orders_marks_by_class() {
let input = "a\u{0301}\u{0323}"; assert_eq!(normalize(input, Form::Nfd), "a\u{0323}\u{0301}");
}
#[test]
fn test_nfkc_folds_ligature() {
assert_eq!(normalize("fi", Form::Nfkc), "fi");
assert_eq!(normalize("\u{FF21}", Form::Nfkc), "A"); }
#[test]
fn test_nfkd_expands_compatibility() {
assert_eq!(normalize("½", Form::Nfkd), "1\u{2044}2");
}
#[test]
fn test_hangul_roundtrip() {
let syllable = "\u{AC00}"; let decomposed = normalize(syllable, Form::Nfd);
assert_eq!(decomposed, "\u{1100}\u{1161}");
assert_eq!(normalize(&decomposed, Form::Nfc), syllable);
}
#[test]
fn test_hangul_lvt() {
let syllable = "\u{AC01}"; assert_eq!(normalize(syllable, Form::Nfd), "\u{1100}\u{1161}\u{11A8}");
assert_eq!(normalize("\u{1100}\u{1161}\u{11A8}", Form::Nfc), syllable);
}
#[test]
fn test_ascii_unchanged() {
for form in [Form::Nfc, Form::Nfd, Form::Nfkc, Form::Nfkd] {
assert_eq!(normalize("hello world 123", form), "hello world 123");
}
}
#[test]
fn test_idempotent() {
let samples = ["e\u{0301}", "fi", "가", "½", "A", "a\u{0323}\u{0301}"];
for s in samples {
for form in [Form::Nfc, Form::Nfd, Form::Nfkc, Form::Nfkd] {
let once = normalize(s, form);
let twice = normalize(&once, form);
assert_eq!(once, twice, "not idempotent: {s:?} {form:?}");
}
}
}
#[cfg(feature = "serde")]
#[test]
#[allow(clippy::unwrap_used)] fn test_form_serde_roundtrip() {
for form in [Form::Nfc, Form::Nfd, Form::Nfkc, Form::Nfkd] {
let json = serde_json::to_string(&form).unwrap();
let back: Form = serde_json::from_str(&json).unwrap();
assert_eq!(form, back);
}
assert_eq!(serde_json::to_string(&Form::Nfkc).unwrap(), "\"Nfkc\"");
}
#[test]
fn test_is_normalized_agrees_with_normalize() {
let samples = [
"",
"abc",
"é",
"e\u{0301}",
"fi",
"가",
"½",
"a\u{0323}\u{0301}",
];
for s in samples {
for form in [Form::Nfc, Form::Nfd, Form::Nfkc, Form::Nfkd] {
let expected = normalize(s, form) == s;
assert_eq!(is_normalized(s, form), expected, "{s:?} {form:?}");
}
}
}
}