aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/shared/fsrs.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/shared/fsrs.ts')
-rw-r--r--src/shared/fsrs.ts74
1 files changed, 74 insertions, 0 deletions
diff --git a/src/shared/fsrs.ts b/src/shared/fsrs.ts
new file mode 100644
index 0000000..1cf60ba
--- /dev/null
+++ b/src/shared/fsrs.ts
@@ -0,0 +1,74 @@
+import {
+ type Card as FSRSCard,
+ type State as FSRSState,
+ fsrs,
+ type Grade,
+} from "ts-fsrs";
+
+const f = fsrs({ enable_fuzz: true });
+
+export interface ScheduleInput {
+ state: number;
+ due: Date;
+ stability: number;
+ difficulty: number;
+ elapsedDays: number;
+ scheduledDays: number;
+ reps: number;
+ lapses: number;
+ lastReview: Date | null;
+}
+
+export interface ScheduleResult {
+ state: number;
+ due: Date;
+ stability: number;
+ difficulty: number;
+ elapsedDays: number;
+ scheduledDays: number;
+ reps: number;
+ lapses: number;
+ lastReview: Date;
+ /** Days elapsed since the previous review (for ReviewLog). */
+ reviewElapsedDays: number;
+}
+
+export function computeNextSchedule(
+ card: ScheduleInput,
+ rating: 1 | 2 | 3 | 4,
+ now: Date,
+): ScheduleResult {
+ const fsrsCard: FSRSCard = {
+ due: card.due,
+ stability: card.stability,
+ difficulty: card.difficulty,
+ elapsed_days: card.elapsedDays,
+ scheduled_days: card.scheduledDays,
+ reps: card.reps,
+ lapses: card.lapses,
+ state: card.state as FSRSState,
+ last_review: card.lastReview ?? undefined,
+ learning_steps: 0,
+ };
+
+ const result = f.next(fsrsCard, now, rating as Grade);
+
+ const reviewElapsedDays = card.lastReview
+ ? Math.round(
+ (now.getTime() - card.lastReview.getTime()) / (1000 * 60 * 60 * 24),
+ )
+ : 0;
+
+ return {
+ state: result.card.state,
+ due: result.card.due,
+ stability: result.card.stability,
+ difficulty: result.card.difficulty,
+ elapsedDays: result.card.elapsed_days,
+ scheduledDays: result.card.scheduled_days,
+ reps: result.card.reps,
+ lapses: result.card.lapses,
+ lastReview: now,
+ reviewElapsedDays,
+ };
+}