SM-2 Algorithm
Spaced repetition algorithm implementation and helper functions
Overview
LoopKit implements the SM-2 (SuperMemo 2) algorithm, the most widely-used spaced repetition algorithm. Cards progress through states based on user grades.
SM2Algorithm
Implements the SRSAlgorithm interface.
calculateNextState
calculateNextState(
card: SRSState,
grade: Grade,
config: DeckConfig,
now?: Date,
): SRSStateCalculates the next card state based on the current state and user grade.
State transitions by grade:
New / Learning Cards
| Grade | Effect |
|---|---|
again | Reset to step 0, schedule after first learning step |
hard | Stay on current step, schedule after 1.5x step duration |
good | Advance to next step. If all steps complete → graduate to review |
easy | Immediately graduate to review with easyInterval |
Review Cards
| Grade | Effect |
|---|---|
again | Lapse → relearning. Ease -0.2, interval x lapseNewInterval |
hard | Ease -0.15, interval x hardIntervalMultiplier |
good | Ease unchanged, interval x ease factor |
easy | Ease +0.15, interval x ease factor x easyBonus |
getInitialState
getInitialState(config: DeckConfig, now?: Date): SRSStateReturns the starting state for a new card:
{
state: 'new',
easeFactor: config.startingEaseFactor, // Default: 2.5
interval: 0,
dueDate: now,
currentStep: 0,
lapseCount: 0,
reviewCount: 0,
}SRSAlgorithm Interface
To implement a custom algorithm:
interface SRSAlgorithm {
calculateNextState(card: SRSState, grade: Grade, config: DeckConfig, now?: Date): SRSState;
getInitialState(config: DeckConfig, now?: Date): SRSState;
}
interface SRSState {
state: 'new' | 'learning' | 'review' | 'relearning';
easeFactor: number;
interval: number; // Days
dueDate: Date;
currentStep: number; // Index into learning/relearning steps
lapseCount: number;
reviewCount: number;
}Helper Functions
applyFuzz
function applyFuzz(interval: number, enabled: boolean): numberAdds ±5% variance to intervals (only for intervals >= 3 days). Prevents cards with similar intervals from always being reviewed together.
clampEase
function clampEase(ease: number): numberEnsures the ease factor never drops below 1.3.
clampInterval
function clampInterval(interval: number, max: number): numberClamps the interval between 0 and maxInterval.
calculateDueDate
function calculateDueDate(now: Date, intervalDays: number): DateCalculates the next due date by adding intervalDays to now.
calculateLearningDueDate
function calculateLearningDueDate(now: Date, stepMinutes: number): DateCalculates the due date for learning steps (minutes precision).
previewNextIntervals
function previewNextIntervals(
card: SRSState,
config: DeckConfig,
algorithm: SRSAlgorithm,
now?: Date,
): Record<Grade, string>Calculates and formats the next interval for each possible grade. Returns human-readable strings like "1m", "6h", "10d", "2mo", "1.5y".
formatInterval
function formatInterval(intervalDays: number): stringConverts a day-based interval to a human-readable string:
| Range | Format | Example |
|---|---|---|
| < 1 hour | Minutes | 1m, 30m |
| < 1 day | Hours | 6h, 12h |
| < 31 days | Days | 1d, 10d |
| < 365 days | Months | 2mo, 6mo |
| >= 365 days | Years | 1y, 2.5y |
Day Boundary Utilities
getDayStart
function getDayStart(now: Date, nextDayStartsAt: number): DateReturns the start of the current "study day" based on nextDayStartsAt (default: 4 AM). If the current time is before the boundary, the day started yesterday.
isDueToday
function isDueToday(dueDate: Date, now: Date, nextDayStartsAt: number): booleanChecks if a card is due within the current study day.
getDaysSince
function getDaysSince(date: Date, now: Date): numberReturns the number of full days between two dates.