use chrono::Duration;
use chrono::NaiveDate;
use crate::fsrs::Difficulty;
use crate::fsrs::Grade;
use crate::fsrs::Interval;
use crate::fsrs::Recall;
use crate::fsrs::Stability;
use crate::fsrs::initial_difficulty;
use crate::fsrs::initial_stability;
use crate::fsrs::interval;
use crate::fsrs::new_difficulty;
use crate::fsrs::new_stability;
use crate::fsrs::retrievability;
use crate::types::date::Date;
use crate::types::timestamp::Timestamp;
const TARGET_RECALL: f64 = 0.9;
const MIN_INTERVAL: f64 = 1.0;
const MAX_INTERVAL: f64 = 256.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Performance {
New,
Reviewed(ReviewedPerformance),
}
impl Performance {
pub fn is_new(&self) -> bool {
matches!(self, Performance::New)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ReviewedPerformance {
pub last_reviewed_at: Timestamp,
pub stability: Stability,
pub difficulty: Difficulty,
pub interval_raw: Interval,
pub interval_days: i64,
pub due_date: Date,
pub review_count: usize,
}
pub fn update_performance(
perf: Performance,
grade: Grade,
reviewed_at: Timestamp,
) -> ReviewedPerformance {
let today: NaiveDate = reviewed_at.date().into_inner();
let (stability, difficulty, review_count): (Stability, Difficulty, usize) = match perf {
Performance::New => (initial_stability(grade), initial_difficulty(grade), 0),
Performance::Reviewed(ReviewedPerformance {
last_reviewed_at,
stability,
difficulty,
review_count,
..
}) => {
let last_reviewed_at: NaiveDate = last_reviewed_at.date().into_inner();
let time: Interval = (today - last_reviewed_at).num_days() as f64;
let retr: Recall = retrievability(time, stability);
let stability: Stability = new_stability(difficulty, stability, retr, grade);
let difficulty: Difficulty = new_difficulty(difficulty, grade);
(stability, difficulty, review_count)
}
};
let interval_raw: Interval = interval(TARGET_RECALL, stability);
let interval_rounded: Interval = interval_raw.round();
let interval_clamped: Interval = interval_rounded.clamp(MIN_INTERVAL, MAX_INTERVAL);
let interval_days: i64 = interval_clamped as i64;
let interval_duration: Duration = Duration::days(interval_days);
let due_date: Date = Date::new(today + interval_duration);
ReviewedPerformance {
last_reviewed_at: reviewed_at,
stability,
difficulty,
interval_raw,
interval_days,
due_date,
review_count: review_count + 1,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-2
}
#[test]
fn test_new() {
assert!(Performance::New.is_new());
let reviewed_at = Timestamp::now();
let reviewed_perf = update_performance(Performance::New, Grade::Good, reviewed_at);
assert!(!Performance::Reviewed(reviewed_perf).is_new());
}
#[test]
fn test_update_new_card() {
let reviewed_at = Timestamp::now();
let result = update_performance(Performance::New, Grade::Good, reviewed_at);
let ReviewedPerformance {
last_reviewed_at,
stability,
difficulty,
interval_raw,
interval_days,
due_date: _,
review_count,
} = result;
assert_eq!(last_reviewed_at, reviewed_at);
assert!(approx_eq(stability, 3.17));
assert!(approx_eq(difficulty, 5.28));
assert!(approx_eq(interval_raw, 3.17));
assert_eq!(interval_days, 3);
assert_eq!(review_count, 1);
}
#[test]
fn test_update_already_reviewed_card() {
let now = Timestamp::now();
let today = now.date();
let duration = Duration::days(3);
let last_reviewed_at = Timestamp::new(now.into_inner() - duration);
let initial_perf = ReviewedPerformance {
last_reviewed_at,
stability: 3.17,
difficulty: 5.28,
interval_raw: 3.17,
interval_days: 3,
due_date: Date::new(today.into_inner() + duration),
review_count: 1,
};
let reviewed_at = now;
let result = update_performance(
Performance::Reviewed(initial_perf),
Grade::Easy,
reviewed_at,
);
let ReviewedPerformance {
last_reviewed_at,
stability,
difficulty,
interval_raw,
interval_days,
due_date: _,
review_count,
} = result;
assert_eq!(last_reviewed_at, reviewed_at);
assert!(approx_eq(stability, 25.80));
assert!(approx_eq(difficulty, 4.50));
assert!(approx_eq(interval_raw, 25.80));
assert_eq!(interval_days, 26);
assert_eq!(review_count, 2);
}
}