Files
french-vocab/src/utils/srs.ts
2026-03-18 15:18:47 +08:00

127 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { StudyProgress, DifficultyRating } from '../types/vocabulary';
interface SRSConfig {
initialEase: number;
minEase: number;
maxInterval: number;
learningStepInterval: number;
}
const DEFAULT_CONFIG: SRSConfig = {
initialEase: 2.5,
minEase: 1.3,
maxInterval: 365, // 最大间隔 365 天
learningStepInterval: 10, // 学习步骤间隔(分钟)
};
/**
* 改进的 SM-2 算法实现
* 基于 Anki 和 SuperMemo 的研究
*/
export function calculateNextReview(
progress: StudyProgress,
rating: DifficultyRating,
config: SRSConfig = DEFAULT_CONFIG
): StudyProgress {
const newProgress = { ...progress };
const { minEase, maxInterval } = config;
// 如果是新单词repetitions === 0使用学习步骤
if (progress.repetitions === 0 && rating === 'again') {
// 标记为"再次",保持在第一步
newProgress.interval = 0;
newProgress.easeFactor = Math.max(minEase, progress.easeFactor - 0.2);
newProgress.nextReviewDate = new Date(Date.now() + DEFAULT_CONFIG.learningStepInterval * 60 * 1000);
newProgress.lastStudiedDate = new Date();
return newProgress;
}
switch (rating) {
case 'again':
// 完全重置,但保留一些学习历史
newProgress.interval = 1;
newProgress.repetitions = 0;
newProgress.easeFactor = Math.max(minEase, progress.easeFactor - 0.2);
break;
case 'hard':
// 困难:间隔增长较慢
newProgress.interval = Math.max(1, Math.round(progress.interval * 1.2));
newProgress.repetitions += 1;
newProgress.easeFactor = Math.max(minEase, progress.easeFactor - 0.15);
break;
case 'good':
// 良好:标准 SM-2 算法
if (progress.repetitions === 0) {
newProgress.interval = 1;
} else if (progress.repetitions === 1) {
newProgress.interval = 6;
} else {
newProgress.interval = Math.round(progress.interval * progress.easeFactor);
}
newProgress.repetitions += 1;
break;
case 'easy':
// 简单:间隔增长更快
if (progress.repetitions === 0) {
newProgress.interval = 4;
} else {
newProgress.interval = Math.round(progress.interval * progress.easeFactor * 1.3);
}
newProgress.repetitions += 1;
newProgress.easeFactor += 0.15;
break;
}
// 限制最大间隔
newProgress.interval = Math.min(newProgress.interval, maxInterval);
// 确保 easeFactor 在合理范围内
newProgress.easeFactor = Math.max(minEase, Math.min(newProgress.easeFactor, 3.0));
// 计算下次复习日期
const now = new Date();
newProgress.nextReviewDate = new Date(now.getTime() + newProgress.interval * 24 * 60 * 60 * 1000);
newProgress.lastStudiedDate = now;
return newProgress;
}
/**
* 计算单词的掌握程度 (0-100%)
*/
export function calculateMastery(progress: StudyProgress): number {
if (progress.repetitions === 0) return 0;
const baseScore = Math.min(progress.repetitions * 20, 80);
const intervalBonus = Math.min(progress.interval / 30 * 20, 20);
return Math.min(100, Math.round(baseScore + intervalBonus));
}
/**
* 获取复习优先级分数(越高越优先)
*/
export function getReviewPriority(progress: StudyProgress): number {
const now = new Date();
const nextReview = new Date(progress.nextReviewDate);
const overdue = now.getTime() - nextReview.getTime();
// 逾期的单词优先级更高
if (overdue > 0) {
return 1000 + Math.min(overdue / (1000 * 60 * 60), 1000); // 每小时增加 1 点优先级
}
// 即将到期的单词
const timeUntilDue = nextReview.getTime() - now.getTime();
const hoursUntilDue = timeUntilDue / (1000 * 60 * 60);
if (hoursUntilDue < 24) {
return 500 + (24 - hoursUntilDue) * 20;
}
return hoursUntilDue;
}