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 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 crates/cli/src/inflection.rs (limited to 'crates/cli/src/inflection.rs') 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 + } +} -- 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/src/inflection.rs') 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/src/inflection.rs') 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