From 8499c476f9a1097b0a7aebe688c6d98d4b7d227b Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Tue, 20 Jan 2026 22:14:57 +0100 Subject: run: allow selection based on tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miquel Sabaté Solà --- crates/cli/src/run.rs | 67 +++++++++++++++++++++++++++++++++++++++++--------- lib/mihi/src/lib.rs | 67 +++++++++++++++++++++++++++++++++++++++++--------- testdata/test.sqlite3 | Bin 2916352 -> 2916352 bytes 3 files changed, 112 insertions(+), 22 deletions(-) diff --git a/crates/cli/src/run.rs b/crates/cli/src/run.rs index 45149ac..beafc72 100644 --- a/crates/cli/src/run.rs +++ b/crates/cli/src/run.rs @@ -33,6 +33,7 @@ fn help(msg: Option<&str>) { println!(" -h, --help\t\t\tPrint this message."); println!(" -i, --inflection\t\tOnly practice word inflections (completing enunciates, declensions and conjugations."); println!(" -k, --kind \t\tOnly ask for exercises for the given ."); + println!(" -t, --tag \t\tFilter words which match the given tag NAME. Multiple tags can be provided to match words with any of the tags provided."); } // Run the quiz for all the given `words` while expecting answers to be @@ -350,14 +351,39 @@ fn run_inflect_words(words: &Vec, locale: &Locale) -> bool { // Returns a vector of words which contain a randomized set of words from // different categories. -fn select_general_words(flags: &Vec) -> Result, String> { - let mut res = select_relevant_words(Category::Noun, flags, 4)?; - res.append(&mut select_relevant_words(Category::Adjective, flags, 2)?); - res.append(&mut select_relevant_words(Category::Verb, flags, 4)?); - res.append(&mut select_relevant_words(Category::Pronoun, flags, 1)?); - res.append(&mut select_relevant_words(Category::Adverb, flags, 2)?); - res.append(&mut select_relevant_words(Category::Preposition, flags, 1)?); - res.append(&mut select_relevant_words(Category::Conjunction, flags, 1)?); +fn select_general_words(flags: &[String], tags: &[String]) -> Result, String> { + let mut res = select_relevant_words(Category::Noun, flags, tags, 4)?; + res.append(&mut select_relevant_words( + Category::Adjective, + flags, + tags, + 2, + )?); + res.append(&mut select_relevant_words(Category::Verb, flags, tags, 4)?); + res.append(&mut select_relevant_words( + Category::Pronoun, + flags, + tags, + 1, + )?); + res.append(&mut select_relevant_words( + Category::Adverb, + flags, + tags, + 2, + )?); + res.append(&mut select_relevant_words( + Category::Preposition, + flags, + tags, + 1, + )?); + res.append(&mut select_relevant_words( + Category::Conjunction, + flags, + tags, + 1, + )?); Ok(res) } @@ -494,6 +520,7 @@ pub fn run(args: Vec) { let mut inflection_only = false; let mut endless = false; let mut flags: Vec = vec![]; + let mut tags: Vec = vec![]; while let Some(first) = it.next() { match first.as_str() { @@ -585,6 +612,22 @@ pub fn run(args: Vec) { } } } + "-t" | "--tag" => match it.next() { + Some(t) => { + let name = t.trim().to_string(); + if let Ok(results) = mihi::select_tag_names(&Some(name.clone())) { + if results.is_empty() { + println!("warning: practice: the tag '{}' does not exist.", name); + } else { + tags.push(name) + } + } + } + None => { + help(Some("error: practice: you have to provide a tag name")); + std::process::exit(1); + } + }, _ => { help(Some( format!("error: practice: unknown flag or command '{first}'").as_str(), @@ -597,9 +640,10 @@ pub fn run(args: Vec) { let locale = current_locale(); loop { + // Select the words depending on the selected category, flags, etc. let words = match category { - Some(cat) => select_relevant_words(cat, &flags, 15), - None => select_general_words(&flags), + Some(cat) => select_relevant_words(cat, &flags, &tags, 15), + None => select_general_words(&flags, &tags), }; if !exercises_only { @@ -611,7 +655,8 @@ pub fn run(args: Vec) { Some(cat) => vec![cat], None => vec![Category::Noun, Category::Adjective], }; - if let Ok(words_to_inflect) = mihi::select_words_except(&list, &cats, &flags) { + if let Ok(words_to_inflect) = mihi::select_words_except(&list, &cats, &flags, &tags) + { if !run_inflect_words(&words_to_inflect, &locale) { break; } diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs index c15e1ed..f563645 100644 --- a/lib/mihi/src/lib.rs +++ b/lib/mihi/src/lib.rs @@ -782,7 +782,7 @@ pub fn find_by(enunciated: &str) -> Result { // `flags` are set for a row. If no flags are given, then an empty string is // returned. Otherwise the string is prepended by an "AND" clause, meaning that // it expects the caller to have other clauses before this one. -fn flags_clause(flags: &Vec) -> String { +fn flags_clause(flags: &[String]) -> String { if flags.is_empty() { return "".to_string(); } @@ -796,15 +796,18 @@ fn flags_clause(flags: &Vec) -> String { } // Select a maximum of `number` words which match a given word `category` and -// have set one of the given boolean `flags`. +// have set one of the given boolean `flags`. You may also pass a `tags` vector +// which contains the name of the tags for which each word must have at least +// one match. pub fn select_relevant_words( category: Category, - flags: &Vec, + flags: &[String], + tags: &[String], number: isize, ) -> Result, String> { let conn = get_connection()?; - let mut stmt = conn - .prepare( + let mut stmt = if tags.is_empty() { + conn.prepare( format!( "SELECT id, enunciated, particle, language_id, declension_id, conjugation_id, \ kind, category, regular, locative, gender, suffix, translation, \ @@ -817,7 +820,26 @@ pub fn select_relevant_words( ) .as_str(), ) - .unwrap(); + .unwrap() + } else { + conn.prepare( + format!( + "SELECT w.id, w.enunciated, w.particle, w.language_id, w.declension_id, w.conjugation_id, \ + w.kind, w.category, w.regular, w.locative, w.gender, w.suffix, w.translation, \ + w.succeeded, w.steps, w.flags, w.weight \ + FROM words w \ + JOIN tag_associations ta ON w.id = ta.word_id \ + JOIN tags t ON t.id = ta.tag_id \ + WHERE w.category = ?1 AND t.name IN ({}) AND w.translation != '{{}}' {} \ + ORDER BY w.weight DESC, w.succeeded ASC, w.updated_at DESC + LIMIT ?2", + tags.iter().map(|t| format!("'{}'", t)).collect::>().join(", "), + flags_clause(flags) + ) + .as_str(), + ) + .unwrap() + }; let mut it = stmt.query([category as isize, number]).unwrap(); let mut res = vec![]; @@ -848,11 +870,13 @@ pub fn select_relevant_words( /// Select a set of words except for the ones passed in the `excluded` /// vector. You have to pass the categories to be selected via the `categories` /// parameter, which cannot be empty. It also accepts a set of boolean `flags` -/// as with functions like `select_relevant_words`. +/// as with functions like `select_relevant_words`; and the `tags` filtering +/// option. pub fn select_words_except( excluded: &[Word], categories: &[Category], - flags: &Vec, + flags: &[String], + tags: &[String], ) -> Result, String> { assert!(!categories.is_empty()); @@ -865,8 +889,8 @@ pub fn select_words_except( .join(", "); let conn = get_connection()?; - let mut stmt = conn - .prepare( + let mut stmt = if tags.is_empty() { + conn.prepare( format!( "SELECT id, enunciated, particle, language_id, declension_id, conjugation_id, \ kind, category, regular, locative, gender, suffix, translation, \ @@ -881,7 +905,28 @@ pub fn select_words_except( ) .as_str(), ) - .unwrap(); + .unwrap() + } else { + conn.prepare( + format!( + "SELECT w.id, w.enunciated, w.particle, w.language_id, w.declension_id, w.conjugation_id, \ + w.kind, w.category, w.regular, w.locative, w.gender, w.suffix, w.translation, \ + w.succeeded, w.steps, w.flags, w.weight \ + FROM words w \ + JOIN tag_associations ta ON w.id = ta.word_id \ + JOIN tags t ON t.id = ta.tag_id \ + WHERE w.id NOT IN ({}) AND t.name IN ({}) AND w.category IN ({}) AND w.translation != '{{}}' {} \ + ORDER BY w.weight DESC, w.succeeded ASC, w.updated_at DESC + LIMIT 5", + placeholders, + tags.iter().map(|t| format!("'{}'", t)).collect::>().join(", "), + cats, + flags_clause(flags) + ) + .as_str(), + ) + .unwrap() + }; let mut it = stmt.query(rusqlite::params_from_iter(ids)).unwrap(); let mut res = vec![]; diff --git a/testdata/test.sqlite3 b/testdata/test.sqlite3 index 96132a1..43d7ca0 100644 Binary files a/testdata/test.sqlite3 and b/testdata/test.sqlite3 differ -- cgit v1.2.3