diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/mihi/src/lib.rs | 388 | ||||
| -rw-r--r-- | lib/mihi/src/migrate.rs | 12 |
2 files changed, 384 insertions, 16 deletions
diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs index 439e92e..57dc93f 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<usize> 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<usize> { 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,8 +375,29 @@ pub fn create_word(word: Word) -> Result<(), String> { )) } }, - // TODO - _ => { + 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 + | Category::Interjection + | Category::Determiner => { + if word.declension_id.is_some() || word.conjugation_id.is_some() { + return Err(format!("no inflection allowed for '{}'", word.category)); + } + } + Category::Unknown | Category::Pronoun => { return Err(format!( "you cannot create a word from the '{}' category", word.category @@ -360,17 +409,17 @@ 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, - 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, @@ -378,7 +427,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(()), @@ -584,7 +634,9 @@ pub fn delete_word(enunciated: &String) -> Result<(), String> { } fn get_connection() -> Result<rusqlite::Connection, 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); + match Connection::open(path) { Ok(handle) => Ok(handle), Err(_) => Err( @@ -812,3 +864,311 @@ pub fn select_relevant_exercises( } Ok(res) } + +#[derive(Debug, Default)] +pub struct DeclensionInfo { + pub inflected: Vec<String>, +} + +#[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 only contracted for feminine nouns. The + // accusative is only not contracted on neuter words. + match case { + 0 | 1 => gender == Gender::Feminine as usize, + 2 => gender != Gender::Neuter as usize, + _ => 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<String> { + 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<usize, String> { + 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<DeclensionTable, String> { + 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 + ORDER BY id", + ) + .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..58a1681 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<usize> { connection.execute( @@ -18,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, |
