/** 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);