From 663fa377b196543f19e4b00e2b4ece423adb8f52 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Fri, 28 Aug 2026 17:44:14 +0300 Subject: [PATCH 1/6] Add history table and modify card_stats --- Cargo.lock | 124 ++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- sql/card_stats.sql | 6 +- sql/history.sql | 11 ++++ src/data_provider/history.rs | 7 +- src/data_provider/sqlite.rs | 7 ++ 6 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 sql/history.sql diff --git a/Cargo.lock b/Cargo.lock index f86de79..d59a4c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1003,7 +1003,7 @@ dependencies = [ "ciborium", "clap", "criterion-plot", - "itertools", + "itertools 0.13.0", "num-traits", "oorandom", "page_size", @@ -1023,7 +1023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", - "itertools", + "itertools 0.13.0", ] [[package]] @@ -1529,6 +1529,23 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fsrs" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a99ea3dec9af37c9ed3835463ff6820a578a0026a8e370b4c1c0db48dfab3f" +dependencies = [ + "itertools 0.14.0", + "log", + "ndarray", + "priority-queue", + "rand", + "rayon", + "serde", + "snafu", + "strum", +] + [[package]] name = "futures" version = "0.3.31" @@ -2411,6 +2428,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -2425,6 +2451,7 @@ dependencies = [ "criterion", "dirs", "env_logger", + "fsrs", "hashbrown 0.17.1", "hex", "iced", @@ -2775,6 +2802,16 @@ dependencies = [ "libc", ] +[[package]] +name = "matrixmultiply" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "memchr" version = "2.8.0" @@ -2911,6 +2948,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2960,6 +3012,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -3772,6 +3833,17 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "priority-queue" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" +dependencies = [ + "equivalent", + "indexmap", + "serde", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -3950,6 +4022,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.12.0" @@ -4715,6 +4793,27 @@ dependencies = [ "serde", ] +[[package]] +name = "snafu" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45cb604038abb7b926b679887b3226d8d0f23874b66623625a0454be425a4b7" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287f59010008f0d7cf5e3b03196d666c1acc46c8d3e9cf34c28a1a7157601e72" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "socket2" version = "0.6.3" @@ -4804,6 +4903,27 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index 06ba459..9e481a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,13 +26,14 @@ rayon = "1.12.0" parking_lot = "0.12.5" env_logger = "0.11.11" log = "0.4.34" +fsrs = "6.6.1" [profile.super-release] inherits = "release" codegen-units = 1 lto = true opt-level = 3 -strip = true +#strip = true panic = "abort" [dev-dependencies] diff --git a/sql/card_stats.sql b/sql/card_stats.sql index fb2b675..4c6c6b4 100644 --- a/sql/card_stats.sql +++ b/sql/card_stats.sql @@ -9,5 +9,9 @@ create table card_stats references card_set on delete cascade, score INTEGER default 1 not null, - last_opened INTEGER not null + last_opened INTEGER not null, + added INTEGER not null, + fsrs_opened INTEGER, + fsrs_memory INTEGER, + fsrs_open_time INTEGER ); \ No newline at end of file diff --git a/sql/history.sql b/sql/history.sql new file mode 100644 index 0000000..cfeff01 --- /dev/null +++ b/sql/history.sql @@ -0,0 +1,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, + timestamp INTEGER not null, + answer INTEGER not null, + before INTEGER not null, + after INTEGER not null +); diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index 023d6b7..1c78cd6 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -4,11 +4,16 @@ use crate::data::entities::HistoryItem; use crate::fs::*; use crate::notification::NotificationData::PermissionDenied; use crate::notification::{MaybeError, NotificationData}; -use chrono::DateTime; +use chrono::{DateTime, NaiveDate}; use std::fs::{File, OpenOptions}; use std::io::Write; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +use rusqlite::Connection; + +pub fn get_open_count_of_set(id: Id, connection: &Connection) -> Result, NotificationData>{ + todo!() +} pub fn get_history_of_set(id: Id) -> Result, NotificationData> { let app_dir = history_dir()?; diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index cbe539d..be04f07 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -20,6 +20,7 @@ fn create_tables(conn: &Connection) { make_word_group(conn).expect("Failed to init table"); make_words(conn).expect("Failed to init table"); make_card_stats(conn).expect("Failed to init table"); + make_history(conn).expect("Failed to init table"); } fn make_settings(conn: &Connection) -> Result<(), rusqlite::Error> { @@ -51,6 +52,12 @@ fn make_card_stats(conn: &Connection) -> Result<(), rusqlite::Error> { Ok(()) } +fn make_history(conn: &Connection) -> Result<(), rusqlite::Error> { + let query = include_str!("../../sql/history.sql"); + conn.execute(query, ())?; + Ok(()) +} + pub fn default_connection() -> Connection { let db_file = db_path(); let connection = Connection::open(db_file).expect("Failed to open database"); From e7019903b0d987c1aebbe94d2f064c2e7157d32a Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Fri, 28 Aug 2026 18:52:38 +0300 Subject: [PATCH 2/6] Create fsrs upadte method --- sql/card_stats.sql | 7 +++---- src/core/deck.rs | 10 +++++++++- src/data/entities.rs | 30 +++++++++++++++++++++++++++++- src/data_provider/card_sets.rs | 5 ++++- src/data_provider/card_stats.rs | 31 ++++++++++++++++++++++++++++--- 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/sql/card_stats.sql b/sql/card_stats.sql index 4c6c6b4..92f8d75 100644 --- a/sql/card_stats.sql +++ b/sql/card_stats.sql @@ -10,8 +10,7 @@ create table card_stats on delete cascade, score INTEGER default 1 not null, last_opened INTEGER not null, - added INTEGER not null, - fsrs_opened INTEGER, - fsrs_memory INTEGER, - fsrs_open_time INTEGER + fsrs_opened INTEGER not null, + fsrs_memory INTEGER not null, + fsrs_open_day INTEGER not null ); \ No newline at end of file diff --git a/src/core/deck.rs b/src/core/deck.rs index cffec4b..9c45295 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -7,7 +7,7 @@ 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::Utc; +use chrono::{Local, Utc}; use parking_lot::Mutex; use std::sync::Arc; @@ -16,6 +16,7 @@ pub struct DeckData { pub(crate) words: Vec, pub(crate) set: Vec, current_word_index: Option, + fsrs_indices: Vec, state: Arc>, order_module: OrderModule, settings: DeckSettings, @@ -41,7 +42,14 @@ impl DeckData { .cloned() .collect(); + let today = Local::now().to_utc().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, diff --git a/src/data/entities.rs b/src/data/entities.rs index 7342947..166ad1a 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -1,8 +1,10 @@ use crate::app::AppState; use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, OrderMode, WordOpenMode}; use crate::data::{INVALID_ID, Id}; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Local, NaiveDate, Utc}; +use fsrs::{FSRS, MemoryState}; use log::info; +use rand::{RngExt, random, rng}; use rayon::prelude::*; use rhai::{Engine, Scope}; use std::collections::HashMap; @@ -137,6 +139,9 @@ pub struct CardStatistics { pub set_id: Id, pub last_open: DateTime, pub score: u8, + pub fsrs_opened: NaiveDate, + pub fsrs_memory: Option, + pub fsrs_open_day: NaiveDate, } impl CardStatistics { pub fn update(&mut self, status: WordOpenMode) { @@ -157,6 +162,29 @@ impl CardStatistics { self.last_open = Utc::now(); } + pub fn update_fsrs(&mut self, status: WordOpenMode, by_fsrs: bool, fsrs: FSRS) + { + let today = Local::now().to_utc().date_naive().to_epoch_days(); + let written = self.fsrs_opened.to_epoch_days(); + let memory = fsrs + .next_states(self.fsrs_memory, 0.9, (today - written) as u32) + .expect("Wrong next stage calculation"); + let next = match status { + WordOpenMode::Easy => memory.easy, + WordOpenMode::Ok => memory.good, + WordOpenMode::Hard => memory.hard, + WordOpenMode::None => memory.again, + }; + + let mut next_day = today + next.interval.round() as i32; + if next_day - today >= 5 { + let offset = ((rng().random::() - 0.5) * 3.0).round() as i32; + next_day += offset; + } + self.fsrs_memory = Some(next.memory); + self.fsrs_open_day = NaiveDate::from_epoch_days(next_day).unwrap(); + self.fsrs_opened = self.last_open.date_naive(); + } pub fn calculated_score(&self) -> f32 { let time = Utc::now() - self.last_open; let days = time.num_days(); diff --git a/src/data_provider/card_sets.rs b/src/data_provider/card_sets.rs index d6f6885..a99f939 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/card_sets.rs @@ -1,8 +1,11 @@ use crate::core::srs::OrderMode; - +use crate::data::Id; use crate::data::entities::DeckSettings; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use crate::notification::{MaybeError, NotificationData}; +use chrono::{Datelike, Local, NaiveDate}; +use fsrs::Card; +use iced::futures::StreamExt; use rusqlite::Connection; pub fn load_sets(connection: &Connection) -> Result, NotificationData> { diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_stats.rs index cf19019..eab5f3f 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_stats.rs @@ -1,7 +1,10 @@ use crate::data::entities::{CardStatistics, DeckSettings}; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use crate::notification::{MaybeError, NotificationData}; +use chrono::NaiveDate; +use fsrs::MemoryState; use log::info; +use reqwest::get; use rusqlite::{Connection, params}; use std::time::Instant; @@ -10,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 FROM card_stats WHERE set_id = ?1") + .prepare("SELECT id, word_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM card_stats WHERE set_id = ?1") .expect("Failed to prepare state query"); let iter = stmt .query_map((set.id,), |row| { @@ -19,7 +22,12 @@ pub fn load_stats_of_deck( word_id: row.get(1)?, set_id: set.id, score: row.get(2)?, + fsrs_opened: NaiveDate::from_epoch_days(row.get::(4)?) + .expect("Incorrect date encoding"), + fsrs_memory: Some(get_memory(row.get(5)?)), last_open: row.get(3)?, + fsrs_open_day: NaiveDate::from_epoch_days(row.get::(6)?) + .expect("Incorrect date encoding"), }) }) .map_err(|_| DbLoadError)?; @@ -30,6 +38,17 @@ pub fn load_stats_of_deck( Ok(buffer) } +fn get_memory(value: i64) -> MemoryState { + MemoryState { + stability: f32::from_bits(value as u32), + difficulty: f32::from_bits((value >> 32) as u32), + } +} + +fn memory_to_i64(value: &MemoryState) -> i64 { + value.stability.to_bits() as i64 | ((value.difficulty.to_bits() as i64) << 32) +} + pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) -> MaybeError { let time = Instant::now(); let tx = connection @@ -40,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) VALUES (?1, ?2, ?3, ?4)", + "INSERT INTO card_stats (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"); @@ -49,7 +68,13 @@ pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) stat.word_id, stat.set_id, stat.score, - stat.last_open.timestamp() + stat.last_open.timestamp(), + stat.fsrs_opened.to_epoch_days(), + memory_to_i64(&stat.fsrs_memory.unwrap_or(MemoryState { + difficulty: 0.0, + stability: 0.0 + })), + stat.fsrs_open_day.to_epoch_days() ]) .map_err(|_| DbSaveError)?; } From 188a26e77dff68f60fae69169ad8b4d25d943570 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Fri, 28 Aug 2026 21:47:47 +0300 Subject: [PATCH 3/6] Change date type --- src/core/deck.rs | 2 +- src/data/entities.rs | 2 +- src/pages/repetitions.rs | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/deck.rs b/src/core/deck.rs index 9c45295..6b1e99e 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -42,7 +42,7 @@ impl DeckData { .cloned() .collect(); - let today = Local::now().to_utc().date_naive(); + let today = Local::now().date_naive(); Ok(Self { fsrs_indices: stats .iter() diff --git a/src/data/entities.rs b/src/data/entities.rs index 166ad1a..3448fb8 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -164,7 +164,7 @@ impl CardStatistics { pub fn update_fsrs(&mut self, status: WordOpenMode, by_fsrs: bool, fsrs: FSRS) { - let today = Local::now().to_utc().date_naive().to_epoch_days(); + let today = Local::now().date_naive().to_epoch_days(); let written = self.fsrs_opened.to_epoch_days(); let memory = fsrs .next_states(self.fsrs_memory, 0.9, (today - written) as u32) diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 075ad28..42a718b 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -590,7 +590,10 @@ impl RepetitionsState { word_id: word.id, last_open: Utc::now(), score: 1, + fsrs_opened: Utc::now().date_naive(), + fsrs_memory: None, set_id: deck.id, + fsrs_open_day: Utc::now().date_naive(), } }) .collect::>(); From a642a8fda402b1e8631362448bc3e47197ceca84 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Sat, 29 Aug 2026 21:31:54 +0300 Subject: [PATCH 4/6] 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(()) } From af6cf107655350d90401e8d447913be81582a367 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Sat, 29 Aug 2026 21:32:54 +0300 Subject: [PATCH 5/6] rename data provider file names --- src/app.rs | 2 +- src/core/deck.rs | 6 +++--- src/data_provider/{card_stats.rs => card_states.rs} | 0 src/data_provider/{card_sets.rs => decks.rs} | 0 src/data_provider/mod.rs | 4 ++-- src/pages/repetition_settings.rs | 4 ++-- src/pages/repetitions.rs | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) rename src/data_provider/{card_stats.rs => card_states.rs} (100%) rename src/data_provider/{card_sets.rs => decks.rs} (100%) diff --git a/src/app.rs b/src/app.rs index 7b273c5..eaafa91 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,7 @@ use crate::data::Id; use crate::data::entities::DeckSettings; use crate::data::entities::{WordData, WordGroup}; -use crate::data_provider::card_sets::load_sets; +use crate::data_provider::decks::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}; diff --git a/src/core/deck.rs b/src/core/deck.rs index 2674687..b76e277 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -5,7 +5,7 @@ use crate::core::srs::{ WordOpenMode, WorstWordsSRSModule, }; use crate::data::entities::*; -use crate::data_provider::card_stats::{load_stats_of_deck, update_stat_score}; +use crate::data_provider::card_states::{load_stats_of_deck, update_stat_score}; use crate::data_provider::history::push_note; use crate::notification::{MaybeError, NotificationData}; use chrono::Utc; @@ -42,7 +42,7 @@ impl DeckData { .map(|index| &state_locked.dictionary[index]) .cloned() .collect(); - + Ok(Self { set: stats, words, @@ -62,7 +62,7 @@ impl DeckData { } module.next(self.set.as_slice()) } - + RandomSRS(ref mut module) => { if !module.initialized { module.init(self.set.as_slice()) diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_states.rs similarity index 100% rename from src/data_provider/card_stats.rs rename to src/data_provider/card_states.rs diff --git a/src/data_provider/card_sets.rs b/src/data_provider/decks.rs similarity index 100% rename from src/data_provider/card_sets.rs rename to src/data_provider/decks.rs diff --git a/src/data_provider/mod.rs b/src/data_provider/mod.rs index 75154c8..f7107f4 100644 --- a/src/data_provider/mod.rs +++ b/src/data_provider/mod.rs @@ -1,5 +1,5 @@ -pub(crate) mod card_sets; -pub(crate) mod card_stats; +pub(crate) mod decks; +pub(crate) mod card_states; pub(crate) mod history; pub(crate) mod import; pub(crate) mod settings; diff --git a/src/pages/repetition_settings.rs b/src/pages/repetition_settings.rs index b648c6b..d09b9e2 100644 --- a/src/pages/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -1,7 +1,7 @@ use crate::app::AppState; use crate::data::entities::DeckSettings; -use crate::data_provider::card_sets::{delete_set, update_deck}; -use crate::data_provider::card_stats::{delete_stats, load_stats_of_deck}; +use crate::data_provider::decks::{delete_set, update_deck}; +use crate::data_provider::card_states::{delete_stats, load_stats_of_deck}; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::MaybeError; diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 42a718b..40223ca 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -2,8 +2,8 @@ use crate::app::AppState; use crate::core::srs::{AppendMode, OrderMode}; use crate::data::Id; use crate::data::entities::{CardStatistics, DeckSettings}; -use crate::data_provider::card_sets::update_deck; -use crate::data_provider::card_stats::{add_stat_list, load_stats_of_deck}; +use crate::data_provider::decks::update_deck; +use crate::data_provider::card_states::{add_stat_list, load_stats_of_deck}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::{MaybeError, NotificationRequest}; From d5a2a575fe0b2d3cf21080354ab7c6049fc58767 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Sat, 29 Aug 2026 22:02:01 +0300 Subject: [PATCH 6/6] Cleanup and format --- src/core/deck.rs | 26 ++++++------- src/core/srs.rs | 65 +++++++++++++++++++++----------- src/data/entities.rs | 28 +------------- src/data_provider/card_states.rs | 1 - src/data_provider/decks.rs | 4 -- src/data_provider/history.rs | 7 +++- src/data_provider/mod.rs | 2 +- src/pages/repetition_settings.rs | 2 +- src/pages/repetitions.rs | 2 +- 9 files changed, 64 insertions(+), 73 deletions(-) diff --git a/src/core/deck.rs b/src/core/deck.rs index b76e277..3bb2cd3 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -1,5 +1,5 @@ use crate::app::AppState; -use crate::core::srs::OrderModule::{WorstWordsSRS, FSRS, RandomSRS, SemiRandomSRS}; +use crate::core::srs::OrderModule::{Fsrs, RandomSRS, SemiRandomSRS, WorstWordsSRS}; use crate::core::srs::{ FsrsModule, OrderMode, OrderModule, RandomSRSModule, SRSModule, SemiRandomSRSModule, WordOpenMode, WorstWordsSRSModule, @@ -48,7 +48,7 @@ impl DeckData { words, current_word_index: None, state: state_for, - order_module: FSRS(FsrsModule::new()), + order_module: Fsrs(FsrsModule::new()), open_mode: settings.open_mode, settings: settings.clone(), }) @@ -75,7 +75,7 @@ impl DeckData { } module.next(self.set.as_slice()) } - FSRS(ref mut module) => { + Fsrs(ref mut module) => { if !module.initialized { module.init(self.set.as_slice()) } @@ -103,22 +103,18 @@ impl DeckData { WorstWordsSRS(ref mut module) => { module.open(status, index, word); } - FSRS(ref mut module) => { + 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()), - } + if let Fsrs(ref fsrs) = self.order_module + && 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)?; diff --git a/src/core/srs.rs b/src/core/srs.rs index dc016ce..7bfd811 100644 --- a/src/core/srs.rs +++ b/src/core/srs.rs @@ -1,10 +1,10 @@ use crate::data::entities::CardStatistics; -use chrono::Local; +use chrono::{Local, TimeDelta}; use fsrs::FSRS; use rand::distr::weighted::WeightedIndex; use rand::prelude::ThreadRng; use rand::prelude::{Distribution, SliceRandom}; -use rand::rng; +use rand::{RngExt, rng}; const MAX_HISTORY_LEN: usize = 20; const MAX_HISTORY_LEN_PART: f32 = 0.33; @@ -28,7 +28,7 @@ pub enum OrderModule { SemiRandomSRS(SemiRandomSRSModule), RandomSRS(RandomSRSModule), WorstWordsSRS(WorstWordsSRSModule), - FSRS(FsrsModule), + Fsrs(FsrsModule), } pub trait SRSModule { fn next(&mut self, set: &[CardStatistics]) -> usize; @@ -183,12 +183,12 @@ pub struct FsrsModule { queue: Vec, rng: ThreadRng, fsrs: FSRS, - pub(crate) initialized: bool + pub(crate) initialized: bool, } impl SRSModule for FsrsModule { - fn next(&mut self, set: &[CardStatistics]) -> usize { - if self.solved.len() == self.current_group.len() { + fn next(&mut self, _: &[CardStatistics]) -> usize { + if self.current_group.is_empty() { self.fill_partial_pool(); } @@ -197,16 +197,39 @@ impl SRSModule for FsrsModule { self.queue.shuffle(&mut self.rng); } - self.queue.pop().expect("empty queue") + self.queue.pop().unwrap_or(0) } 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{ + + let memory = self + .fsrs + .next_states(updated_word.fsrs_memory, 0.9, delta_days as u32) + .expect("Wrong next stage calculation"); + + let next = match status { + WordOpenMode::Easy => memory.easy, + WordOpenMode::Ok => memory.good, + WordOpenMode::Hard => memory.hard, + WordOpenMode::None => memory.again, + }; + + let mut next_day = today + TimeDelta::days(next.interval.round() as i64); + if (next_day - today).num_days() >= 5 { + let offset = ((rng().random::() - 0.5) * 3.0).round() as i32; + next_day += TimeDelta::days(offset as i64); + } + updated_word.fsrs_memory = Some(next.memory); + updated_word.fsrs_open_day = next_day; + updated_word.fsrs_opened = updated_word.last_open.date_naive(); + + if updated_word.fsrs_open_day != today { self.solved.push(index); + let index = self.current_group.iter().position(|x| *x == index).unwrap(); + self.current_group.remove(index); } } @@ -226,8 +249,8 @@ impl SRSModule for FsrsModule { } impl FsrsModule { - pub fn new() -> Self{ - Self{ + pub fn new() -> Self { + Self { today_set: vec![], current_group: vec![], queue: vec![], @@ -238,21 +261,19 @@ impl FsrsModule { } } 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.current_group.extend( + self.today_set + .iter() + .filter(|x| !self.solved.contains(*x)) + .take(PARTIAL_POOL_SIZE), + ); + self.queue.clear(); - self.queue - .extend(self.current_group.clone()); - self.solved.clear(); + self.queue.extend(self.current_group.clone()); } fn fill_queue(&mut self) { - self.queue.clear(); - self.queue.extend(self.current_group.iter().filter(|c| !self.today_set.contains(*c))); + self.queue.extend(self.current_group.clone()); } pub fn finished(&self) -> bool { diff --git a/src/data/entities.rs b/src/data/entities.rs index 3448fb8..17b131c 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -1,10 +1,9 @@ use crate::app::AppState; use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, OrderMode, WordOpenMode}; use crate::data::{INVALID_ID, Id}; -use chrono::{DateTime, Local, NaiveDate, Utc}; -use fsrs::{FSRS, MemoryState}; +use chrono::{DateTime, NaiveDate, Utc}; +use fsrs::MemoryState; use log::info; -use rand::{RngExt, random, rng}; use rayon::prelude::*; use rhai::{Engine, Scope}; use std::collections::HashMap; @@ -162,29 +161,6 @@ impl CardStatistics { self.last_open = Utc::now(); } - pub fn update_fsrs(&mut self, status: WordOpenMode, by_fsrs: bool, fsrs: FSRS) - { - let today = Local::now().date_naive().to_epoch_days(); - let written = self.fsrs_opened.to_epoch_days(); - let memory = fsrs - .next_states(self.fsrs_memory, 0.9, (today - written) as u32) - .expect("Wrong next stage calculation"); - let next = match status { - WordOpenMode::Easy => memory.easy, - WordOpenMode::Ok => memory.good, - WordOpenMode::Hard => memory.hard, - WordOpenMode::None => memory.again, - }; - - let mut next_day = today + next.interval.round() as i32; - if next_day - today >= 5 { - let offset = ((rng().random::() - 0.5) * 3.0).round() as i32; - next_day += offset; - } - self.fsrs_memory = Some(next.memory); - self.fsrs_open_day = NaiveDate::from_epoch_days(next_day).unwrap(); - self.fsrs_opened = self.last_open.date_naive(); - } pub fn calculated_score(&self) -> f32 { let time = Utc::now() - self.last_open; let days = time.num_days(); diff --git a/src/data_provider/card_states.rs b/src/data_provider/card_states.rs index 83643d7..248e156 100644 --- a/src/data_provider/card_states.rs +++ b/src/data_provider/card_states.rs @@ -4,7 +4,6 @@ use crate::notification::{MaybeError, NotificationData}; use chrono::NaiveDate; use fsrs::MemoryState; use log::info; -use reqwest::get; use rusqlite::{Connection, params}; use std::time::Instant; diff --git a/src/data_provider/decks.rs b/src/data_provider/decks.rs index d85fcea..b320e57 100644 --- a/src/data_provider/decks.rs +++ b/src/data_provider/decks.rs @@ -1,11 +1,7 @@ use crate::core::srs::OrderMode; -use crate::data::Id; use crate::data::entities::DeckSettings; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use crate::notification::{MaybeError, NotificationData}; -use chrono::{Datelike, Local, NaiveDate}; -use fsrs::Card; -use iced::futures::StreamExt; use rusqlite::Connection; pub fn load_sets(connection: &Connection) -> Result, NotificationData> { diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index 1c78cd6..c3cd324 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -5,13 +5,16 @@ use crate::fs::*; use crate::notification::NotificationData::PermissionDenied; use crate::notification::{MaybeError, NotificationData}; use chrono::{DateTime, NaiveDate}; +use rusqlite::Connection; use std::fs::{File, OpenOptions}; use std::io::Write; use std::io::{BufRead, BufReader}; use std::path::PathBuf; -use rusqlite::Connection; -pub fn get_open_count_of_set(id: Id, connection: &Connection) -> Result, NotificationData>{ +pub fn get_open_count_of_set( + id: Id, + connection: &Connection, +) -> Result, NotificationData> { todo!() } diff --git a/src/data_provider/mod.rs b/src/data_provider/mod.rs index f7107f4..86660ec 100644 --- a/src/data_provider/mod.rs +++ b/src/data_provider/mod.rs @@ -1,5 +1,5 @@ -pub(crate) mod decks; pub(crate) mod card_states; +pub(crate) mod decks; pub(crate) mod history; pub(crate) mod import; pub(crate) mod settings; diff --git a/src/pages/repetition_settings.rs b/src/pages/repetition_settings.rs index d09b9e2..cf801bb 100644 --- a/src/pages/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -1,7 +1,7 @@ use crate::app::AppState; use crate::data::entities::DeckSettings; -use crate::data_provider::decks::{delete_set, update_deck}; use crate::data_provider::card_states::{delete_stats, load_stats_of_deck}; +use crate::data_provider::decks::{delete_set, update_deck}; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::MaybeError; diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 40223ca..84c4bc5 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -2,8 +2,8 @@ use crate::app::AppState; use crate::core::srs::{AppendMode, OrderMode}; use crate::data::Id; use crate::data::entities::{CardStatistics, DeckSettings}; -use crate::data_provider::decks::update_deck; use crate::data_provider::card_states::{add_stat_list, load_stats_of_deck}; +use crate::data_provider::decks::update_deck; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::{MaybeError, NotificationRequest};