rs-fsrs 1.2.1

Rust-based Scheduler for FSRS
Documentation
use chrono::{DateTime, Duration, Utc};

use crate::{scheduler::Scheduler, Card, ImplScheduler, Parameters, Rating, SchedulingInfo};
use crate::{Rating::*, State::*};
pub struct BasicScheduler {
    pub scheduler: Scheduler,
}

impl BasicScheduler {
    pub fn new(parameters: Parameters, card: Card, now: DateTime<Utc>) -> Self {
        Self {
            scheduler: Scheduler::new(parameters, card, now),
        }
    }
    fn new_state(&mut self, rating: Rating) -> SchedulingInfo {
        if let Some(exist) = self.scheduler.next.get(&rating) {
            return exist.clone();
        }

        let mut next = self.scheduler.current.clone();
        next.difficulty = self.scheduler.parameters.init_difficulty(rating);
        next.stability = self.scheduler.parameters.init_stability(rating);

        match rating {
            Again => {
                next.scheduled_days = 0;
                next.due = self.scheduler.now + Duration::minutes(1);
                next.state = Learning;
            }
            Hard => {
                next.scheduled_days = 0;
                next.due = self.scheduler.now + Duration::minutes(5);
                next.state = Learning;
            }
            Good => {
                next.scheduled_days = 0;
                next.due = self.scheduler.now + Duration::minutes(10);
                next.state = Learning;
            }
            Easy => {
                let easy_interval = self
                    .scheduler
                    .parameters
                    .next_interval(next.stability, next.elapsed_days);
                next.scheduled_days = easy_interval as i64;
                next.due = self.scheduler.now + Duration::days(easy_interval as i64);
                next.state = Review;
            }
        };
        let item = SchedulingInfo {
            card: next,
            review_log: self.scheduler.build_log(rating),
        };

        self.scheduler.next.insert(rating, item.clone());
        item
    }

    fn learning_state(&mut self, rating: Rating) -> SchedulingInfo {
        if let Some(exist) = self.scheduler.next.get(&rating) {
            return exist.clone();
        }

        let mut next = self.scheduler.current.clone();
        let interval = self.scheduler.current.elapsed_days;
        next.difficulty = self
            .scheduler
            .parameters
            .next_difficulty(self.scheduler.last.difficulty, rating);
        next.stability = self
            .scheduler
            .parameters
            .short_term_stability(self.scheduler.last.stability, rating);

        match rating {
            Again => {
                next.scheduled_days = 0;
                next.due = self.scheduler.now + Duration::minutes(5);
                next.state = self.scheduler.last.state;
            }
            Hard => {
                next.scheduled_days = 0;
                next.due = self.scheduler.now + Duration::minutes(10);
                next.state = self.scheduler.last.state;
            }
            Good => {
                let good_interval = self
                    .scheduler
                    .parameters
                    .next_interval(next.stability, interval);
                next.scheduled_days = good_interval as i64;
                next.due = self.scheduler.now + Duration::days(good_interval as i64);
                next.state = Review;
            }
            Easy => {
                let good_stability = self
                    .scheduler
                    .parameters
                    .short_term_stability(self.scheduler.last.stability, Good);
                let good_interval = self
                    .scheduler
                    .parameters
                    .next_interval(good_stability, interval);
                let easy_interval = self
                    .scheduler
                    .parameters
                    .next_interval(next.stability, interval)
                    .max(good_interval + 1.0);
                next.scheduled_days = easy_interval as i64;
                next.due = self.scheduler.now + Duration::days(easy_interval as i64);
                next.state = Review;
            }
        }
        let item = SchedulingInfo {
            card: next,
            review_log: self.scheduler.build_log(rating),
        };

        self.scheduler.next.insert(rating, item.clone());
        item
    }

    fn review_state(&mut self, rating: Rating) -> SchedulingInfo {
        if let Some(exist) = self.scheduler.next.get(&rating) {
            return exist.clone();
        }

        let next = self.scheduler.current.clone();
        let interval = self.scheduler.current.elapsed_days;
        let stability = self.scheduler.last.stability;
        let difficulty = self.scheduler.last.difficulty;
        let retrievability = self.scheduler.last.get_retrievability(self.scheduler.now);

        let mut next_again = next.clone();
        let mut next_hard = next.clone();
        let mut next_good = next.clone();
        let mut next_easy = next;

        self.next_difficulty_stability(
            &mut next_again,
            &mut next_hard,
            &mut next_good,
            &mut next_easy,
            difficulty,
            stability,
            retrievability,
        );
        self.next_interval(
            &mut next_again,
            &mut next_hard,
            &mut next_good,
            &mut next_easy,
            interval,
        );
        self.next_state(
            &mut next_again,
            &mut next_hard,
            &mut next_good,
            &mut next_easy,
        );
        next_again.lapses += 1;

        let item_again = SchedulingInfo {
            card: next_again,
            review_log: self.scheduler.build_log(Again),
        };
        let item_hard = SchedulingInfo {
            card: next_hard,
            review_log: self.scheduler.build_log(Hard),
        };
        let item_good = SchedulingInfo {
            card: next_good,
            review_log: self.scheduler.build_log(Good),
        };
        let item_easy = SchedulingInfo {
            card: next_easy,
            review_log: self.scheduler.build_log(Easy),
        };

        self.scheduler.next.insert(Again, item_again);
        self.scheduler.next.insert(Hard, item_hard);
        self.scheduler.next.insert(Good, item_good);
        self.scheduler.next.insert(Easy, item_easy);

        self.scheduler.next.get(&rating).unwrap().to_owned()
    }

    #[allow(clippy::too_many_arguments)]
    fn next_difficulty_stability(
        &self,
        next_again: &mut Card,
        next_hard: &mut Card,
        next_good: &mut Card,
        next_easy: &mut Card,
        difficulty: f64,
        stability: f64,
        retrievability: f64,
    ) {
        next_again.difficulty = self.scheduler.parameters.next_difficulty(difficulty, Again);
        next_again.stability =
            self.scheduler
                .parameters
                .next_forget_stability(difficulty, stability, retrievability);

        next_hard.difficulty = self.scheduler.parameters.next_difficulty(difficulty, Hard);
        next_hard.stability = self.scheduler.parameters.next_recall_stability(
            difficulty,
            stability,
            retrievability,
            Hard,
        );

        next_good.difficulty = self.scheduler.parameters.next_difficulty(difficulty, Good);
        next_good.stability = self.scheduler.parameters.next_recall_stability(
            difficulty,
            stability,
            retrievability,
            Good,
        );

        next_easy.difficulty = self.scheduler.parameters.next_difficulty(difficulty, Easy);
        next_easy.stability = self.scheduler.parameters.next_recall_stability(
            difficulty,
            stability,
            retrievability,
            Easy,
        );
    }

    fn next_interval(
        &self,
        next_again: &mut Card,
        next_hard: &mut Card,
        next_good: &mut Card,
        next_easy: &mut Card,
        elapsed_days: i64,
    ) {
        let mut hard_interval = self
            .scheduler
            .parameters
            .next_interval(next_hard.stability, elapsed_days);
        let mut good_interval = self
            .scheduler
            .parameters
            .next_interval(next_good.stability, elapsed_days);
        hard_interval = hard_interval.min(good_interval);
        good_interval = good_interval.max(hard_interval + 1.0);
        let easy_interval = self
            .scheduler
            .parameters
            .next_interval(next_easy.stability, elapsed_days)
            .max(good_interval + 1.0);

        next_again.scheduled_days = 0;
        next_again.due = self.scheduler.now + Duration::minutes(5);

        next_hard.scheduled_days = hard_interval as i64;
        next_hard.due = self.scheduler.now + Duration::days(hard_interval as i64);

        next_good.scheduled_days = good_interval as i64;
        next_good.due = self.scheduler.now + Duration::days(good_interval as i64);

        next_easy.scheduled_days = easy_interval as i64;
        next_easy.due = self.scheduler.now + Duration::days(easy_interval as i64);
    }

    fn next_state(
        &self,
        next_again: &mut Card,
        next_hard: &mut Card,
        next_good: &mut Card,
        next_easy: &mut Card,
    ) {
        next_again.state = Relearning;
        next_hard.state = Review;
        next_good.state = Review;
        next_easy.state = Review;
    }
}

impl ImplScheduler for BasicScheduler {
    fn review(&mut self, rating: Rating) -> SchedulingInfo {
        match self.scheduler.last.state {
            New => self.new_state(rating),
            Learning | Relearning => self.learning_state(rating),
            Review => self.review_state(rating),
        }
    }
}