From e7019903b0d987c1aebbe94d2f064c2e7157d32a Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Fri, 28 Aug 2026 18:52:38 +0300 Subject: [PATCH] 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)?; }