162 lines
4.8 KiB
C#
162 lines
4.8 KiB
C#
namespace DramaLing.Api.Services;
|
||
|
||
public record SM2Input(
|
||
int Quality, // 1-5 評分
|
||
float EasinessFactor, // 難度係數
|
||
int Repetitions, // 重複次數
|
||
int IntervalDays // 當前間隔天數
|
||
);
|
||
|
||
public record SM2Result(
|
||
float EasinessFactor, // 新的難度係數
|
||
int Repetitions, // 新的重複次數
|
||
int IntervalDays, // 新的間隔天數
|
||
DateTime NextReviewDate // 下次複習日期
|
||
);
|
||
|
||
public static class SM2Algorithm
|
||
{
|
||
// SM-2 算法常數
|
||
private const float MIN_EASINESS_FACTOR = 1.3f;
|
||
private const float MAX_EASINESS_FACTOR = 2.5f;
|
||
private const float INITIAL_EASINESS_FACTOR = 2.5f;
|
||
private const int MIN_INTERVAL = 1;
|
||
private const int MAX_INTERVAL = 365;
|
||
|
||
/// <summary>
|
||
/// 計算下次複習的間隔和參數
|
||
/// </summary>
|
||
public static SM2Result Calculate(SM2Input input)
|
||
{
|
||
var (quality, easinessFactor, repetitions, intervalDays) = input;
|
||
|
||
// 驗證輸入參數
|
||
if (quality < 1 || quality > 5)
|
||
throw new ArgumentException("Quality must be between 1 and 5", nameof(input));
|
||
|
||
// 更新難度係數
|
||
var newEasinessFactor = UpdateEasinessFactor(easinessFactor, quality);
|
||
|
||
int newRepetitions;
|
||
int newIntervalDays;
|
||
|
||
// 如果回答錯誤 (quality < 3),重置進度
|
||
if (quality < 3)
|
||
{
|
||
newRepetitions = 0;
|
||
newIntervalDays = 1;
|
||
}
|
||
else
|
||
{
|
||
// 如果回答正確,增加重複次數並計算新間隔
|
||
newRepetitions = repetitions + 1;
|
||
|
||
newIntervalDays = newRepetitions switch
|
||
{
|
||
1 => 1,
|
||
2 => 6,
|
||
_ => (int)Math.Round(intervalDays * newEasinessFactor)
|
||
};
|
||
}
|
||
|
||
// 限制間隔範圍
|
||
newIntervalDays = Math.Clamp(newIntervalDays, MIN_INTERVAL, MAX_INTERVAL);
|
||
|
||
// 計算下次複習日期
|
||
var nextReviewDate = DateTime.Today.AddDays(newIntervalDays);
|
||
|
||
return new SM2Result(
|
||
newEasinessFactor,
|
||
newRepetitions,
|
||
newIntervalDays,
|
||
nextReviewDate
|
||
);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新難度係數
|
||
/// </summary>
|
||
private static float UpdateEasinessFactor(float currentEF, int quality)
|
||
{
|
||
// SM-2 公式:EF' = EF + (0.1 - (5-q) * (0.08 + (5-q) * 0.02))
|
||
var newEF = currentEF + (0.1f - (5 - quality) * (0.08f + (5 - quality) * 0.02f));
|
||
|
||
// 限制在有效範圍內
|
||
return Math.Clamp(newEF, MIN_EASINESS_FACTOR, MAX_EASINESS_FACTOR);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 獲取初始參數(新詞卡)
|
||
/// </summary>
|
||
public static SM2Input GetInitialParameters()
|
||
{
|
||
return new SM2Input(
|
||
Quality: 3,
|
||
EasinessFactor: INITIAL_EASINESS_FACTOR,
|
||
Repetitions: 0,
|
||
IntervalDays: 1
|
||
);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根據評分獲取描述
|
||
/// </summary>
|
||
public static string GetQualityDescription(int quality)
|
||
{
|
||
return quality switch
|
||
{
|
||
1 => "完全不記得",
|
||
2 => "有印象但錯誤",
|
||
3 => "困難但正確",
|
||
4 => "猶豫後正確",
|
||
5 => "輕鬆正確",
|
||
_ => "無效評分"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 計算掌握度百分比
|
||
/// </summary>
|
||
public static int CalculateMastery(int repetitions, float easinessFactor)
|
||
{
|
||
// 基於重複次數和難度係數計算掌握度 (0-100)
|
||
var baseScore = Math.Min(repetitions * 20, 80); // 重複次數最多貢獻80分
|
||
var efficiencyBonus = Math.Min((easinessFactor - 1.3f) * 16.67f, 20f); // 難度係數最多貢獻20分
|
||
|
||
return Math.Min((int)Math.Round(baseScore + efficiencyBonus), 100);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 複習優先級計算器
|
||
/// </summary>
|
||
public static class ReviewPriorityCalculator
|
||
{
|
||
/// <summary>
|
||
/// 計算複習優先級 (數字越大優先級越高)
|
||
/// </summary>
|
||
public static double CalculatePriority(DateTime nextReviewDate, float easinessFactor, int repetitions)
|
||
{
|
||
var now = DateTime.Today;
|
||
var daysDiff = (now - nextReviewDate).Days;
|
||
|
||
// 過期天數的權重 (越過期優先級越高)
|
||
var overdueWeight = Math.Max(0, daysDiff) * 10;
|
||
|
||
// 難度權重 (越難的優先級越高)
|
||
var difficultyWeight = (3.8f - easinessFactor) * 5;
|
||
|
||
// 新詞權重 (新詞優先級較高)
|
||
var newWordWeight = repetitions == 0 ? 20 : 0;
|
||
|
||
return overdueWeight + difficultyWeight + newWordWeight;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 獲取應該複習的詞卡
|
||
/// </summary>
|
||
public static bool ShouldReview(DateTime nextReviewDate)
|
||
{
|
||
return DateTime.Today >= nextReviewDate;
|
||
}
|
||
} |