use super::unicode_data as data;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct CptFlags(u8);
impl CptFlags {
pub(super) fn is_separator(self) -> bool {
self.0 & data::SEPARATOR != 0
}
pub(super) fn is_accent_mark(self) -> bool {
self.0 & data::ACCENT_MARK != 0
}
pub(super) fn is_punctuation(self) -> bool {
self.0 & data::PUNCTUATION != 0
}
pub(super) fn is_symbol(self) -> bool {
self.0 & data::SYMBOL != 0
}
pub(super) fn is_control(self) -> bool {
self.0 & data::CONTROL != 0
}
}
pub(super) fn flags(c: char) -> CptFlags {
let cpt = c as u32;
let i = data::RANGES_FLAGS.partition_point(|&(start, _)| start <= cpt);
debug_assert!(i > 0, "RANGES_FLAGS must start at codepoint 0");
CptFlags(data::RANGES_FLAGS[i - 1].1)
}
pub(super) fn is_whitespace(c: char) -> bool {
data::WHITESPACE.binary_search(&(c as u32)).is_ok()
}
pub(super) fn to_lower(c: char) -> char {
let cpt = c as u32;
match data::LOWERCASE.binary_search_by_key(&cpt, |&(from, _)| from) {
Ok(i) => char::from_u32(data::LOWERCASE[i].1).unwrap_or(c),
Err(_) => c,
}
}
pub(super) fn nfd_base(c: char) -> char {
let cpt = c as u32;
let i = data::NFD.partition_point(|&(start, _, _)| start <= cpt);
if i == 0 {
return c;
}
let (start, last, base) = data::NFD[i - 1];
if start <= cpt && cpt <= last {
char::from_u32(base).unwrap_or(c)
} else {
c
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_classification_matches_the_categories() {
assert!(flags('.').is_punctuation());
assert!(flags(',').is_punctuation());
assert!(flags('-').is_punctuation());
assert!(flags('\'').is_punctuation());
assert!(flags('$').is_symbol());
assert!(flags('+').is_symbol());
assert!(flags('=').is_symbol());
assert!(flags('|').is_symbol());
assert!(flags('~').is_symbol());
assert!(flags('\u{1b}').is_control(), "ESC is Cc");
assert!(flags('\u{7}').is_control(), "BEL is Cc");
assert!(flags(' ').is_separator());
for c in ['a', 'Z', '7', 'é', '日'] {
let f = flags(c);
assert!(
!f.is_punctuation() && !f.is_symbol() && !f.is_control() && !f.is_separator(),
"{c:?} should carry no category bit here"
);
}
}
#[test]
fn control_covers_cf_and_co_not_just_cc() {
assert!(flags('\u{200b}').is_control(), "ZWSP is Cf");
assert!(flags('\u{feff}').is_control(), "BOM is Cf");
assert!(flags('\u{00ad}').is_control(), "soft hyphen is Cf");
assert!(flags('\u{e000}').is_control(), "private use is Co");
assert!(
!flags('\u{0378}').is_control(),
"an unassigned codepoint is Cn, which is not a control"
);
}
#[test]
fn accent_marks_are_marks_and_letters_are_not() {
assert!(flags('\u{0301}').is_accent_mark(), "combining acute is Mn");
assert!(flags('\u{0323}').is_accent_mark(), "combining dot below");
assert!(!flags('e').is_accent_mark());
}
#[test]
fn whitespace_is_the_unicode_property() {
for c in [' ', '\t', '\n', '\r', '\u{b}', '\u{c}', '\u{85}'] {
assert!(is_whitespace(c), "{c:?} is White_Space");
}
for c in [
'\u{a0}', '\u{1680}', '\u{2000}', '\u{200a}', '\u{2009}', '\u{3000}',
] {
assert!(is_whitespace(c), "{c:?} is White_Space");
}
assert!(!is_whitespace('\u{200b}'), "ZWSP is Cf, not White_Space");
assert!(!is_whitespace('a'));
}
#[test]
fn simple_lowercase_folds_the_cases_std_would_expand() {
assert_eq!(to_lower('A'), 'a');
assert_eq!(to_lower('a'), 'a');
assert_eq!(to_lower('É'), 'é');
assert_eq!(to_lower('Д'), 'д');
assert_eq!(to_lower('日'), '日');
assert_eq!(to_lower('\u{130}'), 'i');
assert_eq!('\u{130}'.to_lowercase().count(), 2, "std disagrees here");
}
#[test]
fn nfd_folds_a_precomposed_char_to_its_base() {
assert_eq!(nfd_base('é'), 'e');
assert_eq!(nfd_base('É'), 'E');
assert_eq!(nfd_base('ñ'), 'n');
assert_eq!(nfd_base('\u{1e69}'), 's', "double-decomposing ṩ");
assert_eq!(nfd_base('e'), 'e', "no decomposition, unchanged");
assert_eq!(nfd_base('日'), '日');
assert_eq!(
nfd_base('\u{0301}'),
'\u{0301}',
"a standalone mark is not decomposed here"
);
}
#[test]
fn the_generated_tables_are_sorted() {
assert!(
data::RANGES_FLAGS.windows(2).all(|w| w[0].0 < w[1].0),
"RANGES_FLAGS must ascend by start"
);
assert_eq!(data::RANGES_FLAGS[0].0, 0, "must cover codepoint 0");
assert!(
data::WHITESPACE.windows(2).all(|w| w[0] < w[1]),
"WHITESPACE must ascend"
);
assert!(
data::LOWERCASE.windows(2).all(|w| w[0].0 < w[1].0),
"LOWERCASE must ascend by codepoint"
);
assert!(
data::NFD.windows(2).all(|w| w[0].1 < w[1].0),
"NFD ranges must ascend and not overlap"
);
assert!(
data::NFD.iter().all(|&(start, last, _)| start <= last),
"every NFD range must be non-empty"
);
}
}