Skip to main content

logicaffeine_web/
srs.rs

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}