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_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 new file mode 100644 index 0000000..9355ab2 --- /dev/null +++ b/sql/history.sql @@ -0,0 +1,12 @@ +create table history +( + id INTEGER + primary key autoincrement, + 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, + fsrs INTEGER not null +); diff --git a/sql/card_stats.sql b/sql/states.sql similarity index 59% rename from sql/card_stats.sql rename to sql/states.sql index fb2b675..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,8 +6,11 @@ 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 + last_opened INTEGER not null, + 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/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/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 cffec4b..3bb2cd3 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -1,10 +1,11 @@ use crate::app::AppState; +use crate::core::srs::OrderModule::{Fsrs, RandomSRS, SemiRandomSRS, WorstWordsSRS}; 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::card_states::{load_stats_of_deck, update_stat_score}; use crate::data_provider::history::push_note; use crate::notification::{MaybeError, NotificationData}; use chrono::Utc; @@ -18,6 +19,7 @@ pub struct DeckData { current_word_index: Option, state: Arc>, order_module: OrderModule, + open_mode: OrderMode, settings: DeckSettings, } impl DeckData { @@ -46,32 +48,34 @@ impl DeckData { 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()) } @@ -90,15 +94,28 @@ 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 + && 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..7bfd811 100644 --- a/src/core/srs.rs +++ b/src/core/srs.rs @@ -1,14 +1,16 @@ use crate::data::entities::CardStatistics; +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; 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,112 @@ 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, _: &[CardStatistics]) -> usize { + if self.current_group.is_empty() { + self.fill_partial_pool(); + } + + if self.queue.is_empty() { + self.fill_queue(); + self.queue.shuffle(&mut self.rng); + } + + 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(); + + 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); + } + } + + 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.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()); + } + + fn fill_queue(&mut self) { + self.queue.extend(self.current_group.clone()); + } + + pub fn finished(&self) -> bool { + self.today_set.len() == self.solved.len() + } +} + #[derive(Clone, Copy)] pub enum WordOpenMode { Easy, diff --git a/src/data/entities.rs b/src/data/entities.rs index 7342947..17b131c 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -1,7 +1,8 @@ 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, NaiveDate, Utc}; +use fsrs::MemoryState; use log::info; use rayon::prelude::*; use rhai::{Engine, Scope}; @@ -137,6 +138,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) { diff --git a/src/data_provider/card_stats.rs b/src/data_provider/card_states.rs similarity index 64% rename from src/data_provider/card_stats.rs rename to src/data_provider/card_states.rs index cf19019..248e156 100644 --- a/src/data_provider/card_stats.rs +++ b/src/data_provider/card_states.rs @@ -1,6 +1,8 @@ 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 rusqlite::{Connection, params}; use std::time::Instant; @@ -10,7 +12,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 states WHERE set_id = ?1") .expect("Failed to prepare state query"); let iter = stmt .query_map((set.id,), |row| { @@ -19,7 +21,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 +37,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 +58,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 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"); @@ -49,7 +67,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)?; } @@ -73,7 +97,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)?; @@ -92,7 +116,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/card_sets.rs b/src/data_provider/decks.rs similarity index 87% rename from src/data_provider/card_sets.rs rename to src/data_provider/decks.rs index d6f6885..b320e57 100644 --- a/src/data_provider/card_sets.rs +++ b/src/data_provider/decks.rs @@ -1,5 +1,4 @@ use crate::core::srs::OrderMode; - use crate::data::entities::DeckSettings; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use crate::notification::{MaybeError, NotificationData}; @@ -7,7 +6,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| { @@ -31,7 +30,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, @@ -52,7 +51,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, @@ -71,7 +70,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/history.rs b/src/data_provider/history.rs index 023d6b7..c3cd324 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -4,12 +4,20 @@ 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 rusqlite::Connection; use std::fs::{File, OpenOptions}; use std::io::Write; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +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()?; let head = app_dir.clone().join(format!("set_{}_history.csv", id)); diff --git a/src/data_provider/mod.rs b/src/data_provider/mod.rs index 75154c8..86660ec 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 card_states; +pub(crate) mod decks; pub(crate) mod history; pub(crate) mod import; pub(crate) mod settings; diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index cbe539d..32a7bc4 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> { @@ -29,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> { @@ -46,7 +47,13 @@ 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(()) +} + +fn make_history(conn: &Connection) -> Result<(), rusqlite::Error> { + let query = include_str!("../../sql/history.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(()) } diff --git a/src/pages/repetition_settings.rs b/src/pages/repetition_settings.rs index b648c6b..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::card_sets::{delete_set, update_deck}; -use crate::data_provider::card_stats::{delete_stats, load_stats_of_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 075ad28..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::card_sets::update_deck; -use crate::data_provider::card_stats::{add_stat_list, load_stats_of_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}; @@ -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::>();