feat(backend): leitner algorithm with tests

This commit is contained in:
2026-05-20 20:42:41 +02:00
parent 1584901c0a
commit dc64a08320
2 changed files with 75 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest';
import { applyResult, BOX_INTERVALS_SEC, MAX_BOX } from './leitner.js';
describe('leitner.applyResult', () => {
const now = 1_000_000;
it('moves correct answer to next box and schedules due', () => {
const next = applyResult({ box: 1, correctCount: 0, incorrectCount: 0 }, 'correct', now);
expect(next.box).toBe(2);
expect(next.nextDueAt).toBe(now + BOX_INTERVALS_SEC[2]!);
expect(next.correctCount).toBe(1);
});
it('caps box at MAX_BOX on correct answer', () => {
const next = applyResult({ box: MAX_BOX, correctCount: 9, incorrectCount: 0 }, 'correct', now);
expect(next.box).toBe(MAX_BOX);
expect(next.nextDueAt).toBe(now + BOX_INTERVALS_SEC[MAX_BOX]!);
});
it('resets to box 1 on incorrect', () => {
const next = applyResult({ box: 4, correctCount: 3, incorrectCount: 1 }, 'incorrect', now);
expect(next.box).toBe(1);
expect(next.nextDueAt).toBe(now + BOX_INTERVALS_SEC[1]!);
expect(next.incorrectCount).toBe(2);
});
});

View File

@@ -0,0 +1,49 @@
import type { AttemptResult } from '@flashcard/shared';
export const MAX_BOX = 5;
// index 1..5; index 0 unused
export const BOX_INTERVALS_SEC: Record<number, number> = {
1: 0,
2: 1 * 24 * 60 * 60,
3: 3 * 24 * 60 * 60,
4: 7 * 24 * 60 * 60,
5: 14 * 24 * 60 * 60,
};
export interface ProgressLike {
box: number;
correctCount: number;
incorrectCount: number;
}
export interface ProgressDelta {
box: number;
correctCount: number;
incorrectCount: number;
nextDueAt: number;
lastShownAt: number;
}
export function applyResult(
current: ProgressLike,
result: AttemptResult,
nowSec: number
): ProgressDelta {
let box = current.box;
let correctCount = current.correctCount;
let incorrectCount = current.incorrectCount;
if (result === 'correct') {
box = Math.min(MAX_BOX, current.box + 1);
correctCount += 1;
} else {
box = 1;
incorrectCount += 1;
}
return {
box,
correctCount,
incorrectCount,
nextDueAt: nowSec + BOX_INTERVALS_SEC[box]!,
lastShownAt: nowSec,
};
}