aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-07-30 21:21:19 +0200
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-07-30 21:21:19 +0200
commitab60cbfb64b43402e42e7cd823b53b03f651e535 (patch)
tree519f826423290e463c5395a6a850bb0c019e4f7d
parent7b7faf70dbdd83020b4bf6f9c3242a994c97665a (diff)
downloadmihi-ab60cbfb64b43402e42e7cd823b53b03f651e535.tar.gz
mihi-ab60cbfb64b43402e42e7cd823b53b03f651e535.zip
Add the -f/--flag flag to the 'practice' command
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
-rw-r--r--crates/cli/src/run.rs60
-rw-r--r--lib/mihi/src/lib.rs71
2 files changed, 110 insertions, 21 deletions
diff --git a/crates/cli/src/run.rs b/crates/cli/src/run.rs
index 4e6e707..0046d47 100644
--- a/crates/cli/src/run.rs
+++ b/crates/cli/src/run.rs
@@ -1,5 +1,5 @@
use inquire::{Confirm, Editor, Text};
-use mihi::{select_random_words, update_success, Category, Exercise, ExerciseKind, Word};
+use mihi::{select_relevant_words, update_success, Category, Exercise, ExerciseKind, Word};
use std::env;
use std::fs;
use std::io::Write;
@@ -21,6 +21,7 @@ fn help(msg: Option<&str>) {
println!("Options:");
println!(" -c, --category <CATEGORY>\tOnly ask for words on the given <CATEGORY>.");
println!(" -e, --exercises\t\tOnly practice with exercises.");
+ println!(" -f, --flag\t\tFilter words by a boolean flag. Multiple flags can be provided.");
println!(" -h, --help\t\t\tPrint this message.");
println!(" -k, --kind <KIND>\t\tOnly ask for exercises for the given <KIND>.");
}
@@ -95,14 +96,14 @@ fn run_words(words: Vec<Word>, locale: &Locale) -> i32 {
// Returns a vector of words which contain a randomized set of words from
// different categories.
-fn select_general_words() -> Result<Vec<Word>, String> {
- let mut res = select_random_words(Category::Noun, 4)?;
- res.append(&mut select_random_words(Category::Adjective, 2)?);
- res.append(&mut select_random_words(Category::Verb, 4)?);
- res.append(&mut select_random_words(Category::Pronoun, 1)?);
- res.append(&mut select_random_words(Category::Adverb, 2)?);
- res.append(&mut select_random_words(Category::Preposition, 1)?);
- res.append(&mut select_random_words(Category::Conjunction, 1)?);
+fn select_general_words(flags: &Vec<String>) -> Result<Vec<Word>, 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)?);
Ok(res)
}
@@ -143,7 +144,9 @@ fn is_executable(bin: &str) -> bool {
// Returns the string for the name of the command that should be used to show
// diffs.
fn diff_tool() -> Option<&'static str> {
- ["difft", "vimdiff", "diff"].into_iter().find(|&cmd| is_executable(cmd))
+ ["difft", "vimdiff", "diff"]
+ .into_iter()
+ .find(|&cmd| is_executable(cmd))
}
// Perform a diff with the `given` and the `expected` answers for an exercise
@@ -225,6 +228,7 @@ pub fn run(args: Vec<String>) {
let mut category = None;
let mut kind: Option<ExerciseKind> = None;
let mut exercises_only = false;
+ let mut flags: Vec<String> = vec![];
while let Some(first) = it.next() {
match first.as_str() {
@@ -262,6 +266,34 @@ pub fn run(args: Vec<String>) {
"-e" | "--exercises" => {
exercises_only = true;
}
+ "-f" | "--flag" => match it.next() {
+ Some(flag) => {
+ if mihi::is_valid_word_flag(flag.as_str()) {
+ if flags.iter().any(|s| s.as_str() == flag) {
+ println!(
+ "warning: practice: flag '{flag}' was provided multiple times"
+ );
+ } else {
+ flags.push(flag);
+ }
+ } else {
+ let supported = mihi::BOOLEAN_FLAGS.join(", ");
+ help(Some(
+ format!(
+ "error: practice: unknown flag value '{flag}'. You have to pick between: {supported}"
+ )
+ .as_str(),
+ ));
+ std::process::exit(1);
+ }
+ }
+ None => {
+ help(Some(
+ "error: practice: you have to provide a value for the flag",
+ ));
+ std::process::exit(1);
+ }
+ },
"-k" | "--kind" => {
if kind.is_some() {
help(Some(
@@ -273,9 +305,7 @@ pub fn run(args: Vec<String>) {
Some(k) => {
kind = match k.trim().to_lowercase().as_str().try_into() {
Ok(k) => Some(k),
- Err(e) => {
- return help(Some(format!("error: practice: {e}").as_str()))
- }
+ Err(e) => return help(Some(format!("error: practice: {e}").as_str())),
};
}
None => {
@@ -301,8 +331,8 @@ pub fn run(args: Vec<String>) {
};
let words = match category {
- Some(cat) => select_random_words(cat, 15),
- None => select_general_words(),
+ Some(cat) => select_relevant_words(cat, &flags, 15),
+ None => select_general_words(&flags),
};
let exercises = match mihi::select_relevant_exercises(kind, if exercises_only { 5 } else { 1 })
diff --git a/lib/mihi/src/lib.rs b/lib/mihi/src/lib.rs
index 7d13226..439e92e 100644
--- a/lib/mihi/src/lib.rs
+++ b/lib/mihi/src/lib.rs
@@ -282,6 +282,38 @@ const ADJECTIVE_KINDS: &[&[&str]] = &[
],
];
+/// List of boolean flags supported for words.
+pub const BOOLEAN_FLAGS: &[&str] = &[
+ "deponent",
+ "onlysingular",
+ "onlyplural",
+ "contracted_root",
+ "nonpositive",
+ "compsup_prefix",
+ "indeclinable",
+ "irregularsup",
+ "nopassive",
+ "nosupine",
+ "noperfect",
+ "nogerundive",
+ "impersonal",
+ "impersonalpassive",
+ "noimperative",
+ "noinfinitive",
+ "shortimperative",
+ "onlythirdpassive",
+ "enclitic",
+ "notcomparable",
+ "onlyperfect",
+ "semideponent",
+ "contracted_vocative",
+];
+
+/// Returns true if the given flag is supported by this application.
+pub fn is_valid_word_flag(flag: &str) -> bool {
+ BOOLEAN_FLAGS.contains(&flag)
+}
+
/// Creates the given word into the database.
pub fn create_word(word: Word) -> Result<(), String> {
match word.category {
@@ -458,17 +490,44 @@ pub fn find_by(enunciated: &str) -> Result<Word, String> {
}
}
-pub fn select_random_words(category: Category, number: usize) -> Result<Vec<Word>, String> {
+// Builds up a chain of OR clauses that check whether either of the given
+// `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>) -> String {
+ if flags.is_empty() {
+ return "".to_string();
+ }
+
+ let mut clauses: Vec<String> = vec![];
+ for flag in flags {
+ clauses.push(format!("json_extract(flags, '$.{flag}') = 1"));
+ }
+
+ "AND (".to_owned() + &clauses.join(" OR ") + ")"
+}
+
+// Select a maximum of `number` words which match a given word `category` and
+// have set one of the given boolean `flags`.
+pub fn select_relevant_words(
+ category: Category,
+ flags: &Vec<String>,
+ number: usize,
+) -> Result<Vec<Word>, String> {
let conn = get_connection()?;
let mut stmt = conn
.prepare(
- "SELECT id, enunciated, particle, language_id, declension_id, conjugation_id, \
+ format!(
+ "SELECT id, enunciated, particle, language_id, declension_id, conjugation_id, \
kind, category, regular, locative, gender, suffix, translation, \
succeeded, steps, flags, weight \
- FROM words \
- WHERE category = ?1 AND translation != '{}' \
- ORDER BY weight DESC, succeeded ASC, updated_at DESC
- LIMIT ?2",
+ FROM words \
+ WHERE category = ?1 AND translation != '{{}}' {} \
+ ORDER BY weight DESC, succeeded ASC, updated_at DESC
+ LIMIT ?2",
+ flags_clause(flags)
+ )
+ .as_str(),
)
.unwrap();
let mut it = stmt.query([category as usize, number]).unwrap();