From a642a8fda402b1e8631362448bc3e47197ceca84 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Sat, 29 Aug 2026 21:31:54 +0300 Subject: [PATCH] rename tables --- sql/{card_set.sql => decks.sql} | 2 +- sql/history.sql | 7 +- sql/{card_stats.sql => states.sql} | 4 +- sql/{word_group.sql => word_groups.sql} | 2 +- sql/words.sql | 4 +- src/core/deck.rs | 63 +++++++++------ src/core/srs.rs | 100 ++++++++++++++++++++++-- src/data_provider/card_sets.rs | 8 +- src/data_provider/card_stats.rs | 8 +- src/data_provider/sqlite.rs | 8 +- src/data_provider/words.rs | 8 +- 11 files changed, 158 insertions(+), 56 deletions(-) rename sql/{card_set.sql => decks.sql} (88%) rename sql/{card_stats.sql => states.sql} (89%) rename sql/{word_group.sql => word_groups.sql} (75%) diff --git a/sql/card_set.sql b/sql/decks.sql similarity index 88% rename from sql/card_set.sql rename to sql/decks.sql index 305aca2..1842aad 100644 --- a/sql/card_set.sql +++ b/sql/decks.sql @@ -1,4 +1,4 @@ -create table card_set +create table decks ( id INTEGER primary key autoincrement, diff --git a/sql/history.sql b/sql/history.sql index cfeff01..9355ab2 100644 --- a/sql/history.sql +++ b/sql/history.sql @@ -2,10 +2,11 @@ create table history ( id INTEGER primary key autoincrement, - set_id INTEGER not null references card_set on delete cascade, - stat_id INTEGER not null references card_stats on delete cascade, + set_id INTEGER not null references decks on delete cascade, + stat_id INTEGER not null references states on delete cascade, timestamp INTEGER not null, answer INTEGER not null, before INTEGER not null, - after INTEGER not null + after INTEGER not null, + fsrs INTEGER not null ); diff --git a/sql/card_stats.sql b/sql/states.sql similarity index 89% rename from sql/card_stats.sql rename to sql/states.sql index 92f8d75..32c0b1b 100644 --- a/sql/card_stats.sql +++ b/sql/states.sql @@ -1,4 +1,4 @@ -create table card_stats +create table states ( id INTEGER primary key autoincrement, @@ -6,7 +6,7 @@ create table card_stats references words on delete cascade, set_id INTEGER not null - references card_set + references decks on delete cascade, score INTEGER default 1 not null, last_opened INTEGER not null, diff --git a/sql/word_group.sql b/sql/word_groups.sql similarity index 75% rename from sql/word_group.sql rename to sql/word_groups.sql index 28cf3ac..8b10e51 100644 --- a/sql/word_group.sql +++ b/sql/word_groups.sql @@ -1,4 +1,4 @@ -create table word_group +create table word_groups ( id INTEGER primary key autoincrement, diff --git a/sql/words.sql b/sql/words.sql index e51e2e0..f883beb 100644 --- a/sql/words.sql +++ b/sql/words.sql @@ -7,7 +7,7 @@ create table words tags TEXT not null, more TEXT, group_id integer default 1 not null - constraint words_word_group_id_fk - references word_group + constraint words_word_groups_id_fk + references word_groups on update cascade on delete cascade ); \ No newline at end of file diff --git a/src/core/deck.rs b/src/core/deck.rs index 6b1e99e..2674687 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -1,13 +1,14 @@ use crate::app::AppState; +use crate::core::srs::OrderModule::{WorstWordsSRS, FSRS, RandomSRS, SemiRandomSRS}; use crate::core::srs::{ - OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule, WordOpenMode, - WorstWordsSRSModule, + FsrsModule, OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule, + WordOpenMode, WorstWordsSRSModule, }; use crate::data::entities::*; use crate::data_provider::card_stats::{load_stats_of_deck, update_stat_score}; use crate::data_provider::history::push_note; use crate::notification::{MaybeError, NotificationData}; -use chrono::{Local, Utc}; +use chrono::Utc; use parking_lot::Mutex; use std::sync::Arc; @@ -16,9 +17,9 @@ pub struct DeckData { pub(crate) words: Vec, pub(crate) set: Vec, current_word_index: Option, - fsrs_indices: Vec, state: Arc>, order_module: OrderModule, + open_mode: OrderMode, settings: DeckSettings, } impl DeckData { @@ -41,45 +42,40 @@ impl DeckData { .map(|index| &state_locked.dictionary[index]) .cloned() .collect(); - - let today = Local::now().date_naive(); + Ok(Self { - fsrs_indices: stats - .iter() - .enumerate() - .filter(|(_, c)| c.fsrs_open_day == today) - .map(|(i, _)| i) - .collect(), 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()), - }, + order_module: FSRS(FsrsModule::new()), + open_mode: settings.open_mode, settings: settings.clone(), }) } pub fn next(&mut self) -> (WordData, CardStatistics) { let index = match self.order_module { - OrderModule::SemiRandomSRS(ref mut module) => { + SemiRandomSRS(ref mut module) => { if !module.initialized { module.init(self.set.as_slice()) } module.next(self.set.as_slice()) } - OrderModule::RandomSRS(ref mut module) => { + + RandomSRS(ref mut module) => { if !module.initialized { module.init(self.set.as_slice()) } module.next(self.set.as_slice()) } - OrderModule::WorstWordsSRS(ref mut module) => { + WorstWordsSRS(ref mut module) => { + if !module.initialized { + module.init(self.set.as_slice()) + } + module.next(self.set.as_slice()) + } + FSRS(ref mut module) => { if !module.initialized { module.init(self.set.as_slice()) } @@ -98,15 +94,32 @@ impl DeckData { let new_score = word.score; match self.order_module { - OrderModule::SemiRandomSRS(ref mut module) => { + SemiRandomSRS(ref mut module) => { module.open(status, index, word); } - OrderModule::RandomSRS(ref mut module) => { + RandomSRS(ref mut module) => { module.open(status, index, word); } - OrderModule::WorstWordsSRS(ref mut module) => { + WorstWordsSRS(ref mut module) => { module.open(status, index, word); } + FSRS(ref mut module) => { + module.open(status, index, word); + } + } + + if let FSRS(ref fsrs) = self.order_module { + if fsrs.finished() { + self.order_module = match self.open_mode { + OrderMode::Default => { + SemiRandomSRS(SemiRandomSRSModule::new()) + } + OrderMode::TrainWorstFirst => { + WorstWordsSRS(WorstWordsSRSModule::new()) + } + OrderMode::FullRandom => RandomSRS(RandomSRSModule::new()), + } + } } update_stat_score(word, &self.state.lock().connection)?; let _ = push_note( diff --git a/src/core/srs.rs b/src/core/srs.rs index fbb668f..dc016ce 100644 --- a/src/core/srs.rs +++ b/src/core/srs.rs @@ -1,4 +1,6 @@ use crate::data::entities::CardStatistics; +use chrono::Local; +use fsrs::FSRS; use rand::distr::weighted::WeightedIndex; use rand::prelude::ThreadRng; use rand::prelude::{Distribution, SliceRandom}; @@ -8,7 +10,7 @@ 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; - +const PARTIAL_POOL_SIZE: usize = 10; #[derive(Clone, PartialEq, Copy, Eq)] pub enum OrderMode { Default, @@ -26,10 +28,11 @@ pub enum OrderModule { SemiRandomSRS(SemiRandomSRSModule), RandomSRS(RandomSRSModule), WorstWordsSRS(WorstWordsSRSModule), + FSRS(FsrsModule), } pub trait SRSModule { fn next(&mut self, set: &[CardStatistics]) -> usize; - fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &CardStatistics); + fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &mut CardStatistics); fn init(&mut self, set: &[CardStatistics]); } #[derive(Clone)] @@ -57,7 +60,7 @@ impl SRSModule for RandomSRSModule { } } - fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {} + fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardStatistics) {} fn init(&mut self, set: &[CardStatistics]) { self.initialized = true; @@ -97,7 +100,7 @@ impl SRSModule for SemiRandomSRSModule { index } - fn open(&mut self, _: WordOpenMode, index: usize, word: &CardStatistics) { + fn open(&mut self, _: WordOpenMode, index: usize, word: &mut CardStatistics) { let new_weight = (100.0 / word.calculated_score()).powf(2.0); self.last_weights .update_weights(&[(index, &new_weight)]) @@ -143,7 +146,7 @@ impl SRSModule for WorstWordsSRSModule { } } - fn open(&mut self, _: WordOpenMode, _: usize, _: &CardStatistics) {} + fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardStatistics) {} fn init(&mut self, _: &[CardStatistics]) { self.initialized = true; @@ -153,7 +156,7 @@ impl WorstWordsSRSModule { pub(crate) fn new() -> WorstWordsSRSModule { WorstWordsSRSModule { initialized: false, - pool_size: 15, + pool_size: PARTIAL_POOL_SIZE, rounds_count: 2, rounds_remaining: 0, pool: vec![], @@ -172,6 +175,91 @@ impl WorstWordsSRSModule { } } +#[derive(Clone)] +pub struct FsrsModule { + today_set: Vec, + solved: Vec, + current_group: Vec, + queue: Vec, + rng: ThreadRng, + fsrs: FSRS, + pub(crate) initialized: bool +} + +impl SRSModule for FsrsModule { + fn next(&mut self, set: &[CardStatistics]) -> usize { + if self.solved.len() == self.current_group.len() { + self.fill_partial_pool(); + } + + if self.queue.is_empty() { + self.fill_queue(); + self.queue.shuffle(&mut self.rng); + } + + self.queue.pop().expect("empty queue") + } + + fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &mut CardStatistics) { + let today = Local::now().date_naive(); + let delta = today - updated_word.fsrs_opened; + let delta_days = delta.num_days(); + + if updated_word.fsrs_open_day != today{ + self.solved.push(index); + } + } + + fn init(&mut self, set: &[CardStatistics]) { + self.rng = rng(); + let today = Local::now().date_naive(); + self.today_set = set + .iter() + .enumerate() + .filter(|(_, c)| c.fsrs_open_day <= today) + .map(|(index, _)| index) + .collect(); + self.today_set.shuffle(&mut self.rng); + self.fill_partial_pool(); + self.initialized = true; + } +} + +impl FsrsModule { + pub fn new() -> Self{ + Self{ + today_set: vec![], + current_group: vec![], + queue: vec![], + solved: vec![], + rng: Default::default(), + fsrs: FSRS::default(), + initialized: false, + } + } + fn fill_partial_pool(&mut self) { + self.current_group.clear(); + self.current_group + .extend(self.today_set.iter().take(PARTIAL_POOL_SIZE)); + self.current_group.iter().for_each(|_| { + self.today_set.remove(0); + }); + self.queue.clear(); + self.queue + .extend(self.current_group.clone()); + self.solved.clear(); + } + + fn fill_queue(&mut self) { + self.queue.clear(); + self.queue.extend(self.current_group.iter().filter(|c| !self.today_set.contains(*c))); + } + + pub fn finished(&self) -> bool { + self.today_set.len() == self.solved.len() + } +} + #[derive(Clone, Copy)] pub enum WordOpenMode { Easy, diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index a99f939..d85fcea 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -10,7 +10,7 @@ use rusqlite::Connection; pub fn load_sets(connection: &Connection) -> Result, NotificationData> { let mut stmt = connection - .prepare("SELECT id, name, forward, backward, filter FROM card_set") + .prepare("SELECT id, name, forward, backward, filter FROM decks") .expect("Failed to prepare state query"); let iter = stmt .query_map([], |row| { @@ -34,7 +34,7 @@ pub fn load_sets(connection: &Connection) -> Result, Notificat pub fn add_set(set: &mut DeckSettings, connection: &Connection) -> MaybeError { let index : u32 = connection .query_row( - "INSERT INTO card_set (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", + "INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", ( &set.name, &set.forward, @@ -55,7 +55,7 @@ pub fn update_deck(set: &mut DeckSettings, connection: &Connection) -> MaybeErro } else { connection .execute( - "UPDATE card_set SET name = ?1, forward = ?2, backward = ?3, filter = ?4 WHERE id = ?5", + "UPDATE decks SET name = ?1, forward = ?2, backward = ?3, filter = ?4 WHERE id = ?5", ( &set.name, &set.forward, @@ -74,7 +74,7 @@ pub fn delete_set(set: &DeckSettings, connection: &Connection) -> MaybeError { return Err(DbSaveError); } connection - .execute("DELETE FROM card_set WHERE id = ?1", (&set.id,)) + .execute("DELETE FROM decks WHERE id = ?1", (&set.id,)) .map_err(|_| DbSaveError)?; Ok(()) } diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index eab5f3f..83643d7 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -13,7 +13,7 @@ pub fn load_stats_of_deck( connection: &Connection, ) -> Result, NotificationData> { let mut stmt = connection - .prepare("SELECT id, word_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM card_stats WHERE set_id = ?1") + .prepare("SELECT id, word_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM states WHERE set_id = ?1") .expect("Failed to prepare state query"); let iter = stmt .query_map((set.id,), |row| { @@ -59,7 +59,7 @@ pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) { let mut stmt = tx .prepare( - "INSERT INTO card_stats (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)", + "INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)", ) .expect("Failed to prepare state query"); @@ -98,7 +98,7 @@ pub fn update_stat_score(stat: &CardStatistics, connection: &Connection) -> Mayb connection .execute( - "UPDATE card_stats SET score = ?1, last_opened = ?2 WHERE id = ?3", + "UPDATE states SET score = ?1, last_opened = ?2 WHERE id = ?3", (&stat.score, &stat.last_open.timestamp(), &stat.id), ) .map_err(|_| DbSaveError)?; @@ -117,7 +117,7 @@ pub fn delete_stats(stat: &[CardStatistics], connection: &Connection) -> MaybeEr .join(", "); connection .execute( - format!("DELETE FROM card_stats WHERE id in ({ids})").as_str(), + format!("DELETE FROM states WHERE id in ({ids})").as_str(), (), ) .map_err(|_| DbSaveError)?; diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index be04f07..32a7bc4 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -30,15 +30,15 @@ fn make_settings(conn: &Connection) -> Result<(), rusqlite::Error> { } fn make_card_set(conn: &Connection) -> Result<(), rusqlite::Error> { - let query = include_str!("../../sql/card_set.sql"); + let query = include_str!("../../sql/decks.sql"); conn.execute(query, ())?; Ok(()) } fn make_word_group(conn: &Connection) -> Result<(), rusqlite::Error> { - let query = include_str!("../../sql/word_group.sql"); + let query = include_str!("../../sql/word_groups.sql"); conn.execute(query, ())?; - conn.execute("insert into word_group (name) values (\"Слова\");", ())?; + conn.execute("insert into word_groups (name) values (\"Слова\");", ())?; Ok(()) } fn make_words(conn: &Connection) -> Result<(), rusqlite::Error> { @@ -47,7 +47,7 @@ fn make_words(conn: &Connection) -> Result<(), rusqlite::Error> { Ok(()) } fn make_card_stats(conn: &Connection) -> Result<(), rusqlite::Error> { - let query = include_str!("../../sql/card_stats.sql"); + let query = include_str!("../../sql/states.sql"); conn.execute(query, ())?; Ok(()) } diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index 7e20323..9eff98d 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -116,7 +116,7 @@ pub fn load_words(connection: &Connection) -> Result, Notification pub fn load_word_groups(connection: &Connection) -> Result, NotificationData> { let mut stmt = connection - .prepare("SELECT id, name FROM word_group") + .prepare("SELECT id, name FROM word_groups") .expect("Failed to prepare statement"); let group_iter = stmt .query_map([], |row| { @@ -138,7 +138,7 @@ pub fn load_word_groups(connection: &Connection) -> Result, Notif pub fn add_group(group: &mut WordGroup, connection: &Connection) -> MaybeError { let index: u32 = connection .query_row( - "INSERT INTO word_group (name) VALUES (?1) RETURNING id", + "INSERT INTO word_groups (name) VALUES (?1) RETURNING id", (&group.name,), |row| row.get(0), ) @@ -154,7 +154,7 @@ pub fn update_group(group: &mut WordGroup, connection: &Connection) -> MaybeErro } else { connection .execute( - "UPDATE word_group SET name = ?1 WHERE id = ?2", + "UPDATE word_groups SET name = ?1 WHERE id = ?2", (&group.name, &group.id), ) .map_err(|_| DbSaveError)?; @@ -167,7 +167,7 @@ pub fn delete_group(group: &WordGroup, connection: &Connection) -> MaybeError { return Err(DbSaveError); } connection - .execute("DELETE FROM word_group WHERE id = ?1", (&group.id,)) + .execute("DELETE FROM word_groups WHERE id = ?1", (&group.id,)) .map_err(|_| DbSaveError)?; Ok(()) }