From 76e4c5bd5016347dca083b6932e6adf03f398cad Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Mon, 11 Aug 2025 09:28:57 +0200 Subject: Provide initial implementation for word inflection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Word inflection for nouns, adjectives has been added so it can be initially be used in commands like 'words show'. Word categories which have no inflections (e.g. adverbs, conjunctions, et al) are skipped. Signed-off-by: Miquel Sabaté Solà --- crates/cli/src/inflection.rs | 284 +++++++++++++++++++++++++++++++++++++++++++ crates/cli/src/locale.rs | 38 ++++++ crates/cli/src/main.rs | 2 + crates/cli/src/run.rs | 37 +----- crates/cli/src/words.rs | 184 +++++++++++++++++++++++++--- 5 files changed, 492 insertions(+), 53 deletions(-) create mode 100644 crates/cli/src/inflection.rs create mode 100644 crates/cli/src/locale.rs (limited to 'crates/cli') diff --git a/crates/cli/src/inflection.rs b/crates/cli/src/inflection.rs new file mode 100644 index 0000000..1b9d932 --- /dev/null +++ b/crates/cli/src/inflection.rs @@ -0,0 +1,284 @@ +use mihi::{group_declension_inflections, Category, DeclensionInfo, DeclensionTable, Gender, Word}; + +fn get_inflected_from(word: &Word, row: &[DeclensionInfo; 2]) -> String { + if word.is_flag_set("onlysingular") { + row[0].inflected.join("/") + } else if word.is_flag_set("onlyplural") { + row[1].inflected.join("/") + } else { + format!( + "{}, {}", + row[0].inflected.join("/"), + row[1].inflected.join("/") + ) + } +} + +fn get_noun_table(word: &Word) -> Result { + let gender = match word.gender { + Gender::MasculineOrFeminine => Gender::Masculine as usize, + _ => word.gender as usize, + }; + group_declension_inflections(word, &word.kind, gender) +} + +fn print_noun_inflection(word: &Word) -> Result<(), String> { + let table = get_noun_table(word)?; + + println!("\n== Inflection ==\n"); + + println!( + "Nominative:\t{}", + get_inflected_from(&word, &table.nominative) + ); + println!("Vocative:\t{}", get_inflected_from(&word, &table.vocative)); + println!( + "Accusative:\t{}", + get_inflected_from(&word, &table.accusative) + ); + println!("Genitive:\t{}", get_inflected_from(&word, &table.genitive)); + println!("Dative:\t\t{}", get_inflected_from(&word, &table.dative)); + println!("Ablative:\t{}", get_inflected_from(&word, &table.ablative)); + if word.locative { + println!("Locative:\t{}", get_inflected_from(&word, &table.locative)); + } + + Ok(()) +} + +fn get_adjective_table(word: &Word) -> Result<[DeclensionTable; 3], String> { + let kind_f = match word.declension_id { + Some(1 | 2) => &"a".to_string(), + _ => &word.kind, + }; + let kind_n = if word.kind == "us" { + &"um".to_owned() + } else { + &word.kind + }; + + Ok([ + group_declension_inflections(word, &word.kind, Gender::Masculine as usize)?, + group_declension_inflections(word, kind_f, Gender::Feminine as usize)?, + group_declension_inflections(word, kind_n, Gender::Neuter as usize)?, + ]) +} + +fn print_adjective_inflection(word: &Word) -> Result<(), String> { + let tables = get_adjective_table(word)?; + + println!("\n== Inflection ==\n"); + + println!( + "Nominative:\t{} | {} | {}", + get_inflected_from(&word, &tables[0].nominative), + get_inflected_from(&word, &tables[1].nominative), + get_inflected_from(&word, &tables[2].nominative) + ); + println!( + "Vocative:\t{} | {} | {}", + get_inflected_from(&word, &tables[0].vocative), + get_inflected_from(&word, &tables[1].vocative), + get_inflected_from(&word, &tables[2].vocative) + ); + println!( + "Accusative:\t{} | {} | {}", + get_inflected_from(&word, &tables[0].accusative), + get_inflected_from(&word, &tables[1].accusative), + get_inflected_from(&word, &tables[2].accusative) + ); + println!( + "Genitive:\t{} | {} | {}", + get_inflected_from(&word, &tables[0].genitive), + get_inflected_from(&word, &tables[1].genitive), + get_inflected_from(&word, &tables[2].genitive) + ); + println!( + "Dative:\t\t{} | {} | {}", + get_inflected_from(&word, &tables[0].dative), + get_inflected_from(&word, &tables[1].dative), + get_inflected_from(&word, &tables[2].dative) + ); + println!( + "Ablative:\t{} | {} | {}", + get_inflected_from(&word, &tables[0].ablative), + get_inflected_from(&word, &tables[1].ablative), + get_inflected_from(&word, &tables[2].ablative) + ); + if word.locative { + println!( + "Locative:\t{} | {} | {}", + get_inflected_from(&word, &tables[0].locative), + get_inflected_from(&word, &tables[1].locative), + get_inflected_from(&word, &tables[2].locative) + ); + } + + Ok(()) +} + +pub fn print_full_inflection_for(word: Word) -> Result<(), String> { + if word.is_flag_set("indeclinable") { + return Ok(()); + } + + match word.category { + Category::Noun => print_noun_inflection(&word)?, + Category::Adjective => print_adjective_inflection(&word)?, + Category::Verb => todo!(), + Category::Pronoun => todo!(), + Category::Adverb + | Category::Preposition + | Category::Conjunction + | Category::Interjection + | Category::Determiner + | Category::Unknown => { + // Nothing to do. + } + } + // TODO: on the 'extra' info. + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn get_word(enunciated: &str) -> Word { + let words = mihi::select_enunciated(Some(enunciated.to_string())).unwrap(); + + assert_eq!(words.len(), 1); + + mihi::find_by(words.first().unwrap().as_str()).unwrap() + } + + fn stringify_with(word: &Word, table: &DeclensionTable) -> String { + let mut res = get_inflected_from(&word, &table.nominative); + res.push_str(" | "); + res.push_str(get_inflected_from(&word, &table.vocative).as_str()); + res.push_str(" | "); + res.push_str(get_inflected_from(&word, &table.accusative).as_str()); + res.push_str(" | "); + res.push_str(get_inflected_from(&word, &table.genitive).as_str()); + res.push_str(" | "); + res.push_str(get_inflected_from(&word, &table.dative).as_str()); + res.push_str(" | "); + res.push_str(get_inflected_from(&word, &table.ablative).as_str()); + if word.locative { + res.push_str(" | "); + res.push_str(get_inflected_from(&word, &table.locative).as_str()); + } + + res + } + + fn assert_noun_table(enunciated: &str, expected: &str) { + let word = get_word(enunciated); + let table = get_noun_table(&word).unwrap(); + + let res = stringify_with(&word, &table); + + assert_eq!(res, expected); + } + + fn assert_adjective_table(enunciated: &str, masculine: &str, feminine: &str, neuter: &str) { + let word = get_word(enunciated); + let tables = get_adjective_table(&word).unwrap(); + + let res = stringify_with(&word, &tables[0]); + assert_eq!(res, masculine); + + let res = stringify_with(&word, &tables[1]); + assert_eq!(res, feminine); + + let res = stringify_with(&word, &tables[2]); + assert_eq!(res, neuter); + } + + #[test] + fn test_nouns() { + assert_noun_table( + "rosa, rosae", + "rosa, rosae | rosa, rosae | rosam, rosās | rosae, rosārum | rosae, rosīs | rosā, rosīs", + ); + assert_noun_table( + "fīlia, fīliae", + "fīlia, fīliae | fīlia, fīliae | fīliam, fīliās | fīliae, fīliārum | fīliae, fīliīs/fīliābus | fīliā, fīliīs/fīliābus", + ); + assert_noun_table( + "dea, deae", + "dea, deae | dea, deae | deam, deās | deae, deārum | deae, deābus | deā, deābus", + ); + assert_noun_table( + "Rōma, Rōmae", + "Rōma | Rōma | Rōmam | Rōmae | Rōmae | Rōmā | Rōmae", + ); + assert_noun_table( + "lupus, lupī", + "lupus, lupī | lupe, lupī | lupum, lupōs | lupī, lupōrum | lupō, lupīs | lupō, lupīs", + ); + assert_noun_table( + "templum, templī", + "templum, templa | templum, templa | templum, templa | templī, templōrum | templō, templīs | templō, templīs", + ); + assert_noun_table( + "vir, virī", + "vir, virī | vir, virī | virum, virōs | virī, virōrum | virō, virīs | virō, virīs", + ); + assert_noun_table( + "liber, librī", + "liber, librī | liber, librī | librum, librōs | librī, librōrum | librō, librīs | librō, librīs", + ); + assert_noun_table( + "fīlius, fīliī", + "fīlius, fīliī | fīlī, fīliī | fīlium, fīliōs | fīlī/fīliī, fīliōrum | fīliō, fīliīs | fīliō, fīliīs", + ); + assert_noun_table( + "leō, leōnis", + "leō, leōnēs | leō, leōnēs | leōnem, leōnēs | leōnis, leōnum | leōnī, leōnibus | leōne, leōnibus", + ); + assert_noun_table( + "ovis, ovis", + "ovis, ovēs | ovis, ovēs | ovem, ovēs | ovis, ovium | ovī, ovibus | ove, ovibus", + ); + assert_noun_table( + "mare, maris", + "mare, maria | mare, maria | mare, maria | maris, marium/marum | marī, maribus | marī/mare, maribus", + ); + assert_noun_table( + "Iuppiter, Iovis", + "Iuppiter | Iuppiter | Iovem | Iovis | Iovī | Iove", + ); + assert_noun_table( + "portus, portūs", + "portus, portūs | portus, portūs | portum, portūs | portūs, portuum | portuī, portibus | portū, portibus", + ); + assert_noun_table( + "cornū, cornūs", + "cornū, cornua | cornū, cornua | cornū, cornua | cornūs, cornuum | cornuī, cornibus | cornū, cornibus", + ); + // TODO: domus + assert_noun_table( + "diēs, diēī", + "diēs, diēs | diēs, diēs | diem, diēs | diēī, diērum | diēī, diēbus | diē, diēbus", + ); + assert_noun_table( + "rēs, reī", + "rēs, rēs | rēs, rēs | rem, rēs | reī, rērum | reī, rēbus | rē, rēbus", + ); + } + + #[test] + fn test_adjectives() { + assert_adjective_table( + "novus, nova, novum", + "novus, novī | nove, novī | novum, novōs | novī, novōrum | novō, novīs | novō, novīs", + "nova, novae | nova, novae | novam, novās | novae, novārum | novae, novīs | novā, novīs", + "novum, nova | novum, nova | novum, nova | novī, novōrum | novō, novīs | novō, novīs", + ); + // TODO: pulcher + // TODO: unus nauta + // TODO: third + } +} diff --git a/crates/cli/src/locale.rs b/crates/cli/src/locale.rs new file mode 100644 index 0000000..f9d42b6 --- /dev/null +++ b/crates/cli/src/locale.rs @@ -0,0 +1,38 @@ +// Locale represents the locales accepted for delivering answers on this +// tool. That is, it's not about i18n on the strings for this application. but +// rather the different translations accepted in places like +// `Word.translations`. +pub enum Locale { + English, + Catalan, +} + +impl Locale { + // Returns the string representation for the locale's code. + pub fn to_code(&self) -> &str { + match self { + Self::English => "en", + Self::Catalan => "ca", + } + } +} + +impl std::fmt::Display for Locale { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::English => write!(f, "english"), + Self::Catalan => write!(f, "català"), + } + } +} + +/// Fetches the Locale object that is suitable for the current environment. +pub fn current_locale() -> Locale { + let raw_locale = std::env::var("LC_ALL").unwrap_or("en".to_string()); + + if raw_locale.starts_with("ca") { + Locale::Catalan + } else { + Locale::English + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index ffbcdae..62ffd45 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,5 +1,7 @@ mod exercises; +mod inflection; mod init; +mod locale; mod nuke; mod run; mod words; diff --git a/crates/cli/src/run.rs b/crates/cli/src/run.rs index 0046d47..3e1b866 100644 --- a/crates/cli/src/run.rs +++ b/crates/cli/src/run.rs @@ -6,6 +6,8 @@ use std::io::Write; use std::process::Command; use tempfile::NamedTempFile; +use crate::locale::{current_locale, Locale}; + // Maximum number of times a word has to be run in order to increase the number // of successful runs. const MAX_STEPS: usize = 5; @@ -26,34 +28,6 @@ fn help(msg: Option<&str>) { println!(" -k, --kind \t\tOnly ask for exercises for the given ."); } -// Locale represents the locales accepted for delivering answers on this -// tool. That is, it's not about i18n on the strings for this application. but -// rather the different translations accepted in places like -// `Word.translations`. -enum Locale { - English, - Catalan, -} - -impl Locale { - // Returns the string representation for the locale's code. - fn to_code(&self) -> &str { - match self { - Self::English => "en", - Self::Catalan => "ca", - } - } -} - -impl std::fmt::Display for Locale { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::English => write!(f, "english"), - Self::Catalan => write!(f, "català"), - } - } -} - // Run the quiz for all the given `words` while expecting answers to be // delivered in the given `locale`. fn run_words(words: Vec, locale: &Locale) -> i32 { @@ -323,12 +297,7 @@ pub fn run(args: Vec) { } } - let raw_locale = std::env::var("LC_ALL").unwrap_or("en".to_string()); - let locale = if raw_locale.starts_with("ca") { - Locale::Catalan - } else { - Locale::English - }; + let locale = current_locale(); let words = match category { Some(cat) => select_relevant_words(cat, &flags, 15), diff --git a/crates/cli/src/words.rs b/crates/cli/src/words.rs index 7db84a2..d387f00 100644 --- a/crates/cli/src/words.rs +++ b/crates/cli/src/words.rs @@ -1,3 +1,6 @@ +use crate::inflection::print_full_inflection_for; +use crate::locale::current_locale; + use inquire::{Confirm, Editor, Select, Text}; use mihi::{Category, Gender, Language, Word}; use std::vec::IntoIter; @@ -148,15 +151,22 @@ fn get_initial_guess(value: &str) -> Word { return Word::from( second[0..second.len() - 2].to_string(), Category::Noun, - Some(5), + Some(3), None, Gender::Masculine, - "es".to_string(), + "is".to_string(), ); } } - Word::default() + Word::from( + value.to_string(), + Category::Unknown, + None, + None, + Gender::None, + String::from("-"), + ) } // Remove comments from the "flags" text that was provided. @@ -236,18 +246,32 @@ fn ask_for_word_based_on(enunciated: String, word: Word) -> Result _ => Gender::None, }; - let Ok(kind) = Text::new("Kind:").with_initial_value(&word.kind).prompt() else { - return Err("abort!".to_string()); + let inflection_id = match category { + Category::Noun | Category::Adjective => { + 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) + } + _ => None, }; - let Ok(inflection) = Text::new("Inflection:") - .with_initial_value(word.inflection_id().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}'")); + // 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()); + }; + kind.trim().to_string() + } + Category::Verb => String::from("verb"), + _ => String::from("-"), }; let Ok(regular) = Confirm::new("Regular:").with_default(word.regular).prompt() else { @@ -267,7 +291,10 @@ 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}'")); + return Err(format!( + "bad value for inflection ID '{}'", + inflection_id.unwrap_or(0) + )); }; if weight > 10 { return Err(format!( @@ -306,10 +333,10 @@ fn ask_for_word_based_on(enunciated: String, word: Word) -> Result declension_id: if matches!(category, Category::Verb) { None } else { - Some(inflection_id) + inflection_id }, conjugation_id: if matches!(category, Category::Verb) { - Some(inflection_id) + inflection_id } else { None }, @@ -409,7 +436,6 @@ fn create(args: IntoIter) -> i32 { } } -// TODO: accept a --raw flag, which is implied on pipe fn ls(mut args: IntoIter) -> i32 { if args.len() > 1 { help(Some("error: words: too many filters")); @@ -509,8 +535,128 @@ fn edit(mut args: IntoIter) -> i32 { } } -fn show(mut _args: IntoIter) -> i32 { - todo!() +// Returns a string with a more human-readable declension kind. +fn humanize_kind(kind: &str) -> &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", + + // 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'", + + // 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, + } +} + +fn show_info(word: Word) -> Result<(), String> { + // Title. + match word.gender { + Gender::None => println!("Word: {} ({})", word.enunciated, word.category), + _ => println!( + "Word: {} ({} {})", + word.enunciated, + word.gender.abbrev(), + word.category + ), + } + + // Conjugation, declension + kind. + match word.conjugation_id { + Some(id) => println!("Conjugation: {}", id), + None => match word.declension_id { + Some(did) => { + if did > 5 { + println!("Declension: {}", humanize_kind(&word.kind)); + } else { + println!( + "Declension: {} ({})", + word.declension_id.unwrap(), + humanize_kind(&word.kind) + ); + } + } + None => {} + }, + }; + + // Show translation if available. + let locale = current_locale(); + if let Some(translation) = word.translation.get(locale.to_code()) { + let s = translation.as_str().unwrap_or(""); + if !s.is_empty() { + println!("Translation ({}): {}.", locale.to_code(), s); + } + } + + print_full_inflection_for(word)?; + + Ok(()) +} + +fn show(mut args: IntoIter) -> i32 { + if args.len() > 1 { + help(Some( + "error: words: only one argument. If it's an enunciate, wrap it in double quotes", + )); + return 1; + } + + let enunciated = match select_single_word(args.next()) { + Ok(word) => word, + Err(e) => { + println!("error: words: {e}."); + return 1; + } + }; + + let word = match mihi::find_by(enunciated.as_str()) { + Ok(word) => word, + Err(e) => { + println!("error: words: {e}."); + return 1; + } + }; + + if let Err(e) = show_info(word) { + println!("error: words: {e}."); + return 1; + } + + 0 } fn rm(mut args: IntoIter) -> i32 { -- cgit v1.2.3 From 6b098702c6f7df5d400b76031618506dbdda02a5 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Tue, 12 Aug 2025 09:36:10 +0200 Subject: Trim word parts on database insert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miquel Sabaté Solà --- crates/cli/src/words.rs | 4 +++- lib/mihi/src/lib.rs | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) (limited to 'crates/cli') diff --git a/crates/cli/src/words.rs b/crates/cli/src/words.rs index d387f00..ba86041 100644 --- a/crates/cli/src/words.rs +++ b/crates/cli/src/words.rs @@ -206,6 +206,7 @@ fn ask_for_word_based_on(enunciated: String, word: Word) -> Result else { return Err("abort!".to_string()); }; + let particle = particle.trim().to_string(); let categories = vec![ Category::Unknown, @@ -261,6 +262,7 @@ fn ask_for_word_based_on(enunciated: String, word: Word) -> Result } _ => None, }; + let kind = kind.trim().to_string(); // TODO: refine guess once the inflection is known: select from possible values. let kind = match category { @@ -366,7 +368,7 @@ fn ask_for_word_based_on(enunciated: String, word: Word) -> Result // from it, and insert it into the database. fn do_create(enunciated: String) -> Result<(), String> { let mut guess = get_initial_guess(enunciated.as_str()); - guess.enunciated = enunciated.clone(); + guess.enunciated = enunciated.trim().to_string(); let word = ask_for_word_based_on(enunciated.clone(), guess)?; match mihi::create_word(word) { diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs index 41a9953..36da6d2 100644 --- a/lib/mihi/src/lib.rs +++ b/lib/mihi/src/lib.rs @@ -402,12 +402,12 @@ pub fn create_word(word: Word) -> Result<(), String> { VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, \ datetime('now'), datetime('now'))", params![ - word.enunciated, - word.particle, + word.enunciated.trim(), + word.particle.trim(), word.language as usize, word.declension_id, word.conjugation_id, - word.kind, + word.kind.trim(), word.category as usize, word.regular, word.locative, -- cgit v1.2.3 From a6a46c403c780144e55a337a88af49fa9aa55acd Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Mon, 27 Oct 2025 09:31:48 +0100 Subject: Add basic validation for verbs and pronouns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miquel Sabaté Solà --- crates/cli/src/inflection.rs | 4 ++-- crates/cli/src/run.rs | 2 +- crates/cli/src/words.rs | 32 +++++++++++++++++++++++--------- lib/mihi/src/lib.rs | 16 ++++++++++++++-- 4 files changed, 40 insertions(+), 14 deletions(-) (limited to 'crates/cli') diff --git a/crates/cli/src/inflection.rs b/crates/cli/src/inflection.rs index 1b9d932..7d2f195 100644 --- a/crates/cli/src/inflection.rs +++ b/crates/cli/src/inflection.rs @@ -125,8 +125,8 @@ pub fn print_full_inflection_for(word: Word) -> Result<(), String> { match word.category { Category::Noun => print_noun_inflection(&word)?, Category::Adjective => print_adjective_inflection(&word)?, - Category::Verb => todo!(), - Category::Pronoun => todo!(), + Category::Verb => {} // TODO + Category::Pronoun => {} // TODO Category::Adverb | Category::Preposition | Category::Conjunction diff --git a/crates/cli/src/run.rs b/crates/cli/src/run.rs index 3e1b866..10e0721 100644 --- a/crates/cli/src/run.rs +++ b/crates/cli/src/run.rs @@ -23,7 +23,7 @@ fn help(msg: Option<&str>) { println!("Options:"); println!(" -c, --category \tOnly ask for words on the given ."); println!(" -e, --exercises\t\tOnly practice with exercises."); - println!(" -f, --flag\t\tFilter words by a boolean flag. Multiple flags can be provided."); + println!(" -f, --flag\t\t\tFilter words by a boolean flag. Multiple flags can be provided."); println!(" -h, --help\t\t\tPrint this message."); println!(" -k, --kind \t\tOnly ask for exercises for the given ."); } diff --git a/crates/cli/src/words.rs b/crates/cli/src/words.rs index ba86041..23a3946 100644 --- a/crates/cli/src/words.rs +++ b/crates/cli/src/words.rs @@ -248,7 +248,7 @@ fn ask_for_word_based_on(enunciated: String, word: Word) -> Result }; let inflection_id = match category { - Category::Noun | Category::Adjective => { + 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() @@ -262,7 +262,6 @@ fn ask_for_word_based_on(enunciated: String, word: Word) -> Result } _ => None, }; - let kind = kind.trim().to_string(); // TODO: refine guess once the inflection is known: select from possible values. let kind = match category { @@ -276,14 +275,28 @@ fn ask_for_word_based_on(enunciated: String, word: Word) -> Result _ => String::from("-"), }; - let Ok(regular) = Confirm::new("Regular:").with_default(word.regular).prompt() else { - return Err("abort!".to_string()); + let regular = if matches!( + category, + Category::Noun | Category::Adjective | Category::Verb + ) { + let Ok(regular) = Confirm::new("Regular:").with_default(word.regular).prompt() else { + return Err("abort!".to_string()); + }; + regular + } else { + true }; - let Ok(locative) = Confirm::new("Locative:") - .with_default(word.locative) - .prompt() - else { - return Err("abort!".to_string()); + + let locative = if matches!(category, Category::Noun) { + let Ok(locative) = Confirm::new("Locative:") + .with_default(word.locative) + .prompt() + else { + return Err("abort!".to_string()); + }; + locative + } else { + false }; let Ok(raw_weight) = Text::new("Weight:") @@ -597,6 +610,7 @@ fn show_info(word: Word) -> Result<(), String> { } // Conjugation, declension + kind. + // TODO: to_human match word.conjugation_id { Some(id) => println!("Conjugation: {}", id), None => match word.declension_id { diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs index 36da6d2..78400f2 100644 --- a/lib/mihi/src/lib.rs +++ b/lib/mihi/src/lib.rs @@ -375,6 +375,19 @@ pub fn create_word(word: Word) -> Result<(), String> { )) } }, + Category::Verb => match word.conjugation_id { + Some(1..6) => { + if word.kind.as_str() != "verb" { + return Err(format!("bad kind for verb")); + } + } + Some(val) => return Err(format!("the conjugation ID '{val}' is not valid")), + None => { + return Err(String::from( + "you have to provide the conjugation ID for this verb", + )) + } + }, Category::Adverb | Category::Preposition | Category::Conjunction @@ -384,8 +397,7 @@ pub fn create_word(word: Word) -> Result<(), String> { return Err(format!("no inflection allowed for '{}'", word.category)); } } - // TODO - _ => { + Category::Unknown | Category::Pronoun => { return Err(format!( "you cannot create a word from the '{}' category", word.category -- cgit v1.2.3 From 472d7fb9d37ec838fc69692cf24c8cb4a68af47a Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Mon, 12 Jan 2026 15:43:27 +0100 Subject: Various fixes on adjective inflection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This includes providing tests and fixes for feminine adjectives with contracted roots, and feminine versions of "unus nauta" adjectives. Signed-off-by: Miquel Sabaté Solà --- crates/cli/src/inflection.rs | 64 +++++++++++++++++++++++++++++++++++++++----- lib/mihi/src/lib.rs | 11 ++++---- 2 files changed, 64 insertions(+), 11 deletions(-) (limited to 'crates/cli') diff --git a/crates/cli/src/inflection.rs b/crates/cli/src/inflection.rs index 7d2f195..f42d1e8 100644 --- a/crates/cli/src/inflection.rs +++ b/crates/cli/src/inflection.rs @@ -47,10 +47,17 @@ fn print_noun_inflection(word: &Word) -> Result<(), String> { } fn get_adjective_table(word: &Word) -> Result<[DeclensionTable; 3], String> { - let kind_f = match word.declension_id { - Some(1 | 2) => &"a".to_string(), - _ => &word.kind, + // Unless the word is a special "unus nauta" variant, force 1/2 declension + // adjectives in the feminine to grab the "a" kind. + let kind_f = if word.kind.as_str() == "unusnauta" { + &word.kind + } else { + match word.declension_id { + Some(1 | 2) => &"a".to_string(), + _ => &word.kind, + } }; + let kind_n = if word.kind == "us" { &"um".to_owned() } else { @@ -277,8 +284,53 @@ mod tests { "nova, novae | nova, novae | novam, novās | novae, novārum | novae, novīs | novā, novīs", "novum, nova | novum, nova | novum, nova | novī, novōrum | novō, novīs | novō, novīs", ); - // TODO: pulcher - // TODO: unus nauta - // TODO: third + assert_adjective_table( + "pulcher, pulchra, pulchrum", + "pulcher, pulchrī | pulcher, pulchrī | pulchrum, pulchrōs | pulchrī, pulchrōrum | pulchrō, pulchrīs | pulchrō, pulchrīs", + "pulchra, pulchrae | pulchra, pulchrae | pulchram, pulchrās | pulchrae, pulchrārum | pulchrae, pulchrīs | pulchrā, pulchrīs", + "pulcher, pulchra | pulcher, pulchra | pulcher, pulchra | pulchrī, pulchrōrum | pulchrō, pulchrīs | pulchrō, pulchrīs", + ); + assert_adjective_table( + "ūnus, ūna, ūnum", + "ūnus, ūnī | ūne, ūnī | ūnum, ūnōs | ūnīus, ūnōrum | ūnī, ūnīs | ūnō, ūnīs", + "ūna, ūnae | ūna, ūnae | ūnam, ūnās | ūnīus, ūnārum | ūnī, ūnīs | ūnā, ūnīs", + "ūnum, ūna | ūnum, ūna | ūnum, ūna | ūnīus, ūnōrum | ūnī, ūnīs | ūnō, ūnīs", + ); + assert_adjective_table( + "ferōx, ferōx", + "ferōx, ferōcēs | ferōx, ferōcēs | ferōcem, ferōcēs | ferōcis, ferōcium | ferōcī, ferōcibus | ferōcī, ferōcibus", + "ferōx, ferōcēs | ferōx, ferōcēs | ferōcem, ferōcēs | ferōcis, ferōcium | ferōcī, ferōcibus | ferōcī, ferōcibus", + "ferōx, ferōcia | ferōx, ferōcia | ferōx, ferōcia | ferōcis, ferōcium | ferōcī, ferōcibus | ferōcī, ferōcibus", + ); + assert_adjective_table( + "gravis, grave", + "gravis, gravēs | gravis, gravēs | gravem/gravīs, gravēs | gravis, gravium | gravī, gravibus | gravī, gravibus", + "gravis, gravēs | gravis, gravēs | gravem/gravīs, gravēs | gravis, gravium | gravī, gravibus | gravī, gravibus", + "grave, gravia | grave, gravia | grave, gravia | gravis, gravium | gravī, gravibus | gravī, gravibus", + ); + assert_adjective_table( + "celer, celeris, celere", + "celer, celerēs | celer, celerēs | celerem, celerēs | celeris, celerium | celerī, celeribus | celerī, celeribus", + "celeris, celerēs | celeris, celerēs | celerem, celerēs | celeris, celerium | celerī, celeribus | celerī, celeribus", + "celere, celeria | celere, celeria | celere, celeria | celeris, celerium | celerī, celeribus | celerī, celeribus" + ); + assert_adjective_table( + "duo, duae, duo", + "duo | duo | duo/duōs | duōrum | duōbus | duōbus", + "duae | duae | duās | duārum | duābus | duābus", + "duo | duo | duo | duōrum | duōbus | duōbus", + ); + assert_adjective_table( + "trēs, trēs, tria", + "trēs | trēs | trēs/trīs | trium | tribus | tribus", + "trēs | trēs | trēs/trīs | trium | tribus | tribus", + "tria | tria | tria | trium | tribus | tribus", + ); + assert_adjective_table( + "mīlle, mīlle", + "mīlle, mīlia | mīlle, mīlia | mīlle, mīlia | mīlle, mīlium | mīlle, mīlibus | mīlle, mīlibus", + "mīlle, mīlia | mīlle, mīlia | mīlle, mīlia | mīlle, mīlium | mīlle, mīlibus | mīlle, mīlibus", + "mīlle, mīlia | mīlle, mīlia | mīlle, mīlia | mīlle, mīlium | mīlle, mīlibus | mīlle, mīlibus" + ); } } diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs index 78400f2..57dc93f 100644 --- a/lib/mihi/src/lib.rs +++ b/lib/mihi/src/lib.rs @@ -999,11 +999,11 @@ fn contract_root(word: &Word, case: usize, number: usize, gender: usize) -> bool return true; } - // Nominative/vocative singular are never contracted. The accusative is - // only not contracted on neuter words. + // Nominative/vocative singular are only contracted for feminine nouns. The + // accusative is only not contracted on neuter words. match case { - 0 | 1 => false, - 2 => gender != 3, + 0 | 1 => gender == Gender::Feminine as usize, + 2 => gender != Gender::Neuter as usize, _ => true, } } @@ -1095,7 +1095,8 @@ pub fn group_declension_inflections( "SELECT id, number, gender, \"case\", value, declension_id, \ kind, tense, mood, voice, person, conjugation_id \ FROM forms \ - WHERE kind = ?1 AND gender = ?2", + WHERE kind = ?1 AND gender = ?2 + ORDER BY id", ) .unwrap(); let mut it = stmt.query([kind, &gender.to_string()]).unwrap(); -- cgit v1.2.3