From 22b455a640e399eb4f5fc0dbcffc0fe4d3898177 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Mon, 24 Aug 2026 01:37:00 +0300 Subject: [PATCH] Splitting to deck and srs --- src/app.rs | 4 +- src/core.rs | 4 +- src/core/deck.rs | 246 +++++++++++++++++ src/core/srs.rs | 189 +++++++++++++ src/data_provider/card_sets.rs | 3 +- src/data_provider/card_stats.rs | 2 +- src/data_provider/history.rs | 3 +- src/lang.rs | 441 +------------------------------ src/pages/repetition.rs | 4 +- src/pages/repetition_settings.rs | 2 +- src/pages/repetitions.rs | 4 +- 11 files changed, 461 insertions(+), 441 deletions(-) create mode 100644 src/core/deck.rs create mode 100644 src/core/srs.rs diff --git a/src/app.rs b/src/app.rs index 2dd41e8..6ad52ca 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,14 +3,16 @@ use hashbrown::HashMap; use iced::{keyboard, window, Subscription}; use iced_core::{Font, Size, Theme}; use iced_core::window::Position; +#[cfg(target_os = "linux")] use iced_core::window::settings::PlatformSpecific; use log::info; use rusqlite::Connection; +use crate::core::deck::DeckSettings; use crate::data_provider::card_sets::load_sets; use crate::data_provider::settings::get_setting; use crate::data_provider::sqlite::default_connection; use crate::data_provider::words::{load_word_groups, load_words}; -use crate::lang::{DeckSettings, Id, WordData, WordGroup}; +use crate::lang::{Id, WordData, WordGroup}; use crate::navigation::{AppSettings, RootMessage, ScreenState}; use crate::navigation::RootMessage::Keyboard; pub const USER_FONT: Font = Font::with_name("Noto Sans JP"); diff --git a/src/core.rs b/src/core.rs index bb063eb..ed001e5 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1 +1,3 @@ -pub mod kana_set; \ No newline at end of file +pub mod kana_set; +pub mod srs; +pub mod deck; \ No newline at end of file diff --git a/src/core/deck.rs b/src/core/deck.rs new file mode 100644 index 0000000..8b47521 --- /dev/null +++ b/src/core/deck.rs @@ -0,0 +1,246 @@ +use rayon::iter::ParallelIterator; +use rayon::iter::IndexedParallelIterator; +use std::sync::Arc; +use std::time::Instant; +use chrono::{DateTime, Utc}; +use log::info; +use parking_lot::Mutex; +use rayon::prelude::IntoParallelRefIterator; +use rhai::{Engine, Scope}; +use crate::app::AppState; +use crate::core::srs::{OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule, WordOpenMode, WorstWordsSRSModule, FADE_PER_DAY, MAX_SCORE}; +use crate::data_provider::card_stats::{load_stats_of_deck, update_stat_score}; +use crate::data_provider::history::{push_note, HistoryItem}; +use crate::lang::{ Id, WordData}; + +#[derive(Clone)] +pub struct DeckSettings { + pub id: Id, + pub name: String, + pub forward: String, + pub backward: String, + pub filter: String, + pub count: Option, + pub worst_words_list: Option>, + pub open_mode: OrderMode, +} +impl DeckSettings { + pub(crate) fn with_name(name: String) -> DeckSettings { + DeckSettings { + id: 0.into(), + name, + forward: "".to_string(), + backward: "".to_string(), + filter: "true".to_string(), + count: None, + worst_words_list: None, + open_mode: OrderMode::Default, + } + } + pub fn check_filter(&self) -> bool { + let time = Instant::now(); + let engine = Engine::new(); + let ast = engine.compile(&self.filter); + info!("Compile time is {:?}", time.elapsed()); + ast.is_ok() + } + pub fn get_word_list(&self, state: &AppState) -> Vec { + let time = Instant::now(); + let mut list = vec![]; + let engine = Engine::new(); + + let ast = engine.compile(&self.filter); + if let Ok(ast) = ast { + let groups = &state.word_groups; + + list = state + .dictionary + .par_iter() + .enumerate() + .filter(|(_, word)| { + let mut more = rhai::Map::new(); + for iced in &word.additional { + more.insert(iced.0.clone().into(), iced.1.clone().into()); + } + let mut scope = Scope::new(); + scope + .push_constant("id", word.id.0 as i64) + .push_constant("key", word.key.clone()) + .push_constant("value", word.value.clone()) + .push_constant("tags", word.tags.clone()) + .push_constant("more", more) + .push_constant( + "group", + groups + .iter() + .find(|g| g.id == word.group_id) + .cloned() + .unwrap() + .name, + ); + + let result = engine.eval_ast_with_scope::(&mut scope, &ast); + result.is_ok() && result.unwrap() + }) + .map(|(index, _)| index) + .collect(); + + info!("Collecting available words is {:?}", time.elapsed()); + + list + } else { + list + } + } + pub fn require_speech(&self) -> bool { + self.forward == "speech" || self.backward == "speech" + } +} + +#[derive(Clone)] +pub struct DeckData { + pub(crate) words: Vec, + pub(crate) set: Vec, + current_word_index: Option, + state: Arc>, + order_module: OrderModule, + settings: DeckSettings, +} +impl DeckData { + pub fn new(settings: &DeckSettings, state: Arc>) -> Self { + let state_for = state.clone(); + let state_locked = state.lock(); + + let stats = load_stats_of_deck(settings, &state_locked.connection); + let words = stats + .iter() + .map(|stat| { + state_locked + .dictionary + .binary_search_by_key(&stat.word_id, |w| w.id) + .unwrap() + }) + .map(|index| &state_locked.dictionary[index]) + .cloned() + .collect(); + + Self { + set: stats, + words, + current_word_index: None, + state: state_for, + order_module: match settings.open_mode { + OrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()), + OrderMode::TrainWorstFirst => { + OrderModule::WorstWordsSRS(WorstWordsSRSModule::new()) + } + OrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()), + }, + settings: settings.clone(), + } + } + + pub fn next(&mut self) -> (WordData, CardStatistics) { + let index = match self.order_module.clone() { + OrderModule::SemiRandomSRS(mut module) => { + if !module.initialized { + module.init(self) + } + let index = module.next(self); + self.order_module = OrderModule::SemiRandomSRS(module); + index + } + OrderModule::RandomSRS(mut module) => { + if !module.initialized { + module.init(self) + } + let index = module.next(self); + self.order_module = OrderModule::RandomSRS(module); + index + } + OrderModule::WorstWordsSRS(mut module) => { + if !module.initialized { + module.init(self) + } + let index = module.next(self); + self.order_module = OrderModule::WorstWordsSRS(module); + index + } + }; + + self.current_word_index = Some(index); + (self.words[index].clone(), self.set[index].clone()) + } + pub fn open(&mut self, status: WordOpenMode) { + if self.current_word_index.is_none() { + return; + } + let index = self.current_word_index.unwrap(); + + let word : &mut CardStatistics = &mut self.set[index]; + let old_score = word.score; + word.update(status); + let new_score = word.score; + + match self.order_module { + OrderModule::SemiRandomSRS(ref mut module) => { + module.open(status, index, word); + } + OrderModule::RandomSRS(ref mut module) => { + module.open(status, index, word); + } + OrderModule::WorstWordsSRS(ref mut module) => { + module.open(status, index, word); + } + } + update_stat_score(word, &self.state.lock().connection); + let _ = push_note( + self.settings.id, + HistoryItem { + timestamp: Utc::now(), + word_id: word.word_id, + mode: status, + before: old_score, + after: new_score, + }, + ); + } + pub fn len(&self) -> usize { + self.set.len() + } +} + +#[derive(Clone, PartialEq)] +pub struct CardStatistics { + pub id: Id, + pub word_id: Id, + pub set_id: Id, + pub last_open: DateTime, + pub score: u8, +} +impl CardStatistics { + pub fn update(&mut self, status: WordOpenMode) { + match status { + WordOpenMode::Easy => { + self.score = (self.calculated_score() + 5.0).round() as u8; + } + WordOpenMode::Ok => self.score = (self.calculated_score() + 2.0).round() as u8, + WordOpenMode::Hard => { + self.score = (self.calculated_score() * 0.75).round() as u8; + } + WordOpenMode::None => { + self.score = (self.calculated_score() * 0.4) as u8; + } + } + + self.score = self.score.clamp(1, MAX_SCORE); + self.last_open = Utc::now(); + } + + pub fn calculated_score(&self) -> f32 { + let time = Utc::now() - self.last_open; + let days = time.num_days(); + let multiplier = FADE_PER_DAY.powi(days as i32); + (self.score as f32 * multiplier).max(1.0) + } +} \ No newline at end of file diff --git a/src/core/srs.rs b/src/core/srs.rs new file mode 100644 index 0000000..5f03fc8 --- /dev/null +++ b/src/core/srs.rs @@ -0,0 +1,189 @@ +use crate::core::deck::{CardStatistics, DeckData}; +use rand::distr::weighted::WeightedIndex; +use rand::prelude::ThreadRng; +use rand::prelude::{Distribution, SliceRandom}; +use rand::rng; +use rayon::iter::IndexedParallelIterator; +use rayon::iter::ParallelIterator; + +const MAX_HISTORY_LEN: usize = 20; +const MAX_HISTORY_LEN_PART: f32 = 0.33; +pub const MAX_SCORE: u8 = 25; +pub const FADE_PER_DAY: f32 = 0.95; + +#[derive(Clone, PartialEq, Copy, Eq)] +pub enum OrderMode { + Default, + TrainWorstFirst, + FullRandom, +} +#[derive(Clone, Copy, Eq, PartialEq, Default)] +pub enum AppendMode { + Full, + #[default] + Manual, +} +#[derive(Clone)] +pub enum OrderModule { + SemiRandomSRS(SemiRandomSRSModule), + RandomSRS(RandomSRSModule), + WorstWordsSRS(WorstWordsSRSModule), +} +pub trait SRSModule { + fn next(&mut self, set: &mut DeckData) -> usize; + fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &CardStatistics); + fn init(&mut self, set: &mut DeckData); +} +#[derive(Clone)] +pub struct RandomSRSModule { + basket: Vec, + pub(crate) initialized: bool, +} +impl RandomSRSModule { + pub(crate) fn new() -> RandomSRSModule { + Self { + basket: vec![], + initialized: false, + } + } +} +impl SRSModule for RandomSRSModule { + fn next(&mut self, set: &mut DeckData) -> usize { + match self.basket.pop() { + Some(index) => index, + None => { + self.basket = (0..set.words.len()).collect::>(); + self.basket.shuffle(&mut rand::rng()); + self.basket.pop().expect("empty basket") + } + } + } + + fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {} + + fn init(&mut self, set: &mut DeckData) { + self.initialized = true; + self.basket = (0..set.set.len()).collect::>(); + self.basket.shuffle(&mut rand::rng()) + } +} +#[derive(Clone)] +pub struct SemiRandomSRSModule { + history: Vec, + last_weights: WeightedIndex, + generator: ThreadRng, + pub(crate) initialized: bool, +} +impl SemiRandomSRSModule { + pub(crate) fn new() -> SemiRandomSRSModule { + SemiRandomSRSModule { + history: vec![], + last_weights: WeightedIndex::new([1.0]).unwrap(), + generator: rng(), + initialized: false, + } + } +} +impl SRSModule for SemiRandomSRSModule { + fn next(&mut self, set: &mut DeckData) -> usize { + let index = self.last_weights.sample(&mut self.generator); + + if self.history.contains(&index) && self.history.len() != 1 { + return self.next(set); + } + + if self.history.len() == self.history_len(set) { + self.history.remove(0); + } + self.history.push(index); + + index + } + fn open(&mut self, _: WordOpenMode, index: usize, word: &CardStatistics) { + let new_weight = (100.0 / word.calculated_score()).powf(2.0); + self.last_weights + .update_weights(&[(index, &new_weight)]) + .expect("update weights error"); + } + fn init(&mut self, set: &mut DeckData) { + self.initialized = true; + let weights = set + .set + .iter() + .map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0) + .collect::>(); + self.last_weights = WeightedIndex::new(weights).unwrap(); + } +} +impl SemiRandomSRSModule { + fn history_len(&self, set: &DeckData) -> usize { + ((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN) + } +} +#[derive(Clone)] +pub struct WorstWordsSRSModule { + pub(crate) initialized: bool, + pool_size: usize, + rounds_remaining: u8, + pool: Vec, + queue: Vec, + rounds_count: u8, +} +impl SRSModule for WorstWordsSRSModule { + fn next(&mut self, set: &mut DeckData) -> usize { + if self.rounds_remaining == 0 { + self.fill_pool(set); + self.rounds_remaining = self.rounds_count; + } + + match self.queue.pop() { + Some(index) => index, + None => { + self.queue.append(&mut self.pool.clone()); + self.rounds_remaining -= 1; + self.queue.pop().expect("empty queue") + } + } + } + + fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {} + + fn init(&mut self, _: &mut DeckData) { + self.initialized = true; + } +} +impl WorstWordsSRSModule { + pub(crate) fn new() -> WorstWordsSRSModule { + WorstWordsSRSModule { + initialized: false, + pool_size: 15, + rounds_count: 2, + rounds_remaining: 0, + pool: vec![], + queue: vec![], + } + } + fn fill_pool(&mut self, set: &DeckData) { + let mut sorted = set + .set + .clone() + .into_iter() + .zip(0..set.set.len()) + .collect::>(); + sorted.sort_by_key(|c| c.0.score); + let mut worst = sorted[0..self.pool_size] + .iter() + .map(|(_, index)| *index) + .collect::>(); + worst.shuffle(&mut rand::rng()); + self.pool = worst; + } +} + +#[derive(Clone, Copy)] +pub enum WordOpenMode { + Easy, + Ok, + Hard, + None, +} \ No newline at end of file diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index 7e223be..341bdb5 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -1,6 +1,7 @@ -use crate::lang::{DeckSettings, OrderMode}; +use crate::core::srs::{ OrderMode}; use rusqlite::Connection; +use crate::core::deck::DeckSettings; pub fn load_sets(connection: &Connection) -> Vec { let mut stmt = connection diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index 5103608..0cd1056 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,7 +1,7 @@ -use crate::lang::{CardStatistics, DeckSettings}; use rusqlite::{Connection, params}; use std::time::Instant; use log::info; +use crate::core::deck::{CardStatistics, DeckSettings}; pub fn load_stats_of_deck(set: &DeckSettings, connection: &Connection) -> Vec { let mut stmt = connection diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index 2c04dfc..880dacf 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -1,10 +1,11 @@ use crate::fs::*; -use crate::lang::{Id, WordOpenMode}; +use crate::lang::{Id}; use chrono::{DateTime, Utc}; use std::fs::{File, OpenOptions}; use std::io::Write; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +use crate::core::srs::WordOpenMode; use crate::notification::NotificationData; use crate::notification::NotificationData::PermissionDenied; diff --git a/src/lang.rs b/src/lang.rs index a22f31f..e3724b5 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,18 +1,3 @@ -use crate::data_provider::card_stats::load_stats_of_deck; -use crate::data_provider::card_stats::update_stat_score; -use crate::data_provider::history::{push_note, HistoryItem}; -use crate::app::AppState; -use chrono::{DateTime, Utc}; -use parking_lot::Mutex; -use rand::distr::weighted::WeightedIndex; -use rand::distr::Distribution; -use rand::prelude::SliceRandom; -use rand::rng; -use rand::rngs::ThreadRng; -use rayon::iter::IndexedParallelIterator; -use rayon::iter::IntoParallelRefIterator; -use rayon::iter::ParallelIterator; -use rhai::{Engine, Scope}; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; use rusqlite::ToSql; use std::cmp::PartialEq; @@ -20,16 +5,9 @@ use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::num::ParseIntError; use std::str::FromStr; -use std::sync::Arc; -use std::time::Instant; -use log::info; -const MAX_HISTORY_LEN: usize = 20; -const MAX_HISTORY_LEN_PART: f32 = 0.33; -const MAX_SCORE: u8 = 25; -const FADE_PER_DAY: f32 = 0.95; #[derive(Clone, PartialEq, Eq, Debug, Hash, Copy, Ord, PartialOrd)] -pub struct Id(u32); +pub struct Id(pub(crate) u32); pub const INVALID_ID: Id = Id(0); impl FromStr for Id { type Err = ParseIntError; @@ -83,7 +61,6 @@ impl Id { } } - #[derive(Clone, Debug)] pub struct WordData { pub id: Id, @@ -93,6 +70,13 @@ pub struct WordData { pub additional: HashMap, pub group_id: Id, } +#[derive(Clone)] +pub struct WordGroup { + pub id: Id, + pub name: String, +} + + impl WordData { pub fn new() -> Self { Self { @@ -105,412 +89,3 @@ impl WordData { } } } -#[derive(Clone)] -pub struct WordGroup { - pub id: Id, - pub name: String, -} -#[derive(Clone, PartialEq)] -pub struct CardStatistics { - pub id: Id, - pub word_id: Id, - pub set_id: Id, - pub last_open: DateTime, - pub score: u8, -} -impl CardStatistics { - pub fn update(&mut self, status: WordOpenMode) { - match status { - WordOpenMode::Easy => { - self.score = (self.calculated_score() + 5.0).round() as u8; - } - WordOpenMode::Ok => self.score = (self.calculated_score() + 2.0).round() as u8, - WordOpenMode::Hard => { - self.score = (self.calculated_score() * 0.75).round() as u8; - } - WordOpenMode::None => { - self.score = (self.calculated_score() * 0.4) as u8; - } - } - - self.score = self.score.clamp(1, MAX_SCORE); - self.last_open = Utc::now(); - } - - pub fn calculated_score(&self) -> f32 { - let time = Utc::now() - self.last_open; - let days = time.num_days(); - let multiplier = FADE_PER_DAY.powi(days as i32); - (self.score as f32 * multiplier).max(1.0) - } -} -#[derive(Clone, Copy)] -pub enum WordOpenMode { - Easy, - Ok, - Hard, - None, -} -#[derive(Clone)] -pub struct DeckData { - words: Vec, - set: Vec, - current_word_index: Option, - state: Arc>, - order_module: OrderModule, - settings: DeckSettings, -} -impl DeckData { - pub fn new(settings: &DeckSettings, state: Arc>) -> Self { - let state_for = state.clone(); - let state_locked = state.lock(); - - let stats = load_stats_of_deck(settings, &state_locked.connection); - let words = stats - .iter() - .map(|stat| { - state_locked - .dictionary - .binary_search_by_key(&stat.word_id, |w| w.id) - .unwrap() - }) - .map(|index| &state_locked.dictionary[index]) - .cloned() - .collect(); - - Self { - set: stats, - words, - current_word_index: None, - state: state_for, - order_module: match settings.open_mode { - OrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()), - OrderMode::TrainWorstFirst => { - OrderModule::WorstWordsSRS(WorstWordsSRSModule::new()) - } - OrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()), - }, - settings: settings.clone(), - } - } - - pub fn next(&mut self) -> (WordData, CardStatistics) { - let index = match self.order_module.clone() { - OrderModule::SemiRandomSRS(mut module) => { - if !module.initialized { - module.init(self) - } - let index = module.next(self); - self.order_module = OrderModule::SemiRandomSRS(module); - index - } - OrderModule::RandomSRS(mut module) => { - if !module.initialized { - module.init(self) - } - let index = module.next(self); - self.order_module = OrderModule::RandomSRS(module); - index - } - OrderModule::WorstWordsSRS(mut module) => { - if !module.initialized { - module.init(self) - } - let index = module.next(self); - self.order_module = OrderModule::WorstWordsSRS(module); - index - } - }; - - self.current_word_index = Some(index); - (self.words[index].clone(), self.set[index].clone()) - } - pub fn open(&mut self, status: WordOpenMode) { - if self.current_word_index.is_none() { - return; - } - let index = self.current_word_index.unwrap(); - - let word : &mut CardStatistics = &mut self.set[index]; - let old_score = word.score; - word.update(status); - let new_score = word.score; - - match self.order_module { - OrderModule::SemiRandomSRS(ref mut module) => { - module.open(status, index, word); - } - OrderModule::RandomSRS(ref mut module) => { - module.open(status, index, word); - } - OrderModule::WorstWordsSRS(ref mut module) => { - module.open(status, index, word); - } - } - update_stat_score(word, &self.state.lock().connection); - let _ = push_note( - self.settings.id, - HistoryItem { - timestamp: Utc::now(), - word_id: word.word_id, - mode: status, - before: old_score, - after: new_score, - }, - ); - } - pub fn len(&self) -> usize { - self.set.len() - } -} -#[derive(Clone, PartialEq, Copy, Eq)] -pub enum OrderMode { - Default, - TrainWorstFirst, - FullRandom, -} -#[derive(Clone, Copy, Eq, PartialEq, Default)] -pub enum AppendMode { - Full, - #[default] - Manual, -} -#[derive(Clone)] -enum OrderModule { - SemiRandomSRS(SemiRandomSRSModule), - RandomSRS(RandomSRSModule), - WorstWordsSRS(WorstWordsSRSModule), -} -trait SRSModule { - fn next(&mut self, set: &mut DeckData) -> usize; - fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &CardStatistics); - fn init(&mut self, set: &mut DeckData); -} -#[derive(Clone)] -struct RandomSRSModule { - basket: Vec, - initialized: bool, -} -impl RandomSRSModule { - fn new() -> RandomSRSModule { - Self { - basket: vec![], - initialized: false, - } - } -} -impl SRSModule for RandomSRSModule { - fn next(&mut self, set: &mut DeckData) -> usize { - match self.basket.pop() { - Some(index) => index, - None => { - self.basket = (0..set.words.len()).collect::>(); - self.basket.shuffle(&mut rand::rng()); - self.basket.pop().expect("empty basket") - } - } - } - - fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {} - - fn init(&mut self, set: &mut DeckData) { - self.initialized = true; - self.basket = (0..set.set.len()).collect::>(); - self.basket.shuffle(&mut rand::rng()) - } -} -#[derive(Clone)] -struct SemiRandomSRSModule { - history: Vec, - last_weights: WeightedIndex, - generator: ThreadRng, - initialized: bool, -} -impl SemiRandomSRSModule { - fn new() -> SemiRandomSRSModule { - SemiRandomSRSModule { - history: vec![], - last_weights: WeightedIndex::new([1.0]).unwrap(), - generator: rng(), - initialized: false, - } - } -} -impl SRSModule for SemiRandomSRSModule { - fn next(&mut self, set: &mut DeckData) -> usize { - let index = self.last_weights.sample(&mut self.generator); - - if self.history.contains(&index) && self.history.len() != 1 { - return self.next(set); - } - - if self.history.len() == self.history_len(set) { - self.history.remove(0); - } - self.history.push(index); - - index - } - fn open(&mut self, _: WordOpenMode, index: usize, word: &CardStatistics) { - let new_weight = (100.0 / word.calculated_score()).powf(2.0); - self.last_weights - .update_weights(&[(index, &new_weight)]) - .expect("update weights error"); - } - fn init(&mut self, set: &mut DeckData) { - self.initialized = true; - let weights = set - .set - .iter() - .map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0) - .collect::>(); - self.last_weights = WeightedIndex::new(weights).unwrap(); - } -} -impl SemiRandomSRSModule { - fn history_len(&self, set: &DeckData) -> usize { - ((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN) - } -} -#[derive(Clone)] -struct WorstWordsSRSModule { - initialized: bool, - pool_size: usize, - rounds_remaining: u8, - pool: Vec, - queue: Vec, - rounds_count: u8, -} -impl SRSModule for WorstWordsSRSModule { - fn next(&mut self, set: &mut DeckData) -> usize { - if self.rounds_remaining == 0 { - self.fill_pool(set); - self.rounds_remaining = self.rounds_count; - } - - match self.queue.pop() { - Some(index) => index, - None => { - self.queue.append(&mut self.pool.clone()); - self.rounds_remaining -= 1; - self.queue.pop().expect("empty queue") - } - } - } - - fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {} - - fn init(&mut self, _: &mut DeckData) { - self.initialized = true; - } -} -impl WorstWordsSRSModule { - fn new() -> WorstWordsSRSModule { - WorstWordsSRSModule { - initialized: false, - pool_size: 15, - rounds_count: 2, - rounds_remaining: 0, - pool: vec![], - queue: vec![], - } - } - fn fill_pool(&mut self, set: &DeckData) { - let mut sorted = set - .set - .clone() - .into_iter() - .zip(0..set.set.len()) - .collect::>(); - sorted.sort_by_key(|c| c.0.score); - let mut worst = sorted[0..self.pool_size] - .iter() - .map(|(_, index)| *index) - .collect::>(); - worst.shuffle(&mut rand::rng()); - self.pool = worst; - } -} -#[derive(Clone)] -pub struct DeckSettings { - pub id: Id, - pub name: String, - pub forward: String, - pub backward: String, - pub filter: String, - pub count: Option, - pub worst_words_list: Option>, - pub open_mode: OrderMode, -} -impl DeckSettings { - pub(crate) fn with_name(name: String) -> DeckSettings { - DeckSettings { - id: 0.into(), - name, - forward: "".to_string(), - backward: "".to_string(), - filter: "true".to_string(), - count: None, - worst_words_list: None, - open_mode: OrderMode::Default, - } - } - pub fn check_filter(&self) -> bool { - let time = Instant::now(); - let engine = Engine::new(); - let ast = engine.compile(&self.filter); - info!("Compile time is {:?}", time.elapsed()); - ast.is_ok() - } - pub fn get_word_list(&self, state: &AppState) -> Vec { - let time = Instant::now(); - let mut list = vec![]; - let engine = Engine::new(); - - let ast = engine.compile(&self.filter); - if let Ok(ast) = ast { - let groups = &state.word_groups; - - list = state - .dictionary - .par_iter() - .enumerate() - .filter(|(_, word)| { - let mut more = rhai::Map::new(); - for iced in &word.additional { - more.insert(iced.0.clone().into(), iced.1.clone().into()); - } - let mut scope = Scope::new(); - scope - .push_constant("id", word.id.0 as i64) - .push_constant("key", word.key.clone()) - .push_constant("value", word.value.clone()) - .push_constant("tags", word.tags.clone()) - .push_constant("more", more) - .push_constant( - "group", - groups - .iter() - .find(|g| g.id == word.group_id) - .cloned() - .unwrap() - .name, - ); - - let result = engine.eval_ast_with_scope::(&mut scope, &ast); - result.is_ok() && result.unwrap() - }) - .map(|(index, _)| index) - .collect(); - - info!("Collecting available words is {:?}", time.elapsed()); - - list - } else { - list - } - } - pub fn require_speech(&self) -> bool { - self.forward == "speech" || self.backward == "speech" - } -} diff --git a/src/pages/repetition.rs b/src/pages/repetition.rs index 95e020a..a5ab536 100644 --- a/src/pages/repetition.rs +++ b/src/pages/repetition.rs @@ -1,7 +1,7 @@ use crate::app::AppState; use crate::data_provider::voice::get_voice; use crate::lang::Id; -use crate::lang::{CardStatistics, DeckData, DeckSettings, WordData, WordOpenMode}; +use crate::lang::{ WordData}; use crate::navigation::Page::PreviousPage; use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage}; use crate::notification::{NotificationData, NotificationRequest}; @@ -17,6 +17,8 @@ use rodio::MixerDeviceSink; use std::collections::HashSet; use std::sync::Arc; use tokio::task::spawn_blocking; +use crate::core::deck::{CardStatistics, DeckData, DeckSettings}; +use crate::core::srs::{ WordOpenMode}; pub struct RepetitionState { state: Arc>, diff --git a/src/pages/repetition_settings.rs b/src/pages/repetition_settings.rs index 55262cb..2607a4c 100644 --- a/src/pages/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -1,7 +1,6 @@ use crate::data_provider::card_sets::{delete_set, update_deck}; use crate::data_provider::card_stats::{delete_stats, load_stats_of_deck}; -use crate::lang::DeckSettings; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::pages::repetition_settings::RepetitionSettingsMessage::*; @@ -16,6 +15,7 @@ use parking_lot::Mutex; use std::sync::Arc; use std::time::Duration; use crate::app::AppState; +use crate::core::deck::DeckSettings; pub struct RepetitionSettingsState { set: DeckSettings, diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 123c436..3e8381a 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -1,7 +1,7 @@ use crate::data_provider::card_sets::update_deck; use crate::data_provider::card_stats::{add_stat_list, load_stats_of_deck}; use crate::pages::history::HistoryState; -use crate::lang::{AppendMode, CardStatistics, DeckSettings, Id, OrderMode}; +use crate::lang::{Id}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::pages::repetition::RepetitionState; @@ -25,6 +25,8 @@ use parking_lot::Mutex; use std::ops::Deref; use std::sync::Arc; use crate::app::AppState; +use crate::core::deck::{CardStatistics, DeckSettings}; +use crate::core::srs::{AppendMode, OrderMode}; #[derive(Clone)] pub struct RepetitionsState {