1use crate::progress::SrsData;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum ResponseQuality {
5 Blackout = 0,
6 Incorrect = 1,
7 IncorrectEasy = 2,
8 CorrectDifficult = 3,
9 CorrectHesitation = 4,
10 Perfect = 5,
11}
12
13impl ResponseQuality {
14 pub fn from_score(score: u8) -> Self {
15 match score {
16 0 => Self::Blackout,
17 1 => Self::Incorrect,
18 2 => Self::IncorrectEasy,
19 3 => Self::CorrectDifficult,
20 4 => Self::CorrectHesitation,
21 _ => Self::Perfect,
22 }
23 }
24
25 pub fn is_correct(self) -> bool {
26 (self as u8) >= 3
27 }
28}
29
30pub fn sm2_update(srs: &mut SrsData, quality: ResponseQuality) {
31 let q = quality as u8 as f64;
32
33 if quality.is_correct() {
34 srs.repetitions += 1;
35 srs.interval = match srs.repetitions {
36 1 => 1,
37 2 => 6,
38 _ => (srs.interval as f64 * srs.ease_factor).round() as u32,
39 };
40
41 srs.ease_factor += 0.1 - (5.0 - q) * (0.08 + (5.0 - q) * 0.02);
42 if srs.ease_factor < 1.3 {
43 srs.ease_factor = 1.3;
44 }
45 } else {
46 srs.repetitions = 0;
47 srs.interval = 1;
48 }
49}
50
51pub fn calculate_next_review(current_date: &str, interval_days: u32) -> String {
52 if let Ok(date) = parse_date(current_date) {
53 let next = date + interval_days as i64;
54 format_date(next)
55 } else {
56 current_date.to_string()
57 }
58}
59
60pub fn is_due(next_review: Option<&str>, today: &str) -> bool {
61 match next_review {
62 None => true,
63 Some(review_date) => {
64 if let (Ok(review), Ok(now)) = (parse_date(review_date), parse_date(today)) {
65 review <= now
66 } else {
67 true
68 }
69 }
70 }
71}
72
73fn parse_date(date_str: &str) -> Result<i64, ()> {
74 let parts: Vec<&str> = date_str.split('-').collect();
75 if parts.len() != 3 {
76 return Err(());
77 }
78
79 let year: i64 = parts[0].parse().map_err(|_| ())?;
80 let month: i64 = parts[1].parse().map_err(|_| ())?;
81 let day: i64 = parts[2].parse().map_err(|_| ())?;
82
83 Ok(year * 10000 + month * 100 + day)
84}
85
86fn format_date(date_num: i64) -> String {
87 let year = date_num / 10000;
88 let month = (date_num % 10000) / 100;
89 let day = date_num % 100;
90
91 let (year, month, day) = normalize_date(year as i32, month as i32, day as i32);
92 format!("{:04}-{:02}-{:02}", year, month, day)
93}
94
95fn normalize_date(year: i32, month: i32, day: i32) -> (i32, i32, i32) {
96 let days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
97
98 let mut y = year;
99 let mut m = month;
100 let mut d = day;
101
102 while d > days_in_month[(m - 1) as usize] {
103 d -= days_in_month[(m - 1) as usize];
104 m += 1;
105 if m > 12 {
106 m = 1;
107 y += 1;
108 }
109 }
110
111 (y, m, d)
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_sm2_first_correct() {
120 let mut srs = SrsData::default();
121 sm2_update(&mut srs, ResponseQuality::Perfect);
122
123 assert_eq!(srs.repetitions, 1);
124 assert_eq!(srs.interval, 1);
125 assert!(srs.ease_factor > 2.5);
126 }
127
128 #[test]
129 fn test_sm2_second_correct() {
130 let mut srs = SrsData::default();
131 sm2_update(&mut srs, ResponseQuality::Perfect);
132 sm2_update(&mut srs, ResponseQuality::Perfect);
133
134 assert_eq!(srs.repetitions, 2);
135 assert_eq!(srs.interval, 6);
136 }
137
138 #[test]
139 fn test_sm2_incorrect_resets() {
140 let mut srs = SrsData::default();
141 sm2_update(&mut srs, ResponseQuality::Perfect);
142 sm2_update(&mut srs, ResponseQuality::Perfect);
143 sm2_update(&mut srs, ResponseQuality::Incorrect);
144
145 assert_eq!(srs.repetitions, 0);
146 assert_eq!(srs.interval, 1);
147 }
148
149 #[test]
150 fn test_sm2_ease_factor_minimum() {
151 let mut srs = SrsData::default();
152 srs.ease_factor = 1.3;
153 sm2_update(&mut srs, ResponseQuality::CorrectDifficult);
154
155 assert!(srs.ease_factor >= 1.3);
156 }
157
158 #[test]
159 fn test_is_due_none() {
160 assert!(is_due(None, "2025-01-01"));
161 }
162
163 #[test]
164 fn test_is_due_past() {
165 assert!(is_due(Some("2025-01-01"), "2025-01-02"));
166 }
167
168 #[test]
169 fn test_is_due_future() {
170 assert!(!is_due(Some("2025-01-05"), "2025-01-02"));
171 }
172
173 #[test]
174 fn test_is_due_today() {
175 assert!(is_due(Some("2025-01-01"), "2025-01-01"));
176 }
177
178 #[test]
179 fn test_calculate_next_review() {
180 let next = calculate_next_review("2025-01-15", 6);
181 assert_eq!(next, "2025-01-21");
182 }
183
184 #[test]
185 fn test_calculate_next_review_month_overflow() {
186 let next = calculate_next_review("2025-01-28", 6);
187 assert_eq!(next, "2025-02-03");
188 }
189
190 #[test]
191 fn test_response_quality_is_correct() {
192 assert!(!ResponseQuality::Blackout.is_correct());
193 assert!(!ResponseQuality::Incorrect.is_correct());
194 assert!(!ResponseQuality::IncorrectEasy.is_correct());
195 assert!(ResponseQuality::CorrectDifficult.is_correct());
196 assert!(ResponseQuality::CorrectHesitation.is_correct());
197 assert!(ResponseQuality::Perfect.is_correct());
198 }
199}