#![cfg_attr(not(feature = "ru"), allow(dead_code, unused_imports))]
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;
use unicode_normalization::UnicodeNormalization;
use crate::Language;
#[derive(Debug, Default)]
pub(crate) struct Tables {
pub(crate) abbreviations: HashSet<String>,
pub(crate) not_abbreviations: HashSet<String>,
pub(crate) particles: HashSet<String>,
pub(crate) indefinite: HashSet<String>,
pub(crate) whole: HashSet<String>,
pub(crate) numerals: HashSet<String>,
pub(crate) prefixes: HashSet<String>,
pub(crate) endings: HashSet<String>,
pub(crate) linkers: HashSet<String>,
pub(crate) final_abbreviations: HashSet<String>,
pub(crate) dotted_names: HashSet<String>,
homoglyphs: HashMap<char, char>,
}
fn fold(word: &str) -> String {
if word.chars().all(plain) {
return word.chars().flat_map(char::to_lowercase).collect();
}
normalized_fold(word)
}
fn plain(character: char) -> bool {
character.is_ascii() || matches!(character, 'А'..='я' | 'Ё' | 'ё')
}
fn normalized_fold(word: &str) -> String {
word.nfd()
.filter(|&character| !matches!(character, '\u{300}' | '\u{301}' | '\u{ad}'))
.nfc()
.flat_map(char::to_lowercase)
.collect()
}
fn list(text: &str) -> HashSet<String> {
lines(text).map(fold).collect()
}
fn lines(text: &str) -> impl Iterator<Item = &str> {
text.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
}
impl Tables {
fn new(files: [&str; 12]) -> Self {
let [
abbreviations,
not_abbreviations,
particles,
indefinite,
whole,
numerals,
prefixes,
endings,
linkers,
final_abbreviations,
dotted_names,
homoglyphs,
] = files;
Tables {
abbreviations: list(abbreviations),
not_abbreviations: list(not_abbreviations),
particles: list(particles),
indefinite: list(indefinite),
whole: list(whole),
numerals: list(numerals),
prefixes: list(prefixes),
endings: list(endings),
linkers: list(linkers),
final_abbreviations: lines(final_abbreviations)
.map(|line| {
line.split(|c: char| c == '.' || c.is_whitespace())
.filter(|word| !word.is_empty())
.map(fold)
.collect::<Vec<_>>()
.join(" ")
})
.collect(),
dotted_names: list(dotted_names),
homoglyphs: lines(homoglyphs)
.filter_map(|line| {
let mut chars = line
.split_whitespace()
.filter_map(|part| part.chars().next());
Some((chars.next()?, chars.next()?))
})
.collect(),
}
}
pub(crate) fn key(&self, word: &str) -> String {
let folded = fold(word);
if self.homoglyphs.is_empty()
|| !folded
.chars()
.any(|character| character.is_alphabetic() && !character.is_ascii())
{
return folded;
}
folded
.chars()
.map(|character| *self.homoglyphs.get(&character).unwrap_or(&character))
.collect()
}
}
pub(crate) fn tables(language: Language) -> &'static Tables {
match language {
#[cfg(feature = "ru")]
Language::Russian => {
static RUSSIAN: OnceLock<Tables> = OnceLock::new();
RUSSIAN.get_or_init(|| {
Tables::new([
include_str!("../data/ru/abbreviations.txt"),
include_str!("../data/ru/not_abbreviations.txt"),
include_str!("../data/ru/hyphen_particles.txt"),
include_str!("../data/ru/indefinite.txt"),
include_str!("../data/ru/hyphen_whole.txt"),
include_str!("../data/ru/numerals.txt"),
include_str!("../data/ru/hyphen_prefixes.txt"),
include_str!("../data/ru/inflection_endings.txt"),
include_str!("../data/ru/hyphen_linkers.txt"),
include_str!("../data/ru/final_abbreviations.txt"),
include_str!("../data/ru/dotted_names.txt"),
include_str!("../data/ru/homoglyphs.txt"),
])
})
}
}
}
#[cfg(all(test, feature = "ru"))]
mod tests {
use super::*;
#[test]
fn keys() {
let tables = tables(Language::Russian);
assert_eq!(tables.key("Чтó"), "что");
assert_eq!(tables.key("Ку\u{301}\u{ad}рица"), "курица");
assert_eq!(tables.key("Ёлка й"), "ёлка й");
assert_eq!(tables.key("Com"), "com");
assert!(tables.final_abbreviations.contains("т д"));
}
#[test]
fn plain_words_fold_without_normalization() {
let characters: Vec<char> = (0..=0x10ffff)
.filter_map(char::from_u32)
.filter(|&character| plain(character))
.collect();
for &first in &characters {
let word = first.to_string();
assert_eq!(fold(&word), normalized_fold(&word), "{word:?}");
for &second in &characters {
let word = format!("{first}{second}");
assert_eq!(fold(&word), normalized_fold(&word), "{word:?}");
}
}
}
}