use crate::data_provider::card_stats::{ add_stat, delete_stat, load_stats_of_set, update_stat_score, }; use crate::AppState; use chrono::{DateTime, Utc}; use rand::distr::weighted::WeightedIndex; use rand::distr::Distribution; use rand::prelude::SliceRandom; use rand::rng; use rand::rngs::ThreadRng; use serde::{Deserialize, Serialize}; use std::cmp::min; use std::collections::HashMap; use std::sync::{Arc, Mutex}; const MAX_HISTORY_LEN: usize = 20; const MAX_HISTORY_LEN_PART: f32 = 0.33; const MAX_SCORE: i32 = 25; const FADE_PER_DAY: f32 = 0.95; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct WordData { pub id: u32, pub key: String, pub value: String, pub tags: String, pub additional: HashMap, pub group_id: u32, } impl WordData { pub fn new() -> Self { Self { id: 0, key: String::new(), value: String::new(), tags: String::new(), additional: Default::default(), group_id: 1, } } } #[derive(Clone)] pub struct WordGroup { pub id: u32, pub name: String, } #[derive(Clone, PartialEq)] pub struct CardStatistics { pub id: u32, pub word_id: u32, pub set_id: u32, pub last_open: DateTime, pub score: i32, } impl CardStatistics { pub fn update(&mut self, status: WordOpenMode) { match status { WordOpenMode::Easy => { self.score = (self.calculated_score() + 5.0).round() as i32; } WordOpenMode::Ok => self.score = (self.calculated_score() + 2.0).round() as i32, WordOpenMode::Hard => { self.score = (self.calculated_score() - 1.0).round() as i32; } WordOpenMode::None => { self.score = (self.calculated_score() * 0.5) as i32; } } if self.score < 1 { self.score = 1 } else if self.score > MAX_SCORE { self.score = 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 } } #[derive(Clone, Copy)] pub enum WordOpenMode { Easy, Ok, Hard, None, } #[derive(Clone)] pub struct CardSet { words: Vec, set: Vec, current_word_index: Option, state: Arc>, order_module: OrderModule, } #[derive(Clone)] pub struct CardSetSettings { pub id: u32, pub name: String, pub forward: String, pub backward: String, pub filter: String, pub count: Option, pub worst_words_list: Option>, pub open_mode: SetOrderMode } impl CardSet { pub fn new(settings: &CardSetSettings, state: Arc>) -> Self { let state_for = state.clone(); let state_locked = state.lock().unwrap(); let mut current_set = load_stats_of_set(&settings, &state_locked.connection); let last_list = settings.get_word_list(&state_locked); let saved_ids = current_set.iter().map(|l| l.word_id).collect::>(); let word_ids = last_list.iter().map(|l| l.id).collect::>(); last_list .iter() .filter(|word| !saved_ids.contains(&word.id)) .map(|word| CardStatistics { id: 0, word_id: word.id.clone(), last_open: Utc::now(), score: 1, set_id: settings.id.clone(), }) .for_each(|mut new_statistic| { add_stat(&mut new_statistic, &state_locked.connection); current_set.push(new_statistic); }); let mut index = 0; for stat in current_set.clone() { if !word_ids.contains(&stat.word_id) { delete_stat(&stat, &state_locked.connection); current_set.remove(index); } else { index += 1; } } Self { set: current_set, words: last_list, current_word_index: None, state: state_for, order_module: match settings.open_mode { SetOrderMode::Default => OrderModule::SemiRandomSRS(SemiRandomSRSModule::new()), SetOrderMode::TrainWorstFirst => { OrderModule::WorstWordsSRS(WorstWordsSRSModule::new()) } SetOrderMode::FullRandom => OrderModule::RandomSRS(RandomSRSModule::new()), }, } } pub fn next(&mut self) -> (WordData, CardStatistics) { let index = match self.order_module.clone() { OrderModule::SemiRandomSRS(mut module) => { if module.initializated == false { module.init(self) } let index = module.next(self); self.order_module = OrderModule::SemiRandomSRS(module); index } OrderModule::RandomSRS(mut module) => { if module.initializated == false { module.init(self) } let index = module.next(self); self.order_module = OrderModule::RandomSRS(module); index } OrderModule::WorstWordsSRS(mut module) => { if module.initializated == false { 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 let None = self.current_word_index { return; } let index = self.current_word_index.unwrap(); let word = &mut self.set[index]; word.update(status); match self.order_module.clone() { OrderModule::SemiRandomSRS(mut module) => { module.open(status, index, word.clone()); self.order_module = OrderModule::SemiRandomSRS(module); } OrderModule::RandomSRS(mut module) => { module.open(status, index, word.clone()); self.order_module = OrderModule::RandomSRS(module); } OrderModule::WorstWordsSRS(mut module) => { module.open(status, index, word.clone()); self.order_module = OrderModule::WorstWordsSRS(module); } } update_stat_score(word, &self.state.lock().unwrap().connection) } pub fn len(&self) -> usize { self.set.len() } } #[derive(Clone, PartialEq, Copy, Eq)] pub(crate) enum SetOrderMode { Default, TrainWorstFirst, FullRandom, } #[derive(Clone)] enum OrderModule { SemiRandomSRS(SemiRandomSRSModule), RandomSRS(RandomSRSModule), WorstWordsSRS(WorstWordsSRSModule), } trait SRSModule { fn next(&mut self, set: &mut CardSet) -> usize; fn open(&mut self, status: WordOpenMode, index: usize, updated_word: CardStatistics); fn init(&mut self, set: &mut CardSet); } #[derive(Clone)] struct RandomSRSModule { backet: Vec, initializated: bool, } impl RandomSRSModule { fn new() -> RandomSRSModule { Self{ backet: vec![], initializated: false, } } } impl SRSModule for RandomSRSModule { fn next(&mut self, set: &mut CardSet) -> usize { if self.backet.is_empty() { self.backet = (0..set.words.len()).collect::>(); self.backet.shuffle(&mut rand::rng()) } self.backet.pop().unwrap() } fn open(&mut self, _: WordOpenMode, _: usize, _: CardStatistics) {} fn init(&mut self, set: &mut CardSet) { self.initializated = true; self.backet = (0..set.words.len()).collect::>(); self.backet.shuffle(&mut rand::rng()) } } #[derive(Clone)] struct SemiRandomSRSModule { history: Vec, last_weights: WeightedIndex, generator: ThreadRng, initializated: bool, } impl SemiRandomSRSModule { fn new() -> SemiRandomSRSModule { SemiRandomSRSModule { history: vec![], last_weights: WeightedIndex::new([1.0]).unwrap(), generator: rng(), initializated: false, } } } impl SRSModule for SemiRandomSRSModule { fn next(&mut self, set: &mut CardSet) -> usize { let index = self.last_weights.sample(&mut self.generator); if self.history.contains(&index) { 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, status: WordOpenMode, index: usize, word: CardStatistics) { let new_weight = (100.0 / word.calculated_score()).powf(2.0); self.last_weights .update_weights(&[(index, &new_weight)]) .unwrap(); } fn init(&mut self, set: &mut CardSet) { self.initializated = 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: &CardSet) -> usize { min( MAX_HISTORY_LEN, (set.len() as f32 * MAX_HISTORY_LEN_PART) as usize, ) } } #[derive(Clone)] struct WorstWordsSRSModule { initializated: bool, } impl SRSModule for WorstWordsSRSModule { fn next(&mut self, set: &mut CardSet) -> usize { todo!() } fn open(&mut self, status: WordOpenMode, index: usize, updated_word: CardStatistics) { todo!() } fn init(&mut self, set: &mut CardSet) { todo!() } } impl WorstWordsSRSModule { fn new() -> WorstWordsSRSModule { WorstWordsSRSModule { initializated: false, } } }