sm_2/
scheduler.rs

1use chrono::{DateTime, Duration, Utc};
2
3use crate::Card;
4use crate::ReviewLog;
5
6#[derive(Debug)]
7pub enum SchedulerError {
8    NotDue,
9}
10
11pub struct Scheduler;
12
13impl Scheduler {
14    pub fn review_card(
15        card: &Card,
16        rating: u8,
17        review_datetime: Option<DateTime<Utc>>,
18        review_duration: Option<u32>,
19    ) -> Result<(Card, ReviewLog), SchedulerError> {
20        let mut card = card.clone();
21
22        let review_datetime = review_datetime.unwrap_or_else(Utc::now);
23
24        if review_datetime < card.due {
25            return Err(SchedulerError::NotDue);
26        }
27
28        if card.needs_extra_review {
29            if rating >= 4 {
30                card.needs_extra_review = false;
31                card.due = card.due + Duration::days(card.i as i64);
32            }
33        } else {
34            // correct response
35            if rating >= 3 {
36                // note: ef increases when rating = 5, stays the same when rating = 4 and decreases when rating = 3
37                card.ef = (card.ef
38                    + (0.1 - (5f32 - rating as f32) * (0.08 + (5f32 - rating as f32) * 0.02)))
39                    .max(1.3);
40
41                if card.n == 0 {
42                    card.i = 1;
43                } else if card.n == 1 {
44                    card.i = 6;
45                } else {
46                    card.i = (card.i as f32 * card.ef).ceil() as u32;
47                }
48
49                card.n += 1;
50
51                if rating >= 4 {
52                    card.due = card.due + Duration::days(card.i as i64);
53                } else {
54                    card.needs_extra_review = true;
55                    card.due = review_datetime;
56                }
57            } else {
58                // incorrect response
59
60                card.n = 0;
61                card.i = 0;
62                card.due = review_datetime;
63                // ef doesn't change on incorrect responses
64            }
65        }
66
67        let review_log = ReviewLog::new(card.card_id, rating, review_datetime, review_duration);
68
69        Ok((card, review_log))
70    }
71}
72
73#[cfg(test)]
74mod tests {
75
76    use super::*;
77
78    #[test]
79    fn test_quickstart() {
80        let mut card = Card::default();
81
82        assert!(Utc::now() >= card.due);
83
84        let rating = 5;
85
86        (card, _) = Scheduler::review_card(&card, rating, None, None).unwrap();
87
88        let time_delta = card.due - Utc::now();
89
90        assert_eq!((time_delta.as_seconds_f32() / 3600f32).round() as i32, 24);
91    }
92
93    #[test]
94    fn test_intervals() {
95        let mut card = Card::default();
96        let mut now = Utc::now();
97
98        assert!(now >= card.due);
99
100        let ratings = [4, 3, 3, 4, 5, 3, 0, 1, 3, 3, 4, 5, 3];
101
102        let mut ivl_history: Vec<i32> = Vec::new();
103        for rating in ratings {
104            (card, _) = Scheduler::review_card(&card, rating, Some(now), None).unwrap();
105            let ivl = ((card.due - now).as_seconds_f32() / 86400f32).round() as i32;
106            ivl_history.push(ivl);
107            now = card.due;
108        }
109
110        assert_eq!(ivl_history, vec![1, 0, 0, 6, 15, 0, 0, 0, 0, 0, 35, 85, 0]);
111    }
112
113    #[test]
114    fn test_card_serialize() {
115        let card = Card::default();
116
117        let json = card.to_json().unwrap();
118        let copied_card = Card::from_json(&json).unwrap();
119
120        assert_eq!(card, copied_card);
121
122        // (x2) perform the above tests once more with a reviewed card
123        let (reviewed_card, _) = Scheduler::review_card(&card, 5, None, None).unwrap();
124
125        let json = reviewed_card.to_json().unwrap();
126        let copied_reviewed_card = Card::from_json(&json).unwrap();
127
128        assert_eq!(reviewed_card, copied_reviewed_card);
129        assert_ne!(card, reviewed_card);
130    }
131
132    #[test]
133    fn test_review_log_serialize() {
134        let card = Card::default();
135
136        let (card, review_log) = Scheduler::review_card(&card, 0, None, None).unwrap();
137
138        let json = review_log.to_json().unwrap();
139        let copied_review_log = ReviewLog::from_json(&json).unwrap();
140
141        assert_eq!(review_log, copied_review_log);
142
143        // (x2) perform the above tests once more with a ReviewLog from a reviewed card
144        let (_, next_review_log) = Scheduler::review_card(&card, 5, None, None).unwrap();
145
146        let json = next_review_log.to_json().unwrap();
147        let copied_next_review_log = ReviewLog::from_json(&json).unwrap();
148
149        assert_eq!(next_review_log, copied_next_review_log);
150        assert_ne!(review_log, next_review_log);
151    }
152}