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 +++++++++++++++++++++++++++++++----------------- lib/mihi/src/lib.rs | 235 ++++++++++++++++++++++------------------- 2 files changed, 305 insertions(+), 203 deletions(-) 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), + } } } } diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs index 237fe84..11d269a 100644 --- a/lib/mihi/src/lib.rs +++ b/lib/mihi/src/lib.rs @@ -6,6 +6,8 @@ use std::io::prelude::*; use std::io::{self, BufRead, BufReader, Error}; use std::path::{Path, PathBuf}; +use rusqlite::types::{FromSql, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; +use rusqlite::Result; use rusqlite::{params, Connection}; mod migrate; @@ -192,6 +194,98 @@ impl std::fmt::Display for Gender { } } +/// Identifies the declension for a given word, and it allows to do SQL to/from +/// conversions. +#[derive(Clone, Debug)] +pub enum Declension { + First = 1, + Second, + Third, + Fourth, + Fifth, + Other, +} + +impl ToSql for Declension { + fn to_sql(&self) -> Result> { + Ok(ToSqlOutput::from(self.clone() as isize)) + } +} + +impl FromSql for Declension { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + let val = value.as_i64().unwrap_or(0); + + match val { + 1 => Ok(Declension::First), + 2 => Ok(Declension::Second), + 3 => Ok(Declension::Third), + 4 => Ok(Declension::Fourth), + 5 => Ok(Declension::Fifth), + _ => Ok(Declension::Other), + } + } +} + +impl std::fmt::Display for Declension { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Declension::First => write!(f, "1st (-ae)"), + Declension::Second => write!(f, "2nd (-ī)"), + Declension::Third => write!(f, "3rd (-is)"), + Declension::Fourth => write!(f, "4th (-ūs)"), + Declension::Fifth => write!(f, "5th (-eī/-ēī)"), + Declension::Other => write!(f, "other"), + } + } +} + +/// Identifies the conjugation for a given verb, and it allows to do SQL to/from +/// conversions. +#[derive(Clone, Debug)] +pub enum Conjugation { + First = 1, + Second, + Third, + ThirdIo, + Fourth, + Other, +} + +impl ToSql for Conjugation { + fn to_sql(&self) -> Result> { + Ok(ToSqlOutput::from(self.clone() as isize)) + } +} + +impl FromSql for Conjugation { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + let val = value.as_i64().unwrap_or(0); + + match val { + 1 => Ok(Conjugation::First), + 2 => Ok(Conjugation::Second), + 3 => Ok(Conjugation::Third), + 4 => Ok(Conjugation::ThirdIo), + 5 => Ok(Conjugation::Fourth), + _ => Ok(Conjugation::Other), + } + } +} + +impl std::fmt::Display for Conjugation { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Conjugation::First => write!(f, "1st (ā stems)"), + Conjugation::Second => write!(f, "2nd (ē stems)"), + Conjugation::Third => write!(f, "3rd (ĕ stems)"), + Conjugation::ThirdIo => write!(f, "3rd (-iō variants)"), + Conjugation::Fourth => write!(f, "4th (ī stems)"), + Conjugation::Other => write!(f, "other"), + } + } +} + #[derive(Clone, Debug, Default)] pub enum Language { #[default] @@ -380,9 +474,9 @@ pub fn adverb(word: &Word, related: &[Word]) -> String { } let part = word.real_particle(); - match word.declension_id { - Some(1 | 2) => format!("{part}ē"), - Some(3) => format!("{part}iter"), + match word.declension { + Some(Declension::First | Declension::Second) => format!("{part}ē"), + Some(Declension::Third) => format!("{part}iter"), _ => "".to_string(), } } @@ -406,8 +500,8 @@ pub struct Word { pub enunciated: String, pub particle: String, pub language: Language, - pub declension_id: Option, - pub conjugation_id: Option, + pub declension: Option, + pub conjugation: Option, pub kind: String, pub category: Category, pub regular: bool, @@ -425,8 +519,8 @@ impl Word { pub fn from( particle: String, category: Category, - declension_id: Option, - conjugation_id: Option, + declension: Option, + conjugation: Option, gender: Gender, kind: String, ) -> Word { @@ -435,8 +529,8 @@ impl Word { enunciated: "".to_string(), particle, category, - declension_id, - conjugation_id, + declension, + conjugation, kind, regular: true, locative: false, @@ -451,13 +545,6 @@ impl Word { } } - pub fn inflection_id(&self) -> Option { - if matches!(self.category, Category::Verb) { - return Some(self.conjugation_id.unwrap()); - } - self.declension_id - } - /// Returns whether the given flag is set to true on this word. pub fn is_flag_set(&self, flag: &str) -> bool { match self.flags.get(flag) { @@ -488,43 +575,6 @@ impl Word { } } -const DECLENSIONS_WITH_KINDS: &[&[&str]] = &[ - &["a"], - &["us", "um", "ius", "er/ir"], - &[ - "is", - "istem", - "pureistem", - "one", - "onenonistem", - "two", - "three", - "visvis", - "sussuis", - "bosbovis", - "iuppiteriovis", - ], - &["fus", "domusdomus"], - &["ies", "es"], - &["indeclinable"], -]; - -const ADJECTIVE_KINDS: &[&[&str]] = &[ - &["us", "er/ir"], - &[], - &[ - "one", - "onenonistem", - "two", - "three", - "unusnauta", - "unusnautaer/ir", - "duo", - "tres", - "mille", - ], -]; - /// List of boolean flags supported for words. pub const BOOLEAN_FLAGS: &[&str] = &[ "deponent", @@ -560,55 +610,26 @@ pub fn is_valid_word_flag(flag: &str) -> bool { /// Creates the given word into the database and returns its ID on success. pub fn create_word(word: Word) -> Result { match word.category { - Category::Noun => match word.declension_id { - Some(id @ 1..7) => { - if !DECLENSIONS_WITH_KINDS[id as usize - 1].contains(&word.kind.as_str()) { - return Err(format!("bad kind for declension '{id}'")); - } - } - Some(val) => return Err(format!("the declension ID '{val}' is not valid for nouns")), - None => { + Category::Noun | Category::Adjective => { + if word.declension.is_none() { return Err(String::from( - "you have to provide the declension ID for this noun", - )) + "you have to provide the declension for this verb", + )); } - }, - Category::Adjective => match word.declension_id { - Some(id @ (1 | 3)) => { - if !ADJECTIVE_KINDS[id as usize - 1].contains(&word.kind.as_str()) { - return Err(format!("bad kind for declension '{id}'")); - } - } - Some(val) => { - return Err(format!( - "the declension ID '{val}' is not valid for adjectives" - )) - } - None => { - return Err(String::from( - "you have to provide the declension ID for this adjective", - )) - } - }, - Category::Verb => match word.conjugation_id { - Some(1..6) => { - if word.kind.as_str() != "verb" { - return Err("bad kind for verb".to_string()); - } - } - Some(val) => return Err(format!("the conjugation ID '{val}' is not valid")), - None => { + } + Category::Verb => { + if word.conjugation.is_none() { return Err(String::from( - "you have to provide the conjugation ID for this verb", - )) + "you have to provide the conjugation for this verb", + )); } - }, + } Category::Adverb | Category::Preposition | Category::Conjunction | Category::Interjection | Category::Determiner => { - if word.declension_id.is_some() || word.conjugation_id.is_some() { + if word.declension.is_some() || word.conjugation.is_some() { return Err(format!("no inflection allowed for '{}'", word.category)); } } @@ -632,8 +653,8 @@ pub fn create_word(word: Word) -> Result { word.enunciated.trim(), word.particle.trim(), word.language as isize, - word.declension_id, - word.conjugation_id, + word.declension, + word.conjugation, word.kind.trim(), word.category as isize, word.regular, @@ -671,8 +692,8 @@ pub fn update_word(word: Word) -> Result<(), String> { word.id, word.enunciated, word.particle, - word.declension_id, - word.conjugation_id, + word.declension, + word.conjugation, word.kind, word.category as isize, word.regular, @@ -803,8 +824,8 @@ pub fn select_related_words(word: &Word) -> Result<[Vec; 5], String> { enunciated: row.get(1).unwrap(), particle: row.get(2).unwrap(), language: row.get::(3).unwrap().try_into()?, - declension_id: row.get(4).unwrap(), - conjugation_id: row.get(5).unwrap(), + declension: row.get(4).unwrap(), + conjugation: row.get(5).unwrap(), kind: row.get(6).unwrap(), category: row.get::(7).unwrap().try_into()?, regular: row.get(8).unwrap(), @@ -843,8 +864,8 @@ pub fn find_by(enunciated: &str) -> Result { enunciated: row.get(1).unwrap(), particle: row.get(2).unwrap(), language: row.get::(3).unwrap().try_into()?, - declension_id: row.get(4).unwrap(), - conjugation_id: row.get(5).unwrap(), + declension: row.get(4).unwrap(), + conjugation: row.get(5).unwrap(), kind: row.get(6).unwrap(), category: row.get::(7).unwrap().try_into()?, regular: row.get(8).unwrap(), @@ -933,8 +954,8 @@ pub fn select_relevant_words( enunciated: row.get(1).unwrap(), particle: row.get(2).unwrap(), language: row.get::(3).unwrap().try_into()?, - declension_id: row.get(4).unwrap(), - conjugation_id: row.get(5).unwrap(), + declension: row.get(4).unwrap(), + conjugation: row.get(5).unwrap(), kind: row.get(6).unwrap(), category: row.get::(7).unwrap().try_into()?, regular: row.get(8).unwrap(), @@ -1020,8 +1041,8 @@ pub fn select_words_except( enunciated: row.get(1).unwrap(), particle: row.get(2).unwrap(), language: row.get::(3).unwrap().try_into()?, - declension_id: row.get(4).unwrap(), - conjugation_id: row.get(5).unwrap(), + declension: row.get(4).unwrap(), + conjugation: row.get(5).unwrap(), kind: row.get(6).unwrap(), category: row.get::(7).unwrap().try_into()?, regular: row.get(8).unwrap(), @@ -1717,8 +1738,8 @@ pub fn get_adjective_table(word: &Word) -> Result<[DeclensionTable; 3], String> let kind_f = if word.kind.as_str() == "unusnauta" { &word.kind } else { - match word.declension_id { - Some(1 | 2) => &"a".to_string(), + match word.declension { + Some(Declension::First | Declension::Second) => &"a".to_string(), _ => &word.kind, } }; -- cgit v1.2.3