My App
LoopKitBackend

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,
): SRSState

Calculates the next card state based on the current state and user grade.

State transitions by grade:

New / Learning Cards

GradeEffect
againReset to step 0, schedule after first learning step
hardStay on current step, schedule after 1.5x step duration
goodAdvance to next step. If all steps complete → graduate to review
easyImmediately graduate to review with easyInterval

Review Cards

GradeEffect
againLapse → relearning. Ease -0.2, interval x lapseNewInterval
hardEase -0.15, interval x hardIntervalMultiplier
goodEase unchanged, interval x ease factor
easyEase +0.15, interval x ease factor x easyBonus

getInitialState

getInitialState(config: DeckConfig, now?: Date): SRSState

Returns 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): number

Adds ±5% variance to intervals (only for intervals >= 3 days). Prevents cards with similar intervals from always being reviewed together.

clampEase

function clampEase(ease: number): number

Ensures the ease factor never drops below 1.3.

clampInterval

function clampInterval(interval: number, max: number): number

Clamps the interval between 0 and maxInterval.

calculateDueDate

function calculateDueDate(now: Date, intervalDays: number): Date

Calculates the next due date by adding intervalDays to now.

calculateLearningDueDate

function calculateLearningDueDate(now: Date, stepMinutes: number): Date

Calculates 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): string

Converts a day-based interval to a human-readable string:

RangeFormatExample
< 1 hourMinutes1m, 30m
< 1 dayHours6h, 12h
< 31 daysDays1d, 10d
< 365 daysMonths2mo, 6mo
>= 365 daysYears1y, 2.5y

Day Boundary Utilities

getDayStart

function getDayStart(now: Date, nextDayStartsAt: number): Date

Returns 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): boolean

Checks if a card is due within the current study day.

getDaysSince

function getDaysSince(date: Date, now: Date): number

Returns the number of full days between two dates.

On this page