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à --- lib/mihi/src/lib.rs | 358 +++++++++++++++++++++++++++++++++++++++++++++++- lib/mihi/src/migrate.rs | 8 ++ 2 files changed, 360 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs index 439e92e..12953c3 100644 --- a/lib/mihi/src/lib.rs +++ b/lib/mihi/src/lib.rs @@ -70,6 +70,19 @@ pub enum Gender { None, } +impl Gender { + /// Returns a string containing the abbreviation for this gender. + pub fn abbrev(&self) -> &str { + match self { + Self::Masculine => "m.", + Self::Feminine => "f.", + Self::MasculineOrFeminine => "m./f.", + Self::Neuter => "n.", + Self::None => "(genderless)", + } + } +} + impl TryFrom for Gender { type Error = &'static str; @@ -174,7 +187,8 @@ pub fn add_language(language: String) -> Result<(), String> { /// Ensure that in the config path there is a fully initialized database. pub fn init_database() -> Result<(), String> { - let path = get_config_path()?.join("database.sqlite3"); + let name = &std::env::var("MIHI_DATABASE").unwrap_or("database.sqlite3".to_string()); + let path = get_config_path()?.join(name); let conn = match Connection::open(path) { Ok(handle) => handle, Err(e) => return Err(format!("could not initialize the database: {e}")), @@ -186,7 +200,7 @@ pub fn init_database() -> Result<(), String> { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct Word { pub id: i32, pub enunciated: String, @@ -237,11 +251,25 @@ impl Word { } } - pub fn inflection_id(&self) -> usize { + pub fn inflection_id(&self) -> Option { if matches!(self.category, Category::Verb) { - return self.conjugation_id.unwrap(); + 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) { + Some(value) => { + if let Some(b) = value.as_bool() { + b + } else { + false + } + } + None => false, } - self.declension_id.unwrap() } } @@ -347,6 +375,15 @@ pub fn create_word(word: Word) -> Result<(), String> { )) } }, + Category::Adverb + | Category::Preposition + | Category::Conjunction + | Category::Interjection + | Category::Determiner => { + if word.declension_id.is_some() || word.conjugation_id.is_some() { + return Err(format!("no inflection allowed for '{}'", word.category)); + } + } // TODO _ => { return Err(format!( @@ -584,7 +621,9 @@ pub fn delete_word(enunciated: &String) -> Result<(), String> { } fn get_connection() -> Result { - let path = get_config_path()?.join("database.sqlite3"); + let name = &std::env::var("MIHI_DATABASE").unwrap_or("database.sqlite3".to_string()); + let path = get_config_path()?.join(name); + match Connection::open(path) { Ok(handle) => Ok(handle), Err(_) => Err( @@ -812,3 +851,310 @@ pub fn select_relevant_exercises( } Ok(res) } + +#[derive(Debug, Default)] +pub struct DeclensionInfo { + pub inflected: Vec, +} + +#[derive(Debug, Default)] +pub struct DeclensionTable { + pub nominative: [DeclensionInfo; 2], + pub vocative: [DeclensionInfo; 2], + pub accusative: [DeclensionInfo; 2], + pub genitive: [DeclensionInfo; 2], + pub dative: [DeclensionInfo; 2], + pub ablative: [DeclensionInfo; 2], + pub locative: [DeclensionInfo; 2], +} + +impl DeclensionTable { + pub fn consume_blob( + &mut self, + case: usize, + blob: &Value, + word: &Word, + gender: usize, + add: bool, + ) { + if let Some(singular) = blob.get("singular") { + let values = singular.as_array().unwrap(); + for v in values { + let s = v.as_str().unwrap(); + if add { + self.add(word, case, 0, gender, s); + } else { + self.set(word, case, 0, gender, s); + } + } + } + + if let Some(plural) = blob.get("plural") { + let values = plural.as_array().unwrap(); + for v in values { + let s = v.as_str().unwrap(); + if add { + self.add(word, case, 1, gender, s); + } else { + self.set(word, case, 1, gender, s); + } + } + } + } + + pub fn set(&mut self, word: &Word, case: usize, number: usize, gender: usize, term: &str) { + match case { + 0 => { + self.nominative[number].inflected = inflect_from(word, case, number, gender, term); + } + 1 => { + self.vocative[number].inflected = inflect_from(word, case, number, gender, term); + } + 2 => { + self.accusative[number].inflected = inflect_from(word, case, number, gender, term); + } + 3 => { + self.genitive[number].inflected = inflect_from(word, case, number, gender, term); + } + 4 => { + self.dative[number].inflected = inflect_from(word, case, number, gender, term); + } + 5 => { + self.ablative[number].inflected = inflect_from(word, case, number, gender, term); + } + 6 => { + self.locative[number].inflected = inflect_from(word, case, number, gender, term); + } + _ => {} + } + } + + pub fn add(&mut self, word: &Word, case: usize, number: usize, gender: usize, term: &str) { + match case { + 0 => { + self.nominative[number] + .inflected + .append(&mut inflect_from(word, case, number, gender, term)); + } + 1 => { + self.vocative[number] + .inflected + .append(&mut inflect_from(word, case, number, gender, term)); + } + 2 => { + self.accusative[number] + .inflected + .append(&mut inflect_from(word, case, number, gender, term)); + } + 3 => { + self.genitive[number] + .inflected + .append(&mut inflect_from(word, case, number, gender, term)); + } + 4 => { + self.dative[number] + .inflected + .append(&mut inflect_from(word, case, number, gender, term)); + } + 5 => { + self.ablative[number] + .inflected + .append(&mut inflect_from(word, case, number, gender, term)); + } + 6 => { + self.locative[number] + .inflected + .append(&mut inflect_from(word, case, number, gender, term)); + } + _ => {} + } + } +} + +fn contract_root(word: &Word, case: usize, number: usize, gender: usize) -> bool { + // First off, check out that this is a word explicitely marked as to be + // contracted by either the flag or the kind. + if !word.is_flag_set("contracted_root") { + return false; + } + if word.kind != "er/ir" && word.kind != "unusnautaer/ir" { + return false; + } + + // All plurals have to be contracted. + if number == 1 { + return true; + } + + // Nominative/vocative singular are never contracted. The accusative is + // only not contracted on neuter words. + match case { + 0 | 1 => false, + 2 => gender != 3, + _ => true, + } +} + +fn should_use_first_root(word: &Word, case: usize, number: usize, gender: usize) -> bool { + // All plurals always follow `word.particle`. + if number == 1 { + return false; + } + + match case { + 0 | 1 => { + word.kind == "is" + || word.kind == "istem" + || word.kind == "pureistem" + || word.kind == "one" + || word.kind == "onenonistem" + } + 2 => { + // Only neuter words should consider this on the accusative. + if gender != 3 { + return false; + } + word.kind == "is" + || word.kind == "istem" + || word.kind == "pureistem" + || word.kind == "one" + || word.kind == "onenonistem" + } + _ => false, + } +} + +fn inflect_from(word: &Word, case: usize, number: usize, gender: usize, term: &str) -> Vec { + let mut inflections = vec![]; + + if !word.regular { + inflections.push(term.to_owned()); + } else if contract_root(word, case, number, gender) { + inflections.push( + word.particle[0..word.particle.len() - 2].to_string() + + &"r".to_owned() + + &term.to_owned(), + ); + } else if should_use_first_root(word, case, number, gender) { + let parts: Vec<&str> = word.enunciated.split(',').collect(); + inflections.push(parts.first().unwrap().to_string() + &term.to_owned()); + } else if word.kind == "ius" && number == 0 { + // Words of this kind are a bit troublesome on the singular, let's + // handle them now. + if case == 1 && word.is_flag_set("contracted_vocative") { + inflections + .push(word.particle[0..word.particle.len() - 1].to_string() + &term.to_owned()); + } else { + if case == 3 { + inflections + .push(word.particle[0..word.particle.len() - 1].to_string() + &term.to_owned()); + } + inflections.push(word.particle.to_string() + &term.to_owned()); + } + } else { + inflections.push(word.particle.clone() + &term.to_owned()); + } + + inflections +} + +fn case_str_to_i(key: &str) -> Result { + match key { + "nominative" => Ok(0), + "vocative" => Ok(1), + "accusative" => Ok(2), + "genitive" => Ok(3), + "dative" => Ok(4), + "ablative" => Ok(5), + "locative" => Ok(6), + _ => Err(format!("bad key '{}' for a case", key)), + } +} + +pub fn group_declension_inflections( + word: &Word, + kind: &String, + gender: usize, +) -> Result { + let conn = get_connection()?; + let mut stmt = conn + .prepare( + "SELECT id, number, gender, \"case\", value, declension_id, \ + kind, tense, mood, voice, person, conjugation_id \ + FROM forms \ + WHERE kind = ?1 AND gender = ?2", + ) + .unwrap(); + let mut it = stmt.query([kind, &gender.to_string()]).unwrap(); + + let mut table = DeclensionTable::default(); + + while let Some(row) = it.next().unwrap() { + // Fetch the number and account for defectives on number. + let number: usize = row.get(1).unwrap(); + if number == 0 && word.is_flag_set("onlyplural") { + continue; + } else if number == 1 && word.is_flag_set("onlysingular") { + continue; + } + + let case = row.get(3).unwrap(); + let term: String = row.get(4).unwrap(); + + table.add(word, case, number, gender, &term); + } + + if let Some(sets) = word.flags.get("sets") { + let object = sets.as_object().unwrap(); + + for (case_gender, blob) in object.iter() { + let case_gender_str = case_gender.as_str(); + match case_gender_str { + "masculine" | "feminine" | "neuter" => { + if (gender == 0 && case_gender_str == "masculine") + || (gender == 1 && case_gender_str == "feminine") + || (gender == 2 && case_gender_str == "neuter") + { + let inner = blob.as_object().unwrap(); + for (case, blob) in inner.iter() { + let case_i = case_str_to_i(case)?; + table.consume_blob(case_i, blob, word, gender, false); + } + } + } + _ => { + let case_i = case_str_to_i(case_gender)?; + table.consume_blob(case_i, blob, word, gender, false); + } + } + } + } + + if let Some(adds) = word.flags.get("adds") { + let object = adds.as_object().unwrap(); + + for (case_gender, blob) in object.iter() { + let case_gender_str = case_gender.as_str(); + match case_gender_str { + "masculine" | "feminine" | "neuter" => { + if (gender == 0 && case_gender_str == "masculine") + || (gender == 1 && case_gender_str == "feminine") + || (gender == 2 && case_gender_str == "neuter") + { + let inner = blob.as_object().unwrap(); + for (case, blob) in inner.iter() { + let case_i = case_str_to_i(case)?; + table.consume_blob(case_i, blob, word, gender, true); + } + } + } + _ => { + let case_i = case_str_to_i(case_gender)?; + table.consume_blob(case_i, blob, word, gender, true); + } + } + } + } + + Ok(table) +} diff --git a/lib/mihi/src/migrate.rs b/lib/mihi/src/migrate.rs index 97f32c9..f8fcc8d 100644 --- a/lib/mihi/src/migrate.rs +++ b/lib/mihi/src/migrate.rs @@ -1,5 +1,13 @@ use rusqlite::{Connection, Result}; +// TODO: not just the definition, but also the bootstrapping (into Git as there is nothing sensitive?). +// - conjugations +// - declensions +// - forms +// - languages +// - language_cases +// - word_relations + /// Use the given `connection` in order to initialize the database. pub fn init(connection: Connection) -> Result { connection.execute( -- cgit v1.2.3 From 83972171d712c60ddfaeb4a68ef1cfd8c314fc10 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Tue, 12 Aug 2025 09:28:42 +0200 Subject: Ensure a non-NULL value for words.succeeded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miquel Sabaté Solà --- lib/mihi/src/lib.rs | 7 ++++--- lib/mihi/src/migrate.rs | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs index 12953c3..41a9953 100644 --- a/lib/mihi/src/lib.rs +++ b/lib/mihi/src/lib.rs @@ -397,9 +397,9 @@ pub fn create_word(word: Word) -> Result<(), String> { match conn.execute( "INSERT INTO words (enunciated, particle, language_id, declension_id, \ conjugation_id, kind, category, regular, locative, \ - gender, suffix, flags, translation, weight, \ + gender, suffix, flags, translation, weight, succeeded, \ updated_at, created_at) \ - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, \ datetime('now'), datetime('now'))", params![ word.enunciated, @@ -415,7 +415,8 @@ pub fn create_word(word: Word) -> Result<(), String> { word.suffix, serde_json::to_string(&word.flags).unwrap(), serde_json::to_string(&word.translation).unwrap(), - word.weight + word.weight, + 0 ], ) { Ok(_) => Ok(()), diff --git a/lib/mihi/src/migrate.rs b/lib/mihi/src/migrate.rs index f8fcc8d..58a1681 100644 --- a/lib/mihi/src/migrate.rs +++ b/lib/mihi/src/migrate.rs @@ -26,8 +26,8 @@ CREATE TABLE IF NOT EXISTS "words" ( "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL, "suffix" varchar, - "language_id" integer, - "succeeded" integer, + "language_id" integer NOT NULL, + "succeeded" integer DEFAULT 0 NOT NULL, "steps" integer DEFAULT 0 NOT NULL, "translation" jsonb DEFAULT '{}', "pending" boolean DEFAULT 0, -- 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 'lib') 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 'lib') 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 'lib') 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