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 if rating >= 3 {
36 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 card.n = 0;
61 card.i = 0;
62 card.due = review_datetime;
63 }
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 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 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}