Files
personal-website/src/lib/language-learner/srs.ts
Stan Runge 438623d857
Some checks failed
Test and deploy staging / staging (push) Has been cancelled
add language learner project
Dutch and Korean (explained in English) and English (explained in Dutch)
from A1 to C1, with a Hangul level for Korean. Lessons with notes, words
and sentences generate multiple-choice, typing, sentence building, cloze,
listening and matching exercises. Endless challenges: a seeded daily
challenge, sprint, survival, mixed practice, level tests, spaced-repetition
reviews and generated drills (numbers, clock times, de/het, plurals, Dutch
and Korean conjugation, particles, Hangul reading, irregular verbs).
Progress is stored in localStorage with export/import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 17:34:56 +00:00

77 lines
1.9 KiB
TypeScript

/** A simplified SM-2 schedule. Days are counted as local calendar days. */
export type Card = {
due: number;
interval: number;
ease: number;
reps: number;
lapses: number;
seen: number;
right: number;
last: number;
};
export function dayKey(date = new Date()): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
export function dayNumber(key = dayKey()): number {
const [y, m, d] = key.split('-').map(Number);
return Math.round(Date.UTC(y, m - 1, d) / 86_400_000);
}
export const newCard = (today: number): Card => ({
due: today,
interval: 0,
ease: 2.5,
reps: 0,
lapses: 0,
seen: 0,
right: 0,
last: -1
});
export function review(card: Card, correct: boolean, today: number): Card {
const next = { ...card, seen: card.seen + 1, right: card.right + (correct ? 1 : 0) };
if (!correct) {
return {
...next,
reps: 0,
interval: 0,
lapses: card.lapses + 1,
ease: Math.max(1.3, card.ease - 0.2),
due: today,
last: today
};
}
// Grinding the same card again on one day should not push it months ahead.
if (card.last === today && card.reps > 0) return next;
const reps = card.reps + 1;
const interval =
reps === 1 ? 1 : reps === 2 ? 3 : Math.round(Math.max(card.interval, 1) * card.ease);
return {
...next,
reps,
interval,
ease: Math.min(3, card.ease + 0.05),
due: today + interval,
last: today
};
}
export type Mastery = 'new' | 'learning' | 'familiar' | 'known' | 'mastered';
export function mastery(card: Card | undefined): Mastery {
if (!card || card.seen === 0) return 'new';
if (card.interval < 3) return 'learning';
if (card.interval < 14) return 'familiar';
if (card.interval < 60) return 'known';
return 'mastered';
}
/** 0 for new cards, approaching 1 for cards reviewed months apart. */
export const strength = (card: Card | undefined) => (card ? Math.min(1, card.interval / 30) : 0);