#[inline(always)]
pub const fn is_format_control(c: char) -> bool {
let cp = c as u32;
if cp < 0x200B || cp > 0xFEFF {
return false;
}
matches!(cp,
0x200B..=0x200F |
0x202A..=0x202E |
0x2060..=0x2064 |
0x2066..=0x2069 |
0x206A..=0x206F |
0xFEFF
)
}
#[inline]
pub fn contains_format_controls(text: &str) -> bool {
text.chars().any(is_format_control)
}
#[inline(always)]
pub fn is_unicode_whitespace(c: char) -> bool {
matches!(
c as u32,
0x0085 | 0x00A0 | 0x1680 | 0x2000
..=0x200A | 0x2028 | 0x2029 | 0x202F | 0x205F | 0x3000 )
}
static ASCII_WS_TABLE: [bool; 256] = {
let mut table = [false; 256];
table[b' ' as usize] = true;
table[b'\t' as usize] = true;
table[b'\n' as usize] = true;
table[b'\r' as usize] = true;
table[b'\x0B' as usize] = true; table[b'\x0C' as usize] = true; table
};
#[inline(always)]
pub fn is_ascii_whitespace_fast(b: u8) -> bool {
ASCII_WS_TABLE[b as usize]
}
#[inline(always)]
pub fn is_any_whitespace(c: char) -> bool {
c.is_whitespace() || is_unicode_whitespace(c)
}
#[inline(always)]
pub const fn is_control(c: char) -> bool {
let cp = c as u32;
cp <= 0x1F || (cp >= 0x7F && cp <= 0x9F)
}
#[inline(always)]
pub fn is_fullwidth(c: char) -> bool {
let cp = c as u32;
(0xFF01..=0xFF5E).contains(&cp) || cp == 0x3000
}
#[inline(always)]
pub fn fullwidth_to_halfwidth(c: char) -> char {
let cp = c as u32;
if (0xFF01..=0xFF5E).contains(&cp) {
char::from_u32(cp - 0xFEE0).unwrap_or(c)
} else if cp == 0x3000 {
' '
} else {
c
}
}
#[inline(always)]
pub fn normalize_punctuation_char(c: char) -> char {
match c {
'“' | '”' | '„' | '«' | '»' | '′' | '″' => '"',
'‘' | '’' | '‚' => '\'',
'–' | '—' | '─' | '―' => '-',
'…' | '⋯' | '․' | '‧' => '.',
'•' | '·' | '∙' => '*',
'‹' => '<',
'›' => '>',
_ => c, }
}
#[inline(always)]
pub fn is_hangul(c: char) -> bool {
matches!(c as u32,
0xAC00..=0xD7AF | 0x1100..=0x11FF | 0x3130..=0x318F | 0xA960..=0xA97F | 0xD7B0..=0xD7FF )
}
#[inline(always)]
pub fn is_hiragana(c: char) -> bool {
matches!(c as u32, 0x3040..=0x309F)
}
#[inline(always)]
pub fn is_katakana(c: char) -> bool {
matches!(c as u32,
0x30A0..=0x30FF | 0x31F0..=0x31FF )
}
#[inline(always)]
pub fn is_kana_supplement(c: char) -> bool {
matches!(c as u32, 0x1B000..=0x1B16F)
}
#[inline(always)]
pub fn is_cjk_unified_ideograph(c: char) -> bool {
matches!(c as u32,
0x4E00..=0x9FFF | 0x3400..=0x4DBF | 0x20000..=0x2A6DF | 0x2A700..=0x2B73F | 0x2B740..=0x2B81F | 0x2B820..=0x2CEAF | 0x2CEB0..=0x2EBEF | 0x30000..=0x3134F | 0x31350..=0x323AF | 0xF900..=0xFAFF )
}
#[inline(always)]
pub fn is_kangxi_radical(c: char) -> bool {
matches!(c as u32, 0x2F00..=0x2FDF)
}
#[inline(always)]
pub fn is_cjk_han_or_kana(c: char) -> bool {
is_cjk_unified_ideograph(c)
|| is_hiragana(c)
|| is_katakana(c)
|| is_kana_supplement(c)
|| is_kangxi_radical(c)
}
#[inline(always)]
pub fn is_se_asian_script(c: char) -> bool {
matches!(c as u32,
0x0E00..=0x0E7F | 0x0E80..=0x0EFF | 0x1000..=0x109F | 0xAA60..=0xAA7F | 0xA9E0..=0xA9FF | 0x1780..=0x17FF | 0x19E0..=0x19FF | 0x1A00..=0x1AAF )
}
#[inline(always)]
pub fn is_indic_script(c: char) -> bool {
matches!(c as u32,
0x0900..=0x097F | 0x0980..=0x09FF | 0x0A00..=0x0A7F | 0x0A80..=0x0AFF | 0x0B00..=0x0B7F | 0x0B80..=0x0BFF | 0x0C00..=0x0C7F | 0x0C80..=0x0CFF | 0x0D00..=0x0D7F | 0x0D80..=0x0DFF | 0xA8E0..=0xA8FF | 0x11FC0..=0x11FFF )
}
pub const fn is_virama(c: char) -> bool {
matches!(
c as u32,
0x094D | 0x09CD | 0x0A4D | 0x0ACD | 0x0B4D | 0x0BCD | 0x0C4D | 0x0CCD | 0x0D4D | 0x0DCA | 0x103A | 0x17D2 | 0x1BAA | 0x1B44 )
}
#[inline(always)]
pub const fn should_prevent_indic_break(c: char) -> bool {
matches!(
c as u32,
0x0930 | 0x092F | 0x0935 | 0x0939 )
}
#[inline(always)]
const fn is_modern_alphabetic_script(cp: u32) -> bool {
matches!(cp,
0x0370..=0x03FF | 0x0400..=0x052F | 0x0530..=0x058F | 0x0590..=0x05FF | 0x0600..=0x06FF | 0x0700..=0x074F | 0x0750..=0x077F | 0x0870..=0x089F | 0x08A0..=0x08FF | 0x10A0..=0x10FF | 0x13A0..=0x13FF )
}
#[inline(always)]
pub fn zwsp() -> char {
'\u{200B}'
}
#[inline(always)]
pub fn is_extended_latin(c: char) -> bool {
matches!(c as u32, 0x00C0..=0x02AF) }
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[repr(u8)]
pub enum CharClass {
Other, Whitespace, Western, Cjk, Hangul, SEAsian, NonCJKScript, Indic,
}
#[inline(always)]
pub fn classify(c: char) -> CharClass {
let cp = c as u32;
if cp < 0x80 {
if c.is_ascii_whitespace() {
return CharClass::Whitespace;
}
if c.is_ascii_alphanumeric() || c.is_ascii_punctuation() {
return CharClass::Western;
}
return CharClass::Other;
}
if is_any_whitespace(c) {
return CharClass::Whitespace;
}
if is_cjk_han_or_kana(c) {
return CharClass::Cjk;
}
if is_hangul(c) {
return CharClass::Hangul;
}
if is_se_asian_script(c) {
return CharClass::SEAsian;
}
if is_indic_script(c) {
return CharClass::Indic;
}
if is_extended_latin(c) {
return CharClass::Western;
}
if is_modern_alphabetic_script(cp) {
return CharClass::NonCJKScript;
}
CharClass::Other }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_controls_are_correctly_detected() {
for c in ['\u{200B}', '\u{200C}', '\u{200D}', '\u{FEFF}', '\u{2066}'] {
assert!(
is_format_control(c),
"Missed format control U+{:04X}",
c as u32
);
}
assert!(!is_format_control('A'));
assert!(!is_format_control(' '));
}
#[test]
fn unicode_whitespace_is_correctly_detected() {
assert!(is_unicode_whitespace('\u{00A0}'));
assert!(is_unicode_whitespace('\u{3000}'));
assert!(!is_unicode_whitespace(' '));
}
#[test]
fn control_characters() {
assert!(is_control('\0'));
assert!(is_control('\u{001F}'));
assert!(is_control('\u{007F}'));
assert!(!is_control(' '));
}
#[test]
fn char_classification() {
for c in ['h', '5', '!', 'é', 'Ā', 'ſ', 'Ə', 'ǃ'] {
assert_eq!(
classify(c),
CharClass::Western,
"Western failed U+{:04X}",
c as u32
);
}
for c in ['Я', 'α', 'מ', 'م', 'ა', 'Բ', 'ܐ'] {
assert_eq!(
classify(c),
CharClass::NonCJKScript,
"Modern script failed U+{:04X}",
c as u32
);
}
for c in ['𐍈', 'Ꝛ', 'ᚠ', '☭', '𐤀', '𒀀', '𐌰', 'አ', '\u{200B}'] {
assert_eq!(
classify(c),
CharClass::Other,
"Should be Other U+{:04X}",
c as u32
);
}
for c in [' ', '\t', '\u{00A0}', '\u{3000}', '\u{1680}'] {
assert_eq!(classify(c), CharClass::Whitespace);
}
}
#[test]
fn classify_is_exhaustive_and_correct() {
use CharClass::*;
macro_rules! assert_class {
($c:expr, $expected:expr) => {
assert_eq!(
classify($c),
$expected,
"U+{:04X} '{}' misclassified",
$c as u32,
$c
);
};
}
assert_class!('A', Western);
assert_class!('5', Western);
assert_class!('!', Western);
assert_class!('À', Western); assert_class!('ÿ', Western); assert_class!('Ā', Western); assert_class!('ſ', Western); assert_class!('Ə', Western); assert_class!('ǃ', Western);
assert_class!('𱁬', Cjk); assert_class!('𲁨', Cjk); assert_class!('豈', Cjk); assert_class!('㐀', Cjk); assert_class!('㐀', Cjk); assert_class!('世', Cjk);
assert_class!('界', Cjk);
assert_class!('অ', Indic); assert_class!('ਅ', Indic); assert_class!('અ', Indic); assert_class!('ଅ', Indic); assert_class!('అ', Indic); assert_class!('ಕ', Indic); assert_class!('മ', Indic); assert_class!('අ', Indic);
assert_class!('ꩠ', SEAsian);
assert_class!('᧠', SEAsian);
assert_class!('𐐧', Other); assert_class!('𐐀', Other); assert_class!('Ꝛ', Other); assert_class!('★', Other);
assert_class!('☭', Other);
assert_class!('𐍈', Other);
assert_class!('\u{200B}', Other); assert_class!('\u{2060}', Other); assert_class!('\u{FEFF}', Other);
assert_class!('\u{1680}', Whitespace); assert_class!('\u{2028}', Whitespace); }
}