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/words.rs | 184 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 165 insertions(+), 19 deletions(-) (limited to 'crates/cli/src/words.rs') 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/src/words.rs') 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/src/words.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