init
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import { EBBINGHAUS_INTERVALS } from '../constants.js';
|
||||
import { state } from '../core/state.js';
|
||||
import { MAX_RECORDS, saveRecords, saveSchedule } from '../core/storage.js';
|
||||
|
||||
/**
|
||||
* Ebbinghaus scheduling and study-record helpers. Inputs are word IDs, dates,
|
||||
* and quiz outcomes; mutations update shared state and persist records:<libraryId>
|
||||
* and schedule:<libraryId> through the storage service.
|
||||
*/
|
||||
// ==================== Ebbinghaus Algorithm ====================
|
||||
export function toLocalDateStr(d) {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function getToday() {
|
||||
return toLocalDateStr(new Date());
|
||||
}
|
||||
|
||||
export function addDays(dateStr, days) {
|
||||
const [y, m, day] = dateStr.split('-').map(Number);
|
||||
const d = new Date(y, m - 1, day);
|
||||
d.setDate(d.getDate() + days);
|
||||
return toLocalDateStr(d);
|
||||
}
|
||||
|
||||
export function getDueWords() {
|
||||
const today = getToday();
|
||||
const due = [];
|
||||
for (const [wordId, sch] of Object.entries(state.schedule)) {
|
||||
if (sch.nextReview <= today) {
|
||||
const word = state.words.find(w => w.id === Number(wordId));
|
||||
if (word) due.push({ ...word, schedule: sch });
|
||||
}
|
||||
}
|
||||
return due;
|
||||
}
|
||||
|
||||
export function initWordSchedule(wordId, save = true) {
|
||||
if (state.schedule[wordId]) return true;
|
||||
|
||||
state.schedule[wordId] = {
|
||||
stage: 0,
|
||||
nextReview: getToday(),
|
||||
correctCount: 0,
|
||||
incorrectCount: 0
|
||||
};
|
||||
if (!save || saveSchedule()) return true;
|
||||
|
||||
delete state.schedule[wordId];
|
||||
return false;
|
||||
}
|
||||
|
||||
export function trimRecords(records, maxRecords, keepPerWord) {
|
||||
if (records.length <= maxRecords) return records;
|
||||
|
||||
const selected = new Set();
|
||||
const counts = new Map();
|
||||
|
||||
// 第一轮尽可能为每个单词和记录类型保留最新一条,避免整个组合被裁掉
|
||||
for (let i = records.length - 1; i >= 0; i--) {
|
||||
const record = records[i];
|
||||
const key = `${record.wordId}:${record.type}`;
|
||||
if (counts.has(key)) continue;
|
||||
|
||||
selected.add(i);
|
||||
counts.set(key, 1);
|
||||
if (selected.size === maxRecords) break;
|
||||
}
|
||||
|
||||
// 第二轮按时间从新到旧填充剩余容量,每个组合优先保留 keepPerWord 条
|
||||
for (let i = records.length - 1; i >= 0 && selected.size < maxRecords; i--) {
|
||||
if (selected.has(i)) continue;
|
||||
|
||||
const record = records[i];
|
||||
const key = `${record.wordId}:${record.type}`;
|
||||
const count = counts.get(key) || 0;
|
||||
if (count < keepPerWord) {
|
||||
selected.add(i);
|
||||
counts.set(key, count + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 组合较少时前两轮可能未填满容量,继续保留其余最新记录,避免一次裁剪丢失大量历史
|
||||
for (let i = records.length - 1; i >= 0 && selected.size < maxRecords; i--) {
|
||||
selected.add(i);
|
||||
}
|
||||
|
||||
return [...selected]
|
||||
.sort((a, b) => a - b)
|
||||
.map(index => records[index]);
|
||||
}
|
||||
|
||||
export function recordAndUpdateSchedule(wordId, isCorrect, type, quizMode, options = {}) {
|
||||
const previousRecords = state.records;
|
||||
const previousSchedule = state.schedule;
|
||||
const nextRecords = previousRecords.slice();
|
||||
const previousWordSchedule = previousSchedule[wordId];
|
||||
const nextWordSchedule = previousWordSchedule ? { ...previousWordSchedule } : {
|
||||
stage: 0,
|
||||
nextReview: getToday(),
|
||||
correctCount: 0,
|
||||
incorrectCount: 0
|
||||
};
|
||||
|
||||
nextRecords.push({
|
||||
wordId,
|
||||
date: getToday(),
|
||||
time: Date.now(),
|
||||
isCorrect,
|
||||
type,
|
||||
quizMode
|
||||
});
|
||||
|
||||
let recordsToSave = nextRecords;
|
||||
if (options.removeIncorrectQuizRecords && isCorrect) {
|
||||
recordsToSave = nextRecords.filter(record => !(
|
||||
record.wordId === wordId
|
||||
&& record.type === 'quiz'
|
||||
&& !record.isCorrect
|
||||
));
|
||||
}
|
||||
if (recordsToSave.length > MAX_RECORDS) {
|
||||
recordsToSave = trimRecords(recordsToSave, MAX_RECORDS, 10);
|
||||
}
|
||||
|
||||
if (isCorrect) {
|
||||
nextWordSchedule.correctCount++;
|
||||
nextWordSchedule.stage = Math.min(nextWordSchedule.stage + 1, EBBINGHAUS_INTERVALS.length - 1);
|
||||
} else {
|
||||
nextWordSchedule.incorrectCount++;
|
||||
nextWordSchedule.stage = Math.max(0, nextWordSchedule.stage - 1);
|
||||
}
|
||||
nextWordSchedule.nextReview = addDays(getToday(), EBBINGHAUS_INTERVALS[nextWordSchedule.stage]);
|
||||
|
||||
state.records = recordsToSave;
|
||||
state.schedule = { ...previousSchedule, [wordId]: nextWordSchedule };
|
||||
|
||||
const recordsSaved = saveRecords();
|
||||
const scheduleSaved = saveSchedule();
|
||||
if (recordsSaved && scheduleSaved) return true;
|
||||
|
||||
state.records = previousRecords;
|
||||
state.schedule = previousSchedule;
|
||||
saveRecords();
|
||||
saveSchedule();
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user