use english_core::EnglishCore;
pub use english_core::grammar::*;
mod noun_phf {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/generated/noun_phf.rs"
));
}
use noun_phf::*;
mod adj_phf {
include!(concat!(env!("CARGO_MANIFEST_DIR"), "/generated/adj_phf.rs"));
}
use adj_phf::*;
mod verb_phf {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/generated/verb_phf.rs"
));
}
use verb_phf::*;
mod adverb_phf {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/generated/adverb_phf.rs"
));
}
use adverb_phf::*;
fn canonical_sense_suffix_base(word: &str) -> Option<&str> {
english_core::sense_key::split(word).map(|(base, _)| base)
}
fn base_lemma(word: &str, is_key: impl Fn(&str) -> bool) -> &str {
match canonical_sense_suffix_base(word) {
Some(base) if is_key(word) || is_key(base) => base,
_ => word,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CaseStyle {
AsIs,
Title,
Upper,
}
fn case_style(word: &str) -> CaseStyle {
let Some(first) = word.chars().next() else {
return CaseStyle::AsIs;
};
if !first.is_uppercase() {
return CaseStyle::AsIs;
}
let letters = word.chars().filter(|c| c.is_alphabetic());
if letters.clone().count() > 1 && letters.clone().all(|c| !c.is_lowercase()) {
CaseStyle::Upper
} else if word.chars().skip(1).all(|c| !c.is_uppercase()) {
CaseStyle::Title
} else {
CaseStyle::AsIs }
}
fn restyle(s: String, style: CaseStyle) -> String {
match style {
CaseStyle::AsIs => s,
CaseStyle::Upper => s.to_uppercase(),
CaseStyle::Title => English::capitalize_first(&s),
}
}
fn rule_with_case(word: &str, rule: impl Fn(&str) -> String) -> String {
match case_style(word) {
CaseStyle::AsIs => rule(word),
style => restyle(rule(&word.to_lowercase()), style),
}
}
const ACRONYM_SHADOWED: &[&str] = &["us"];
fn ci_lookup<T>(word: &str, get: impl Fn(&str) -> Option<T>) -> Option<(T, CaseStyle)> {
if let Some(v) = get(word) {
return Some((v, CaseStyle::AsIs));
}
let style = case_style(word);
if style != CaseStyle::AsIs {
let lower = word.to_lowercase();
if !ACRONYM_SHADOWED.contains(&lower.as_str())
&& let Some(v) = get(&lower)
{
return Some((v, style));
}
}
None
}
fn ci_lookup_with_base<T>(
word: &str,
base: &str,
get: impl Fn(&str) -> Option<T>,
) -> Option<(T, CaseStyle)> {
ci_lookup(word, &get).or_else(|| {
if base != word {
ci_lookup(base, &get)
} else {
None
}
})
}
pub struct English;
impl English {
pub fn noun(word: &str, number: &Number) -> String {
let base_word = base_lemma(word, |w| ci_lookup(w, get_plural).is_some());
match number {
Number::Singular => base_word.to_string(),
Number::Plural => {
if let Some((x, style)) = ci_lookup_with_base(word, base_word, get_plural) {
restyle(x.to_owned(), style)
} else {
rule_with_case(base_word, |w| EnglishCore::noun(w, number))
}
}
}
}
pub fn adj(word: &str, degree: &Degree) -> String {
let base_word = base_lemma(word, |w| ci_lookup(w, get_adjective_forms).is_some());
match degree {
Degree::Positive => base_word.to_owned(),
Degree::Comparative => {
match ci_lookup_with_base(word, base_word, get_adjective_forms) {
Some(((comp, _), style)) => restyle(comp.to_owned(), style),
None => rule_with_case(base_word, EnglishCore::comparative),
}
}
Degree::Superlative => {
match ci_lookup_with_base(word, base_word, get_adjective_forms) {
Some(((_, sup), style)) => restyle(sup.to_owned(), style),
None => rule_with_case(base_word, EnglishCore::superlative),
}
}
}
}
pub fn adverb(word: &str, degree: &Degree) -> String {
let base_word = base_lemma(word, |w| ci_lookup(w, get_adverb_forms).is_some());
match degree {
Degree::Positive => base_word.to_owned(),
Degree::Comparative => match ci_lookup_with_base(word, base_word, get_adverb_forms) {
Some(((comp, _), style)) => restyle(comp.to_owned(), style),
None => rule_with_case(base_word, EnglishCore::comparative_adverb),
},
Degree::Superlative => match ci_lookup_with_base(word, base_word, get_adverb_forms) {
Some(((_, sup), style)) => restyle(sup.to_owned(), style),
None => rule_with_case(base_word, EnglishCore::superlative_adverb),
},
}
}
pub fn verb(
word: &str,
person: &Person,
number: &Number,
tense: &Tense,
form: &Form,
) -> String {
let base_word = base_lemma(word, |w| ci_lookup(w, get_verb_forms).is_some());
match ci_lookup_with_base(word, base_word, get_verb_forms) {
Some((wordik, style)) => match (person, number, tense, form) {
(_, _, _, Form::Infinitive) => base_word.to_owned(),
(Person::Third, Number::Singular, Tense::Present, Form::Finite) => {
restyle(wordik.0.to_string(), style)
}
(_, _, Tense::Present, Form::Finite) => base_word.to_owned(),
(_, _, Tense::Present, Form::Participle) => restyle(wordik.2.to_owned(), style),
(_, _, Tense::Past, Form::Participle) => restyle(wordik.3.to_owned(), style),
(_, _, Tense::Past, Form::Finite) => restyle(wordik.1.to_owned(), style),
},
None => rule_with_case(base_word, |w| {
EnglishCore::verb(w, person, number, tense, form)
}),
}
}
pub fn pronoun(person: &Person, number: &Number, gender: &Gender, case: &Case) -> &'static str {
EnglishCore::pronoun(person, number, gender, case)
}
pub fn add_possessive(word: &str) -> String {
EnglishCore::add_possessive(word)
}
pub fn capitalize_first(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + c.as_str(),
}
}
}
pub fn count(noun: &str, count: u32) -> String {
if count == 1 {
English::noun(noun, &Number::Singular)
} else {
English::noun(noun, &Number::Plural)
}
}
pub fn count_with_number(noun: &str, amount: u32) -> String {
format!("{} {}", amount, count(noun, amount))
}
#[cfg(test)]
mod rule_table_sync_tests {
use super::{ADJECTIVE_MAP, ADVERB_MAP, PLURAL_MAP, VERB_MAP, canonical_sense_suffix_base};
use english_core::EnglishCore;
use english_core::grammar::{Form, Number, Person, Tense};
fn is_bare(key: &str) -> bool {
canonical_sense_suffix_base(key).is_none()
}
#[test]
fn bare_rows_hold_only_what_the_rules_cannot_predict() {
let nouns: Vec<_> = PLURAL_MAP.entries().map(|(k, v)| (*k, *v)).collect();
let verbs: Vec<_> = VERB_MAP.entries().map(|(k, v)| (*k, *v)).collect();
let adjs: Vec<_> = ADJECTIVE_MAP.entries().map(|(k, v)| (*k, *v)).collect();
let adverbs: Vec<_> = ADVERB_MAP.entries().map(|(k, v)| (*k, *v)).collect();
let mut redundant = Vec::new();
for (key, plural) in &nouns {
if is_bare(key) && EnglishCore::pluralize_noun(key) == *plural {
redundant.push(format!("noun {key} -> {plural} (rule already predicts it)"));
}
}
for (key, (third, past, pres_part, past_part)) in &verbs {
if !is_bare(key) {
continue;
}
let p = |tense: &Tense, form: &Form| {
EnglishCore::verb(key, &Person::Third, &Number::Singular, tense, form)
};
if p(&Tense::Present, &Form::Finite) == *third
&& p(&Tense::Past, &Form::Finite) == *past
&& p(&Tense::Present, &Form::Participle) == *pres_part
&& p(&Tense::Past, &Form::Participle) == *past_part
{
redundant.push(format!("verb {key} (rule already predicts all four forms)"));
}
}
for (key, (comp, sup)) in &adjs {
if is_bare(key)
&& EnglishCore::comparative(key) == *comp
&& EnglishCore::superlative(key) == *sup
{
redundant.push(format!(
"adj {key} -> {comp}/{sup} (rule already predicts it)"
));
}
}
for (key, (comp, sup)) in &adverbs {
if is_bare(key)
&& EnglishCore::comparative_adverb(key) == *comp
&& EnglishCore::superlative_adverb(key) == *sup
{
redundant.push(format!(
"adverb {key} -> {comp}/{sup} (rule already predicts it)"
));
}
}
assert!(
redundant.is_empty(),
"{} table row(s) are redundant with the regular rules — a core rule changed \
without regenerating: run `cargo xtask refresh-data`:\n {}",
redundant.len(),
redundant.join("\n ")
);
}
}