SM-2 Spaced Repetition Algorithm
Complete Implementation Guide for Developers
What is SM-2?
SM-2 (SuperMemo 2) is the algorithm that revolutionized computer-assisted learning in 1987. It's the
grandfather of most modern spaced repetition systems, including Anki's default algorithm. The core
idea: calculate the optimal time to review information just before you're about to forget it.
📐 The Core Algorithm
Key Variables
javascript
// For each piece of information you track:
{
n: 0, // Repetition number (how many times reviewed)
EF: 2.5, // Easiness Factor (how easy the item is for you)
I: 1, // Inter-repetition interval (days until next review)
lastReview: Date, // When last reviewed
nextReview: Date // When to review next
}
The SM-2 Formula
javascript
function calculateNextInterval(quality, n, EF, I) {
// quality: Your self-assessment (0-5 scale)
// 0 - Complete blackout
// 1 - Incorrect, but recognized when shown answer
// 2 - Incorrect, but felt close
// 3 - Correct, but difficult recall
// 4 - Correct, with hesitation
// 5 - Perfect recall
// Step 1: Calculate new Easiness Factor
let newEF = EF + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
// Keep EF bounded (minimum 1.3 for hardest items)
if (newEF < 1.3) newEF = 1.3;
// Step 2: Calculate next interval
let newInterval;
if (quality < 3) {
// Failed recall - reset to beginning
newInterval = 1;
n = 0;
} else {
// Successful recall - increase interval
if (n === 0) {
newInterval = 1;
} else if (n === 1) {
newInterval = 6;
} else {
newInterval = Math.round(I * newEF);
}
n = n + 1;
}
return {
interval: newInterval,
EF: newEF,
repetition: n
};
}
🎯 How It Actually Works
Example Learning Journey
Let's say you're learning the concept "React Hooks useEffect cleanup function":
Day 0 (First Learning)
You learn it for the first time
Initial values: EF = 2.5 , n = 0 , I = 1
Next review: Tomorrow
Day 1 (First Review)
You recall it correctly but with hesitation (quality = 4)
New EF: 2.5 + (0.1 - 1 * 0.1) = 2.5 (unchanged)
Next interval: 6 days
Next review: Day 7
Day 7 (Second Review)
Perfect recall (quality = 5)
New EF: 2.5 + 0.1 = 2.6 (gets easier)
Next interval: 6 * 2.6 = 15.6 ≈ 16 days
Next review: Day 23
Day 23 (Third Review)
Struggled but got it (quality = 3)
New EF: 2.6 + (0.1 - 2 * 0.12) = 2.46
Next interval: 16 * 2.46 = 39.36 ≈ 39 days
Next review: Day 62
💻 Complete JavaScript Implementation
javascript
class SM2SpacedRepetition {
constructor() {
this.cards = new Map(); // Store all learning items
}
// Add new item to learn
addCard(id, content) {
this.cards.set(id, {
id: id,
content: content,
n: 0,
EF: 2.5,
I: 1,
lastReview: null,
nextReview: new Date(),
history: []
});
}
// Review a card and calculate next review
reviewCard(id, quality) {
const card = this.cards.get(id);
if (!card) throw new Error('Card not found');
// Record the review
const reviewDate = new Date();
card.history.push({
date: reviewDate,
quality: quality,
EF: card.EF,
interval: card.I
});
// Calculate new values
let newEF = card.EF + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
newEF = Math.max(1.3, newEF); // Minimum bound
let newInterval;
let newN = card.n;
if (quality < 3) {
// Reset on failure
newInterval = 1;
newN = 0;
} else {
// Progress on success
switch(card.n) {
case 0:
newInterval = 1;
break;
case 1:
newInterval = 6;
break;
default:
newInterval = Math.round(card.I * card.EF);
}
newN = card.n + 1;
}
// Update card
card.n = newN;
card.EF = newEF;
card.I = newInterval;
card.lastReview = reviewDate;
card.nextReview = this.addDays(reviewDate, newInterval);
return {
nextReview: card.nextReview,
interval: newInterval,
easiness: newEF
};
}
// Get cards due for review
getDueCards() {
const now = new Date();
const due = [];
for (const [id, card] of this.cards) {
if (card.nextReview <= now) {
due.push(card);
}
}
// Sort by priority (overdue items first)
return due.sort((a, b) => a.nextReview - b.nextReview);
}
// Helper function
addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
// Get statistics
getStats() {
let total = this.cards.size;
let learned = 0;
let due = 0;
const now = new Date();
for (const card of this.cards.values
values()) {
if (card.n > 0) learned++;
if (card.nextReview <= now) due++;
}
return {
total: total,
learned: learned,
due: due,
retention: learned / total * 100
};
}
}
// Usage Example
const srs = new SM2SpacedRepetition();
// Add a new concept to learn
srs.addCard('react-1', 'useEffect cleanup function returns cleanup logic');
// Review it (quality: 0-5)
const result = srs.reviewCard('react-1', 4); // Correct with hesitation
console.log(`Next review in ${result.interval} days`);
🔧 Custom Modifications for Your Use Case
1. Time-of-Day Optimization
javascript
// Modify intervals based on your schedule
function adjustForSchedule(baseInterval, cardType) {
const adjustments = {
'technical': 1.0, // Review technical stuff normally
'concepts': 1.2, // Give more time for abstract concepts
'commands': 0.8, // Review commands more frequently
'facts': 1.1 // Slightly longer for pure facts
};
return Math.round(baseInterval * (adjustments[cardType] || 1.0));
}
2. Contextual Difficulty Adjustment
javascript
// Adjust EF based on context
function contextualEF(baseEF, context) {
// If reviewed during gym (harder context), boost EF
if (context === 'gym') {
return baseEF * 1.1; // Give credit for harder conditions
}
// If reviewed while tired (after 8pm), adjust
const hour = new Date().getHours();
if (hour >= 20) {
return baseEF * 1.05;
}
return baseEF;
}
3. Workout-Integrated Algorithm
javascript
class WorkoutSRS extends SM2SpacedRepetition {
reviewCard(id, quality, context = {}) {
// Track where review happened
const location = context.location || 'default';
const energy = context.energyLevel || 'normal';
// Adjust quality based on context
let adjustedQuality = quality;
if (location === 'gym' && quality >= 3) {
// Successful recall at gym = bonus
adjustedQuality = Math.min(5, quality + 0.5);
}
if (energy === 'tired' && quality >= 3) {
// Successful recall when tired = bonus
adjustedQuality = Math.min(5, quality + 0.3);
}
return super.reviewCard(id, adjustedQuality);
}
}
📊 Alternative Algorithms to Consider
1. SM-18 (Latest SuperMemo)
More sophisticated but complex
Considers time of day, sleep quality
Better for long-term retention
2. FSRS (Free Spaced Repetition Scheduler)
javascript
// Simplified FSRS - Better than SM-2 for most cases
function FSRS(difficulty, stability, retrievability) {
// Uses machine learning principles
const optimalInterval = stability * Math.log(0.9) / Math.log(retrievability);
return Math.max(1, Math.round(optimalInterval));
}
3. Custom Hybrid for Developers
javascript
class DeveloperSRS {
calculateInterval(card, quality) {
// Base: SM-2 algorithm
let interval = this.sm2Calculate(card, quality);
// Adjust for information type
if (card.type === 'syntax') {
interval *= 0.7; // See syntax more often
} else if (card.type === 'concept') {
interval *= 1.3; // Concepts need less frequent review
} else if (card.type === 'bug-pattern') {
interval *= 0.5; // Critical to remember
}
// Adjust for source
if (card.source === 'production-bug') {
interval *= 0.6; // Never forget production issues!
}
// Cap based on importance
if (card.importance === 'critical') {
interval = Math.min(interval, 30); // Never go beyond 30 days
}
return interval;
}
}
🎮 Gamification Layer
javascript
class GamifiedSRS extends SM2SpacedRepetition {
constructor() {
super();
this.streaks = new Map();
this.achievements = [];
}
reviewCard(id, quality) {
const result = super.reviewCard(id, quality);
// Track streaks
if (quality >= 3) {
const currentStreak = this.streaks.get(id) || 0;
this.streaks.set(id, currentStreak + 1);
// Check achievements
if (currentStreak === 10) {
this.unlockAchievement('Consistent Learner', id);
}
// Bonus interval for streaks
if (currentStreak > 5) {
result.interval *= 1.1; // 10% bonus for good streaks
}
} else {
this.streaks.set(id, 0); // Reset streak on failure
}
return result;
}
unlockAchievement(name, cardId) {
this.achievements.push({
name: name,
cardId: cardId,
date: new Date()
});
// Could trigger notification here
console.log(`🏆 Achievement Unlocked: ${name}!`);
}
}
🚀 Implementation Tips
1. Start Simple: Begin with basic SM-2, add modifications after 30 days of data
2. Track Everything: Log quality scores, time of day, energy levels
3. Analyze Your Data: After 100+ reviews, analyze your personal forgetting curve
4. Adjust Parameters: Your optimal EF might not be 2.5 - experiment!
5. Mobile Sync: Use IndexedDB for offline, sync when online
📱 Database Schema
sql
-- SQLite schema for mobile app
CREATE TABLE cards (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
type TEXT,
n INTEGER DEFAULT 0,
ef REAL DEFAULT 2.5,
interval INTEGER DEFAULT 1,
last_review TIMESTAMP,
next_review TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
card_id TEXT,
quality INTEGER,
interval_before INTEGER,
interval_after INTEGER,
ef_before REAL,
ef_after REAL,
reviewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
context JSON,
FOREIGN KEY (card_id) REFERENCES cards(id)
);
-- Index for performance
CREATE INDEX idx_next_review ON cards(next_review);
CREATE INDEX idx_card_reviews ON reviews(card_id, reviewed_at);
The beauty of SM-2 is its simplicity and proven effectiveness. Start with the basic implementation,
then customize based on your learning patterns. After 30 days, you'll have enough data to fine-tune
the algorithm specifically for your brain!