use unicode_general_category::{GeneralCategory, get_general_category};
pub(crate) use crate::g2p::pystr::{is_letter, is_py_space, is_upper_letter};
pub(crate) const PRIMARY_STRESS: char = 'ˈ';
pub(crate) const SECONDARY_STRESS: char = 'ˌ';
pub(crate) const VOWELS: &str = "AIOQWYaiuæɑɒɔəɛɜɪʊʌᵻ";
pub(crate) const CONSONANTS: &str = "bdfhjklmnpstvwzðŋɡɹɾʃʒʤʧθ";
pub(crate) const DIPHTHONGS: &str = "AIOQWYʤʧ";
pub(crate) const US_TAUS: &str = "AIOWYiuæɑəɛɪɹʊʌ";
pub(crate) const PUNCTS: &str = ";:,.!?—…\"“”";
pub(crate) const NON_QUOTE_PUNCTS: &str = ";:,.!?—…";
pub(crate) const SUBTOKEN_JUNKS: &str = "',-._‘’/";
pub(crate) const QUOTES: &str = "'‘’";
pub(crate) fn is_vowel(c: char) -> bool {
VOWELS.contains(c)
}
pub(crate) fn is_lower_letter(c: char) -> bool {
get_general_category(c) == GeneralCategory::LowercaseLetter
}
pub(crate) fn is_decimal(c: char) -> bool {
get_general_category(c) == GeneralCategory::DecimalNumber
}
pub(crate) fn is_alpha(s: &str) -> bool {
!s.is_empty() && s.chars().all(is_letter)
}
pub(crate) fn is_ascii_digit(c: char) -> bool {
c.is_ascii_digit()
}
pub(crate) fn is_digit_str(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
const OTHER_DIGITS: &[(u32, u32, u32)] = &[
(0x1369, 0x1371, 1),
(0x19DA, 0x19DA, 1),
(0x24F5, 0x24FD, 1),
(0x24FF, 0x24FF, 0),
(0x2776, 0x277E, 1),
(0x2780, 0x2788, 1),
(0x278A, 0x2792, 1),
(0x10A40, 0x10A43, 1),
(0x10E60, 0x10E68, 1),
(0x11052, 0x1105A, 1),
];
pub(crate) fn digit_value(c: char) -> Option<u32> {
if c.is_ascii_digit() {
return Some(c as u32 - '0' as u32);
}
if is_decimal(c) {
let mut offset = 0u32;
let mut cp = c as u32;
while cp > 0 {
cp -= 1;
match char::from_u32(cp) {
Some(prev) if is_decimal(prev) => offset += 1,
_ => break,
}
}
return Some(offset % 10);
}
let cp = c as u32;
OTHER_DIGITS
.iter()
.find(|(lo, hi, _)| (*lo..=*hi).contains(&cp))
.map(|(lo, _, first)| first + (cp - lo))
}
pub(crate) fn lower(s: &str) -> String {
s.to_lowercase()
}
pub(crate) fn upper(s: &str) -> String {
s.to_uppercase()
}
pub(crate) fn char_is_lower_fixed(c: char) -> bool {
let mut it = c.to_lowercase();
it.next() == Some(c) && it.next().is_none()
}
pub(crate) fn tail(s: &str) -> &str {
let mut it = s.chars();
it.next();
it.as_str()
}
pub(crate) fn drop_last(s: &str, n: usize) -> &str {
if n == 0 {
return s;
}
match s.char_indices().rev().nth(n - 1) {
Some((i, _)) => &s[..i],
None => "",
}
}
pub(crate) fn char_len(s: &str) -> usize {
s.chars().count()
}
pub(crate) fn is_lexicon_char(c: char) -> bool {
c == '\'' || c == '-' || c.is_ascii_alphabetic()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn letters_follow_general_category() {
assert!(is_letter('a') && is_letter('É') && is_letter('Dž'));
assert!(!is_letter('1') && !is_letter('\'') && !is_letter('\u{0301}'));
assert!(is_upper_letter('Q') && !is_upper_letter('Dž'));
assert!(is_lower_letter('q'));
assert!(is_decimal('٣') && !is_decimal('²'));
}
#[test]
fn digit_values_match_python_isdigit() {
assert_eq!(digit_value('7'), Some(7));
assert_eq!(digit_value('٣'), Some(3));
assert_eq!(digit_value('\u{1D7D5}'), Some(7));
assert_eq!(digit_value('\u{136B}'), Some(3));
assert_eq!(digit_value('\u{24FF}'), Some(0));
assert_eq!(digit_value('a'), None);
}
#[test]
fn python_slices() {
assert_eq!(tail("héllo"), "éllo");
assert_eq!(drop_last("héllo", 2), "hél");
assert_eq!(drop_last("ab", 5), "");
assert!(is_py_space('\u{1f}') && !is_py_space('x'));
assert!(char_is_lower_fixed('a') && !char_is_lower_fixed('A'));
}
}