use core::cmp::Ordering;
pub(crate) fn is_silent_mark(c: char) -> bool {
is_punctuation(c) && !READ_ALOUD.contains(&c)
}
pub(crate) const READ_ALOUD: [char; 13] = [
'#', '%', '&', '@', '\u{A7}', '\u{B6}', '\u{66A}', '\u{2030}', '\u{2031}', '\u{FF03}',
'\u{FF05}', '\u{FF06}', '\u{FF20}',
];
fn is_punctuation(c: char) -> bool {
PUNCTUATION
.binary_search_by(|&(first, last)| {
if last < c {
Ordering::Less
} else if c < first {
Ordering::Greater
} else {
Ordering::Equal
}
})
.is_ok()
}
const PUNCTUATION: [(char, char); 198] = [
('\u{21}', '\u{23}'),
('\u{25}', '\u{2A}'),
('\u{2C}', '\u{2F}'),
('\u{3A}', '\u{3B}'),
('\u{3F}', '\u{40}'),
('\u{5B}', '\u{5D}'),
('\u{5F}', '\u{5F}'),
('\u{7B}', '\u{7B}'),
('\u{7D}', '\u{7D}'),
('\u{A1}', '\u{A1}'),
('\u{A7}', '\u{A7}'),
('\u{AB}', '\u{AB}'),
('\u{B6}', '\u{B7}'),
('\u{BB}', '\u{BB}'),
('\u{BF}', '\u{BF}'),
('\u{37E}', '\u{37E}'),
('\u{387}', '\u{387}'),
('\u{55A}', '\u{55F}'),
('\u{589}', '\u{58A}'),
('\u{5BE}', '\u{5BE}'),
('\u{5C0}', '\u{5C0}'),
('\u{5C3}', '\u{5C3}'),
('\u{5C6}', '\u{5C6}'),
('\u{5F3}', '\u{5F4}'),
('\u{609}', '\u{60A}'),
('\u{60C}', '\u{60D}'),
('\u{61B}', '\u{61B}'),
('\u{61D}', '\u{61F}'),
('\u{66A}', '\u{66D}'),
('\u{6D4}', '\u{6D4}'),
('\u{700}', '\u{70D}'),
('\u{7F7}', '\u{7F9}'),
('\u{830}', '\u{83E}'),
('\u{85E}', '\u{85E}'),
('\u{964}', '\u{965}'),
('\u{970}', '\u{970}'),
('\u{9FD}', '\u{9FD}'),
('\u{A76}', '\u{A76}'),
('\u{AF0}', '\u{AF0}'),
('\u{C77}', '\u{C77}'),
('\u{C84}', '\u{C84}'),
('\u{DF4}', '\u{DF4}'),
('\u{E4F}', '\u{E4F}'),
('\u{E5A}', '\u{E5B}'),
('\u{F04}', '\u{F12}'),
('\u{F14}', '\u{F14}'),
('\u{F3A}', '\u{F3D}'),
('\u{F85}', '\u{F85}'),
('\u{FD0}', '\u{FD4}'),
('\u{FD9}', '\u{FDA}'),
('\u{104A}', '\u{104F}'),
('\u{10FB}', '\u{10FB}'),
('\u{1360}', '\u{1368}'),
('\u{1400}', '\u{1400}'),
('\u{166E}', '\u{166E}'),
('\u{169B}', '\u{169C}'),
('\u{16EB}', '\u{16ED}'),
('\u{1735}', '\u{1736}'),
('\u{17D4}', '\u{17D6}'),
('\u{17D8}', '\u{17DA}'),
('\u{1800}', '\u{180A}'),
('\u{1944}', '\u{1945}'),
('\u{1A1E}', '\u{1A1F}'),
('\u{1AA0}', '\u{1AA6}'),
('\u{1AA8}', '\u{1AAD}'),
('\u{1B4E}', '\u{1B4F}'),
('\u{1B5A}', '\u{1B60}'),
('\u{1B7D}', '\u{1B7F}'),
('\u{1BFC}', '\u{1BFF}'),
('\u{1C3B}', '\u{1C3F}'),
('\u{1C7E}', '\u{1C7F}'),
('\u{1CC0}', '\u{1CC7}'),
('\u{1CD3}', '\u{1CD3}'),
('\u{2010}', '\u{2027}'),
('\u{2030}', '\u{2043}'),
('\u{2045}', '\u{2051}'),
('\u{2053}', '\u{205E}'),
('\u{207D}', '\u{207E}'),
('\u{208D}', '\u{208E}'),
('\u{2308}', '\u{230B}'),
('\u{2329}', '\u{232A}'),
('\u{2768}', '\u{2775}'),
('\u{27C5}', '\u{27C6}'),
('\u{27E6}', '\u{27EF}'),
('\u{2983}', '\u{2998}'),
('\u{29D8}', '\u{29DB}'),
('\u{29FC}', '\u{29FD}'),
('\u{2CF9}', '\u{2CFC}'),
('\u{2CFE}', '\u{2CFF}'),
('\u{2D70}', '\u{2D70}'),
('\u{2E00}', '\u{2E2E}'),
('\u{2E30}', '\u{2E4F}'),
('\u{2E52}', '\u{2E5D}'),
('\u{3001}', '\u{3003}'),
('\u{3008}', '\u{3011}'),
('\u{3014}', '\u{301F}'),
('\u{3030}', '\u{3030}'),
('\u{303D}', '\u{303D}'),
('\u{30A0}', '\u{30A0}'),
('\u{30FB}', '\u{30FB}'),
('\u{A4FE}', '\u{A4FF}'),
('\u{A60D}', '\u{A60F}'),
('\u{A673}', '\u{A673}'),
('\u{A67E}', '\u{A67E}'),
('\u{A6F2}', '\u{A6F7}'),
('\u{A874}', '\u{A877}'),
('\u{A8CE}', '\u{A8CF}'),
('\u{A8F8}', '\u{A8FA}'),
('\u{A8FC}', '\u{A8FC}'),
('\u{A92E}', '\u{A92F}'),
('\u{A95F}', '\u{A95F}'),
('\u{A9C1}', '\u{A9CD}'),
('\u{A9DE}', '\u{A9DF}'),
('\u{AA5C}', '\u{AA5F}'),
('\u{AADE}', '\u{AADF}'),
('\u{AAF0}', '\u{AAF1}'),
('\u{ABEB}', '\u{ABEB}'),
('\u{FD3E}', '\u{FD3F}'),
('\u{FE10}', '\u{FE19}'),
('\u{FE30}', '\u{FE52}'),
('\u{FE54}', '\u{FE61}'),
('\u{FE63}', '\u{FE63}'),
('\u{FE68}', '\u{FE68}'),
('\u{FE6A}', '\u{FE6B}'),
('\u{FF01}', '\u{FF03}'),
('\u{FF05}', '\u{FF0A}'),
('\u{FF0C}', '\u{FF0F}'),
('\u{FF1A}', '\u{FF1B}'),
('\u{FF1F}', '\u{FF20}'),
('\u{FF3B}', '\u{FF3D}'),
('\u{FF3F}', '\u{FF3F}'),
('\u{FF5B}', '\u{FF5B}'),
('\u{FF5D}', '\u{FF5D}'),
('\u{FF5F}', '\u{FF65}'),
('\u{10100}', '\u{10102}'),
('\u{1039F}', '\u{1039F}'),
('\u{103D0}', '\u{103D0}'),
('\u{1056F}', '\u{1056F}'),
('\u{10857}', '\u{10857}'),
('\u{1091F}', '\u{1091F}'),
('\u{1093F}', '\u{1093F}'),
('\u{10A50}', '\u{10A58}'),
('\u{10A7F}', '\u{10A7F}'),
('\u{10AF0}', '\u{10AF6}'),
('\u{10B39}', '\u{10B3F}'),
('\u{10B99}', '\u{10B9C}'),
('\u{10D6E}', '\u{10D6E}'),
('\u{10EAD}', '\u{10EAD}'),
('\u{10F55}', '\u{10F59}'),
('\u{10F86}', '\u{10F89}'),
('\u{11047}', '\u{1104D}'),
('\u{110BB}', '\u{110BC}'),
('\u{110BE}', '\u{110C1}'),
('\u{11140}', '\u{11143}'),
('\u{11174}', '\u{11175}'),
('\u{111C5}', '\u{111C8}'),
('\u{111CD}', '\u{111CD}'),
('\u{111DB}', '\u{111DB}'),
('\u{111DD}', '\u{111DF}'),
('\u{11238}', '\u{1123D}'),
('\u{112A9}', '\u{112A9}'),
('\u{113D4}', '\u{113D5}'),
('\u{113D7}', '\u{113D8}'),
('\u{1144B}', '\u{1144F}'),
('\u{1145A}', '\u{1145B}'),
('\u{1145D}', '\u{1145D}'),
('\u{114C6}', '\u{114C6}'),
('\u{115C1}', '\u{115D7}'),
('\u{11641}', '\u{11643}'),
('\u{11660}', '\u{1166C}'),
('\u{116B9}', '\u{116B9}'),
('\u{1173C}', '\u{1173E}'),
('\u{1183B}', '\u{1183B}'),
('\u{11944}', '\u{11946}'),
('\u{119E2}', '\u{119E2}'),
('\u{11A3F}', '\u{11A46}'),
('\u{11A9A}', '\u{11A9C}'),
('\u{11A9E}', '\u{11AA2}'),
('\u{11B00}', '\u{11B09}'),
('\u{11BE1}', '\u{11BE1}'),
('\u{11C41}', '\u{11C45}'),
('\u{11C70}', '\u{11C71}'),
('\u{11EF7}', '\u{11EF8}'),
('\u{11F43}', '\u{11F4F}'),
('\u{11FFF}', '\u{11FFF}'),
('\u{12470}', '\u{12474}'),
('\u{12FF1}', '\u{12FF2}'),
('\u{16A6E}', '\u{16A6F}'),
('\u{16AF5}', '\u{16AF5}'),
('\u{16B37}', '\u{16B3B}'),
('\u{16B44}', '\u{16B44}'),
('\u{16D6D}', '\u{16D6F}'),
('\u{16E97}', '\u{16E9A}'),
('\u{16FE2}', '\u{16FE2}'),
('\u{1BC9F}', '\u{1BC9F}'),
('\u{1DA87}', '\u{1DA8B}'),
('\u{1E5FF}', '\u{1E5FF}'),
('\u{1E95E}', '\u{1E95F}'),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_punctuation_is_unicodes_p_categories() {
for code in 0..=0x7F_u8 {
let c = char::from(code);
assert_eq!(
is_punctuation(c),
c.is_ascii_punctuation() && !"$+<=>^`|~".contains(c),
"{c:?}"
);
}
}
#[test]
fn the_table_is_unicode_16s_p_categories() {
for pair in PUNCTUATION.windows(2) {
let ((first, last), (next, _)) = (pair[0], pair[1]);
assert!(first <= last, "{pair:?}");
assert!(u32::from(last) + 1 < u32::from(next), "{pair:?}");
}
let code_points: u32 = PUNCTUATION
.iter()
.map(|&(first, last)| u32::from(last) - u32::from(first) + 1)
.sum();
assert_eq!(code_points, 855);
for &(first, last) in &PUNCTUATION {
assert!(
is_punctuation(first) && is_punctuation(last),
"{first:?}..={last:?}"
);
let before = u32::from(first).checked_sub(1).and_then(char::from_u32);
let after = char::from_u32(u32::from(last) + 1);
for outside in before.into_iter().chain(after) {
assert!(!is_punctuation(outside), "{outside:?}");
}
}
for mark in [
'\u{A1}',
'\u{B7}',
'\u{2019}',
'\u{2026}',
'\u{3001}',
'\u{300A}',
'\u{FF08}',
'\u{1E95F}',
] {
assert!(is_punctuation(mark), "{mark:?}");
}
for other in [
'\u{A0}',
'\u{A9}',
'\u{B0}',
'\u{E9}',
'\u{2BC}',
'\u{20AC}',
'\u{65E5}',
'\u{1E960}',
] {
assert!(!is_punctuation(other), "{other:?}");
}
}
#[test]
fn a_mark_nobody_reads_aloud_is_silent_and_every_other_character_is_spoken() {
for mark in [
'.', ',', '?', '!', ';', ':', '"', '\'', '(', ')', '-', '/', '*', '_', '\\', '\u{2014}',
'\u{201C}', '\u{2019}', '\u{2026}', '\u{AB}', '\u{BF}', '\u{3001}', '\u{3002}', '\u{300A}',
'\u{301C}', '\u{FF0C}',
] {
assert!(is_silent_mark(mark), "{mark:?} is silent");
}
for spoken in [
'a', 'Z', '\u{E9}', '5', '\u{65E5}', '$', '<', '+', '|', '\u{A9}', '\u{20AC}', '\u{2BC}',
] {
assert!(!is_silent_mark(spoken), "{spoken:?} is spoken");
}
for aloud in READ_ALOUD {
assert!(
is_punctuation(aloud),
"{aloud:?} is punctuation, or listing it changes nothing"
);
assert!(!is_silent_mark(aloud), "{aloud:?} is read aloud");
}
}
}