forked from admin/french-vocab
127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
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;
|
||
}
|