2026-03-07 05:42:32 +00:00
|
|
|
|
import type { StudyProgress, DifficultyRating } from '../types/vocabulary';
|
|
|
|
|
|
|
2026-03-18 15:18:47 +08:00
|
|
|
|
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 的研究
|
|
|
|
|
|
*/
|
2026-03-07 05:42:32 +00:00
|
|
|
|
export function calculateNextReview(
|
|
|
|
|
|
progress: StudyProgress,
|
2026-03-18 15:18:47 +08:00
|
|
|
|
rating: DifficultyRating,
|
|
|
|
|
|
config: SRSConfig = DEFAULT_CONFIG
|
2026-03-07 05:42:32 +00:00
|
|
|
|
): StudyProgress {
|
|
|
|
|
|
const newProgress = { ...progress };
|
2026-03-18 15:18:47 +08:00
|
|
|
|
const { minEase, maxInterval } = config;
|
2026-03-07 05:42:32 +00:00
|
|
|
|
|
2026-03-18 15:18:47 +08:00
|
|
|
|
// 如果是新单词(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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 05:42:32 +00:00
|
|
|
|
switch (rating) {
|
|
|
|
|
|
case 'again':
|
2026-03-18 15:18:47 +08:00
|
|
|
|
// 完全重置,但保留一些学习历史
|
2026-03-07 05:42:32 +00:00
|
|
|
|
newProgress.interval = 1;
|
|
|
|
|
|
newProgress.repetitions = 0;
|
2026-03-18 15:18:47 +08:00
|
|
|
|
newProgress.easeFactor = Math.max(minEase, progress.easeFactor - 0.2);
|
2026-03-07 05:42:32 +00:00
|
|
|
|
break;
|
2026-03-18 15:18:47 +08:00
|
|
|
|
|
2026-03-07 05:42:32 +00:00
|
|
|
|
case 'hard':
|
2026-03-18 15:18:47 +08:00
|
|
|
|
// 困难:间隔增长较慢
|
|
|
|
|
|
newProgress.interval = Math.max(1, Math.round(progress.interval * 1.2));
|
2026-03-07 05:42:32 +00:00
|
|
|
|
newProgress.repetitions += 1;
|
2026-03-18 15:18:47 +08:00
|
|
|
|
newProgress.easeFactor = Math.max(minEase, progress.easeFactor - 0.15);
|
2026-03-07 05:42:32 +00:00
|
|
|
|
break;
|
2026-03-18 15:18:47 +08:00
|
|
|
|
|
2026-03-07 05:42:32 +00:00
|
|
|
|
case 'good':
|
2026-03-18 15:18:47 +08:00
|
|
|
|
// 良好:标准 SM-2 算法
|
2026-03-07 05:42:32 +00:00
|
|
|
|
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;
|
2026-03-18 15:18:47 +08:00
|
|
|
|
|
2026-03-07 05:42:32 +00:00
|
|
|
|
case 'easy':
|
2026-03-18 15:18:47 +08:00
|
|
|
|
// 简单:间隔增长更快
|
2026-03-07 05:42:32 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 15:18:47 +08:00
|
|
|
|
// 限制最大间隔
|
|
|
|
|
|
newProgress.interval = Math.min(newProgress.interval, maxInterval);
|
|
|
|
|
|
|
|
|
|
|
|
// 确保 easeFactor 在合理范围内
|
|
|
|
|
|
newProgress.easeFactor = Math.max(minEase, Math.min(newProgress.easeFactor, 3.0));
|
|
|
|
|
|
|
|
|
|
|
|
// 计算下次复习日期
|
2026-03-07 05:42:32 +00:00
|
|
|
|
const now = new Date();
|
|
|
|
|
|
newProgress.nextReviewDate = new Date(now.getTime() + newProgress.interval * 24 * 60 * 60 * 1000);
|
|
|
|
|
|
newProgress.lastStudiedDate = now;
|
|
|
|
|
|
|
|
|
|
|
|
return newProgress;
|
|
|
|
|
|
}
|
2026-03-18 15:18:47 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 计算单词的掌握程度 (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;
|
|
|
|
|
|
}
|