use std::sync::LazyLock;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum Gender {
Masculine,
Feminine,
Neuter,
}
impl Gender {
fn parse(value: &str) -> Self {
match value {
"f" => Gender::Feminine,
"n" => Gender::Neuter,
_ => Gender::Masculine,
}
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Forms {
pub one: &'static str,
pub few: &'static str,
pub many: &'static str,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Unit {
pub key: &'static str,
pub forms: Forms,
pub decimal: &'static str,
pub gender: Gender,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct CountedNoun {
pub key: &'static str,
pub forms: Forms,
pub gender: Gender,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct FinanceUnit {
pub code: &'static str,
pub forms: Forms,
pub decimal: &'static str,
pub feminine: bool,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Currency {
pub code: &'static str,
pub symbol: &'static str,
pub word_re: &'static str,
pub main: Forms,
pub main_feminine: bool,
pub sub: Forms,
pub sub_feminine: bool,
pub trailing_symbol: bool,
pub minor_digits: u32,
}
pub(crate) type Pair = (&'static str, &'static str);
fn rows(source: &'static str, expected_header: &str) -> Vec<Vec<&'static str>> {
let mut lines = source
.lines()
.map(|line| line.strip_suffix('\r').unwrap_or(line))
.filter(|line| !line.is_empty() && !line.starts_with('#'));
let header = lines.next().expect("lexicon table is empty");
assert_eq!(header, expected_header, "unexpected lexicon header");
let width = expected_header.split('\t').count();
lines
.map(|line| {
let fields: Vec<&str> = line.split('\t').collect();
assert_eq!(fields.len(), width, "wrong column count in lexicon row: {line:?}");
fields
})
.collect()
}
fn pairs(source: &'static str, header: &str) -> Vec<Pair> {
rows(source, header).into_iter().map(|r| (r[0], r[1])).collect()
}
macro_rules! table {
($name:ident, $ty:ty, $file:literal, $header:literal, $row:expr) => {
pub(crate) static $name: LazyLock<Vec<$ty>> = LazyLock::new(|| {
rows(include_str!(concat!("../../data/lexicons/", $file)), $header)
.into_iter()
.map($row)
.collect()
});
};
}
macro_rules! pair_table {
($name:ident, $file:literal, $header:literal) => {
pub(crate) static $name: LazyLock<Vec<Pair>> =
LazyLock::new(|| pairs(include_str!(concat!("../../data/lexicons/", $file)), $header));
};
}
table!(UNITS, Unit, "units.tsv", "key\tone\tfew\tmany\tdecimal\tgender", |r| Unit {
key: r[0],
forms: Forms { one: r[1], few: r[2], many: r[3] },
decimal: r[4],
gender: Gender::parse(r[5]),
});
table!(COUNTED_NOUNS, CountedNoun, "counted_nouns.tsv", "key\tone\tfew\tmany\tgender", |r| {
CountedNoun {
key: r[0],
forms: Forms { one: r[1], few: r[2], many: r[3] },
gender: Gender::parse(r[4]),
}
});
table!(
FINANCE_UNITS,
FinanceUnit,
"finance_units.tsv",
"code\tone\tfew\tmany\tdecimal\tfeminine",
|r| FinanceUnit {
code: r[0],
forms: Forms { one: r[1], few: r[2], many: r[3] },
decimal: r[4],
feminine: r[5] == "1",
}
);
table!(
CURRENCIES,
Currency,
"currencies.tsv",
"code\tsymbol\tword_re\tmain_one\tmain_few\tmain_many\tmain_fem\tsub_one\tsub_few\tsub_many\tsub_fem\ttrailing_symbol\tminor_digits",
|r| Currency {
code: r[0],
symbol: r[1],
word_re: r[2],
main: Forms { one: r[3], few: r[4], many: r[5] },
main_feminine: r[6] == "1",
sub: Forms { one: r[7], few: r[8], many: r[9] },
sub_feminine: r[10] == "1",
trailing_symbol: r[11] == "1",
minor_digits: r[12].parse().expect("minor_digits must be a number"),
}
);
pub(crate) static COUNTED_OBLIQUE: LazyLock<Vec<Pair>> = LazyLock::new(|| {
pairs(include_str!("../../data/lexicons/counted_oblique.tsv"), "key\tgrammatical_case")
});
pair_table!(ACRONYMS, "acronyms.tsv", "acronym\texpansion");
pair_table!(ABBREVIATIONS, "abbreviations.tsv", "key\texpansion");
pair_table!(BRANDS, "brands.tsv", "latin\tcyrillic");
pair_table!(ENGLISH_WORDS, "english_words.tsv", "latin\tcyrillic");
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn keys_are_unique() {
fn unique<'a>(name: &str, keys: impl Iterator<Item = &'a str>) {
let mut seen = HashSet::new();
for key in keys {
assert!(seen.insert(key), "{name}: duplicate key {key:?}");
}
}
unique("units", UNITS.iter().map(|u| u.key));
unique("counted_nouns", COUNTED_NOUNS.iter().map(|n| n.key));
unique("counted_oblique", COUNTED_OBLIQUE.iter().map(|&(k, _)| k));
unique("finance_units", FINANCE_UNITS.iter().map(|u| u.code));
unique("currencies", CURRENCIES.iter().map(|c| c.code));
unique("acronyms", ACRONYMS.iter().map(|&(k, _)| k));
unique("abbreviations", ABBREVIATIONS.iter().map(|&(k, _)| k));
unique("brands", BRANDS.iter().map(|&(k, _)| k));
unique("english_words", ENGLISH_WORDS.iter().map(|&(k, _)| k));
}
#[test]
fn column_values_are_in_range() {
for currency in CURRENCIES.iter() {
assert!(
matches!(currency.minor_digits, 0 | 2 | 3 | 4),
"{}: minor_digits must be 0, 2, 3 or 4",
currency.code
);
}
for &(key, case) in COUNTED_OBLIQUE.iter() {
assert!(
matches!(case, "instr" | "prep"),
"{key}: grammatical case must be instr or prep"
);
}
}
#[test]
fn required_columns_are_present() {
for unit in UNITS.iter() {
for (name, value) in [
("key", unit.key),
("one", unit.forms.one),
("few", unit.forms.few),
("many", unit.forms.many),
("decimal", unit.decimal),
] {
assert!(!value.is_empty(), "unit {:?}: {name} is empty", unit.key);
}
}
for currency in CURRENCIES.iter() {
for (name, value) in [
("main_one", currency.main.one),
("main_many", currency.main.many),
("sub_one", currency.sub.one),
("sub_many", currency.sub.many),
] {
assert!(!value.is_empty(), "currency {}: {name} is empty", currency.code);
}
}
}
}