From 765b569282c0494b2c7bac54d895a63b0577072a Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Wed, 28 Jan 2026 21:42:00 +0100 Subject: Move declension/conjugation IDs into enums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There were quite a lot of hacks that came out of my laziness on refusing to give inflections their own proper enums. This commit introduces this, while also providing implementations for the traits involving SQL. This way, moving data in and out from these enums feels more seamless. One advantage from this move is that a lot of redundant checks are gone, as the validation checks are performed when fetching the data. This is one of those times in which type masturbation as encouraged by the Rust programming language actually makes sense. Last but not least, this conversion also allows us to perform a change on the creation/edit of words, as suggestions can be done for conjugations/declensions and their kinds. Signed-off-by: Miquel Sabaté Solà --- crates/cli/src/words.rs | 273 +++++++++++++++++++++++++++++++----------------- 1 file changed, 177 insertions(+), 96 deletions(-) (limited to 'crates/cli/src') diff --git a/crates/cli/src/words.rs b/crates/cli/src/words.rs index 40f3f26..953760a 100644 --- a/crates/cli/src/words.rs +++ b/crates/cli/src/words.rs @@ -4,7 +4,7 @@ use crate::locale::current_locale; use inquire::{Confirm, Editor, MultiSelect, Select, Text}; use mihi::{ adverb, comparative, joint_related_words, select_related_words, select_tags_for, superlative, - Category, Gender, Language, RelationKind, Word, + Category, Conjugation, Declension, Gender, Language, RelationKind, Word, }; use std::vec::IntoIter; @@ -98,7 +98,7 @@ fn get_initial_guess(value: &str) -> Word { return Word::from( first[0..first.len() - 1].to_string(), Category::Noun, - Some(1), + Some(Declension::First), None, Gender::Feminine, "a".to_string(), @@ -107,7 +107,7 @@ fn get_initial_guess(value: &str) -> Word { return Word::from( first[0..first.len() - 2].to_string(), Category::Noun, - Some(2), + Some(Declension::Second), None, Gender::Masculine, "us".to_string(), @@ -116,7 +116,7 @@ fn get_initial_guess(value: &str) -> Word { return Word::from( first[0..first.len() - 2].to_string(), Category::Noun, - Some(2), + Some(Declension::Second), None, Gender::Neuter, "um".to_string(), @@ -125,7 +125,7 @@ fn get_initial_guess(value: &str) -> Word { return Word::from( first[0..first.len() - 2].to_string(), Category::Noun, - Some(4), + Some(Declension::Fourth), None, Gender::Masculine, "fus".to_string(), @@ -134,7 +134,7 @@ fn get_initial_guess(value: &str) -> Word { return Word::from( first[0..first.len() - 1].to_string(), Category::Noun, - Some(4), + Some(Declension::Fourth), None, Gender::Masculine, "fus".to_string(), @@ -143,7 +143,7 @@ fn get_initial_guess(value: &str) -> Word { return Word::from( first[0..first.len() - 3].to_string(), Category::Noun, - Some(5), + Some(Declension::Fifth), None, Gender::Masculine, "ies".to_string(), @@ -152,7 +152,7 @@ fn get_initial_guess(value: &str) -> Word { return Word::from( first[0..first.len() - 2].to_string(), Category::Noun, - Some(5), + Some(Declension::Fifth), None, Gender::Masculine, "es".to_string(), @@ -161,7 +161,7 @@ fn get_initial_guess(value: &str) -> Word { return Word::from( second[0..second.len() - 2].to_string(), Category::Noun, - Some(3), + Some(Declension::Third), None, Gender::Masculine, "is".to_string(), @@ -207,6 +207,62 @@ fn get_translated<'a>(word: &'a Word, key: &'a str) -> Result<&'a String, String } } +fn prompt_declension(cat: &Category, declension: Declension) -> Result { + let options; + let idx; + + match cat { + Category::Noun => { + options = vec![ + Declension::First, + Declension::Second, + Declension::Third, + Declension::Fourth, + Declension::Fifth, + ]; + idx = declension as usize - 1; + } + Category::Adjective => { + options = vec![Declension::First, Declension::Third]; + idx = if matches!(declension, Declension::Third) { + 1 + } else { + 0 + }; + } + _ => panic!("bad parameter"), + } + + let Ok(result) = Select::new("Declension:", options) + .with_starting_cursor(idx) + .prompt() + else { + return Err("abort!".to_string()); + }; + + Ok(result) +} + +fn prompt_conjugation(conjugation: Conjugation) -> Result { + let options = vec![ + Conjugation::First, + Conjugation::Second, + Conjugation::Third, + Conjugation::ThirdIo, + Conjugation::Fourth, + ]; + let idx = conjugation as usize - 1; + + let Ok(result) = Select::new("Conjugation:", options) + .with_starting_cursor(idx) + .prompt() + else { + return Err("abort!".to_string()); + }; + + Ok(result) +} + // Interactively ask the user to provide information for a word by the given // `enunciated`. The default values will be based on the given `word` parameter. fn ask_for_word_based_on(enunciated: String, word: Word) -> Result { @@ -257,29 +313,68 @@ fn ask_for_word_based_on(enunciated: String, word: Word) -> Result _ => Gender::None, }; - let inflection_id = match category { - Category::Noun | Category::Adjective | Category::Verb => { - let Ok(inflection) = Text::new("Inflection:") - .with_initial_value(word.inflection_id().unwrap_or(0).to_string().as_str()) - .prompt() - else { - return Err("abort!".to_string()); - }; - let Ok(inflection_id) = inflection.parse::() else { - return Err(format!("bad value for inflection ID '{inflection}'")); - }; - Some(inflection_id) + let declension; + let conjugation; + match category { + Category::Noun | Category::Adjective => { + declension = Some(prompt_declension( + &category, + word.declension.clone().unwrap_or(Declension::First), + )?); + conjugation = None; } - _ => None, - }; + Category::Verb => { + declension = None; + conjugation = Some(prompt_conjugation( + word.conjugation.clone().unwrap_or(Conjugation::First), + )?); + } + _ => { + declension = None; + conjugation = None; + } + } - // TODO: refine guess once the inflection is known: select from possible values. let kind = match category { - Category::Noun | Category::Adjective => { - let Ok(kind) = Text::new("Kind:").with_initial_value(&word.kind).prompt() else { - return Err("abort!".to_string()); + Category::Noun => { + let options = match declension { + Some(Declension::First) => vec!["a"], + Some(Declension::Second) => vec!["us", "um", "ius", "er/ir"], + Some(Declension::Third) => vec![ + "is", + "istem", + "pureistem", + "one", + "onenonistem", + "two", + "three", + "visvis", + "sussuis", + "bosbovis", + "iuppiteriovis", + ], + Some(Declension::Fourth) => vec!["fus"], + Some(Declension::Fifth) => vec!["ies", "es"], + _ => panic!("shouldn't be here :D"), }; - kind.trim().to_string() + if options.len() == 1 { + options.first().unwrap().to_string() + } else { + match Select::new("Kind:", options).prompt() { + Ok(kind) => kind.to_string(), + Err(_) => return Err("abort!".to_string()), + } + } + } + Category::Adjective => { + let options = match declension { + Some(Declension::First) => vec!["us", "er/ir"], + _ => vec!["one", "onenonistem", "two", "three"], + }; + match Select::new("Kind:", options).prompt() { + Ok(kind) => kind.to_string(), + Err(_) => return Err("abort!".to_string()), + } } Category::Verb => String::from("verb"), _ => String::from("-"), @@ -316,10 +411,7 @@ fn ask_for_word_based_on(enunciated: String, word: Word) -> Result return Err("abort!".to_string()); }; let Ok(weight) = raw_weight.parse::() else { - return Err(format!( - "bad value for inflection ID '{}'", - inflection_id.unwrap_or(0) - )); + return Err("bad value".to_string()); }; if weight > 10 { return Err(format!( @@ -355,13 +447,13 @@ fn ask_for_word_based_on(enunciated: String, word: Word) -> Result enunciated, particle, language: Language::Latin, - declension_id: if matches!(category, Category::Verb) { + declension: if matches!(category, Category::Verb) { None } else { - inflection_id + declension }, - conjugation_id: if matches!(category, Category::Verb) { - inflection_id + conjugation: if matches!(category, Category::Verb) { + conjugation } else { None }, @@ -762,50 +854,52 @@ fn edit(mut args: IntoIter) -> i32 { } } -// Returns a string with a more human-readable declension kind. -fn humanize_kind(kind: &str) -> &str { +// Returns a string with a more human-readable declension kind. If the kind is +// self-explanatory, then None is returned (e.g. "a" is the only kind for the +// first declension, so it's redundant). +fn humanize_kind(kind: &str) -> Option<&str> { match kind { // Noun - "a" => "-a", - "us" => "-us", - "er/ir" => "-er/-ir", - "um" => "-um", - "ius" => "-ius; like 'fīlius'", - "is" => "-is", - "istem" => "i-stem; '-i-' also in the genitive plural", - "pureistem" => "pure i-stem; '-i-' also in the ablative singular", - "visvis" => "irregular 'vīs, vīs'", - "sussuis" => "irregular 'sūs, suis'", - "bosbovis" => "irregular 'bōs, bovis'", - "iuppiteriovis" => "irregular 'Iuppiter, Iovis'", - "fus" => "-u-", - "domusdomus" => "irregular 'domus, domūs/domī'", - "ies" => "-iēs; like 'diēs, diēī'", - "es" => "-ēs; like 'rēs, reī'", - "indeclinable" => "indeclinable", + "a" => None, + "us" => Some("regular -us"), + "er/ir" => Some("-er/-ir"), + "um" => Some("neuter -um"), + "ius" => Some("-ius; like 'fīlius'"), + "is" => Some("regular -is"), + "istem" => Some("i-stem; '-i-' also in the genitive plural"), + "pureistem" => Some("pure i-stem; '-i-' also in the ablative singular"), + "visvis" => Some("irregular 'vīs, vīs'"), + "sussuis" => Some("irregular 'sūs, suis'"), + "bosbovis" => Some("irregular 'bōs, bovis'"), + "iuppiteriovis" => Some("irregular 'Iuppiter, Iovis'"), + "fus" => None, + "domusdomus" => Some("irregular 'domus, domūs/domī'"), + "ies" => Some("-iēs; like 'diēs, diēī'"), + "es" => Some("-ēs; like 'rēs, reī'"), + "indeclinable" => Some("indeclinable"), // Adjective - "one" => "one termination adjective", - "onenonistem" => "one termination adjective; non i-stem like 'melior, melius'", - "two" => "two termination adjective", - "three" => "three termination adjective", - "unusnauta" => "'ūnus nauta' like 'ūnus, ūna, ūnum'", - "unusnautaer/ir" => "'ūnus nauta' like 'neuter, neutra, neutrum'", - "duo" => "number 'duo, duae, duo'", - "tres" => "number 'trēs, trēs, tria'", - "mille" => "number 'mīlle, mīlle'", + "one" => Some("one termination adjective"), + "onenonistem" => Some("one termination adjective; non i-stem like 'melior, melius'"), + "two" => Some("two termination adjective"), + "three" => Some("three termination adjective"), + "unusnauta" => Some("'ūnus nauta' like 'ūnus, ūna, ūnum'"), + "unusnautaer/ir" => Some("'ūnus nauta' like 'neuter, neutra, neutrum'"), + "duo" => Some("number 'duo, duae, duo'"), + "tres" => Some("number 'trēs, trēs, tria'"), + "mille" => Some("number 'mīlle, mīlle'"), // Others - "egonos" => "'ego, nōs'", - "demonstrative-weak" => "weak demonstrative", - "demonstrative-proximal" => "proximal demonstrative", - "demonstrative-distal" => "distal demonstrative", - "demonstrative-medial" => "medial demonstrative", - "demonstrative-idem" => "'īdem, eadem, idem' demonstrative", - "tuvos" => "'tū, vōs'", - "sesui" => "'sē, suī'", - - _ => kind, + "egonos" => Some("'ego, nōs'"), + "demonstrative-weak" => Some("weak demonstrative"), + "demonstrative-proximal" => Some("proximal demonstrative"), + "demonstrative-distal" => Some("distal demonstrative"), + "demonstrative-medial" => Some("medial demonstrative"), + "demonstrative-idem" => Some("'īdem, eadem, idem' demonstrative"), + "tuvos" => Some("'tū, vōs'"), + "sesui" => Some("'sē, suī'"), + + _ => Some(kind), } } @@ -868,35 +962,22 @@ fn title_for_word(word: &Word) -> String { format!("{}; {})", s, flags) } -fn humanize_conjugation(id: isize) -> String { - match id { - 1 => "1st (ā-stems)", - 2 => "2nd (ē-stems)", - 3 => "3rd (ĕ-stems)", - 4 => "3rd (-iō variants)", - 5 => "4th (ī-stems)", - _ => panic!("bad conjugation id {id}"), - } - .to_string() -} - fn show_info(word: Word) -> Result<(), String> { // Title. println!("Word: {}", title_for_word(&word)); // Conjugation, declension + kind. - match word.conjugation_id { - Some(id) => println!("Conjugation: {}", humanize_conjugation(id)), + match word.conjugation { + Some(ref conjugation) => println!("Conjugation: {}", conjugation), None => { - if let Some(did) = word.declension_id { - if did > 5 { - println!("Declension: {}", humanize_kind(&word.kind)); + if let Some(ref d) = word.declension { + if matches!(d, Declension::Other) { + println!("Declension: {}.", humanize_kind(&word.kind).unwrap_or("-")); } else { - println!( - "Declension: {} ({})", - word.declension_id.unwrap(), - humanize_kind(&word.kind) - ); + match humanize_kind(&word.kind) { + Some(k) => println!("Declension: {}; kind: {}.", d, k), + None => println!("Declension: {}", d), + } } } } -- cgit v1.2.3