add language learner project
Some checks failed
Test and deploy staging / staging (push) Has been cancelled
Some checks failed
Test and deploy staging / staging (push) Has been cancelled
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>
This commit is contained in:
76
src/lib/language-learner/srs.ts
Normal file
76
src/lib/language-learner/srs.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/** 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);
|
||||
Reference in New Issue
Block a user