fsrs-algorithm
Version:
Free Spaced Repetition Scheduler (FSRS) algorithm implementation in TypeScript
257 lines • 10.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FSRS = void 0;
const types_1 = require("./types");
const cardValidator_1 = require("./utils/cardValidator");
const timeFuncs_1 = require("./utils/timeFuncs");
// Implementation of FSRS-4.5
// Based on the FSRS-4.5 algorithm specification.
// This version has been updated for clarity and adherence to the standard.
class FSRS {
constructor(parameters) {
// Default FSRS-4.5 parameters optimized for general use.
// These 17 parameters are the core of the algorithm.
this.parameters = {
requestRetention: 0.9, // Target probability of recalling a card
maximumInterval: 36500, // Maximum number of days for an interval
w: [
// w[0]: Initial stability for Again
0.4,
// w[1]: Initial stability for Hard
0.6,
// w[2]: Initial stability for Good
2.4,
// w[3]: Initial stability for Easy
5.8,
// w[4]: Initial difficulty for Good
4.93,
// w[5]: Difficulty change factor
0.94,
// w[6]: Difficulty change factor
0.86,
// w[7]: Mean reversion weight for difficulty
0.01,
// w[8]: Stability increase factor
1.49,
// w[9]: Stability exponent
0.14,
// w[10]: Stability factor for memory retrieval
0.94,
// w[11]: Stability factor for forgotten cards
2.18,
// w[12]: Difficulty exponent for forgotten cards
0.05,
// w[13]: Stability exponent for forgotten cards
0.34,
// w[14]: Retrieval exponent for forgotten cards
1.26,
// w[15]: Penalty factor for "Hard" rating
0.29,
// w[16]: Bonus factor for "Easy" rating
2.61,
],
...parameters,
};
}
/**
* Creates a new, empty card object.
* @param now The current date, defaults to new Date().
* @returns A new Card object.
*/
createEmptyCard(now = new Date()) {
return {
due: new Date(now),
stability: 0,
difficulty: 0,
elapsedDays: 0,
scheduledDays: 0,
reps: 0,
lapses: 0,
state: types_1.State.New,
lastReview: undefined,
};
}
/**
* Generates scheduling information for all possible ratings for a given card.
* @param card The card to schedule.
* @param now The current date of the review.
* @returns An object containing the card and review log for each rating.
*/
schedule(card, now = new Date()) {
if (!card)
throw new Error("card cannot be null or undefined");
if (card.lastReview && now < card.lastReview)
throw new Error("Current time cannot be before the last review");
return this.buildSchedulingCards(card, now);
}
/**
* Converts raw card data, validates it, and then schedules it.
* @param rawData The raw card data from a database or API.
* @param now The current date of the review.
* @returns Scheduling cards for all ratings.
*/
scheduleRawCard(rawData, now = new Date()) {
const card = this.convertRawCard(rawData);
return this.schedule(card, now);
}
/**
* Converts and validates raw card data into a formal Card object.
* @param rawData The raw card data.
* @returns A validated Card object.
*/
convertRawCard(rawData) {
return cardValidator_1.CardValidator.validateAndConvert(rawData);
}
/**
* Batch converts and validates multiple raw cards.
* @param rawDataArray Array of raw card data.
* @returns An object containing arrays of valid cards and any errors encountered.
*/
convertRawCardBatch(rawDataArray) {
return cardValidator_1.CardValidator.validateAndConvertBatch(rawDataArray);
}
/**
* Calculates the probability of recalling a card at a given time.
* @param card The card to calculate retrievability for.
* @param now The date for which to calculate retrievability.
* @returns A number between 0 and 1, representing the probability of recall.
*/
getRetrievability(card, now = new Date()) {
if (card.state === types_1.State.New || !card.lastReview) {
return undefined;
}
const elapsedDays = (0, timeFuncs_1.calcElapsedDays)(card.lastReview, now);
return this.retrievability(elapsedDays, card.stability);
}
// ----------------------------- FSRS-4.5 Algorithm Core -----------------------------
buildSchedulingCards(card, now) {
const cards = {};
[types_1.Rating.Again, types_1.Rating.Hard, types_1.Rating.Good, types_1.Rating.Easy].forEach((rating) => {
const scheduledCard = this.calculateScheduledCard(card, rating, now);
const reviewLog = this.buildReviewLog(card, rating, now);
const key = this.getRatingKey(rating);
cards[key] = {
card: scheduledCard,
reviewLog,
};
});
return cards;
}
calculateScheduledCard(card, rating, now) {
const newCard = { ...card };
newCard.reps += 1;
newCard.lastReview = new Date(now);
if (card.state === types_1.State.New) {
// First review of a new card
newCard.elapsedDays = 0;
newCard.difficulty = this.initDifficulty(rating);
newCard.stability = this.initStability(rating);
}
else {
// Review of a card that has been seen before
const elapsedDays = card.lastReview ? (0, timeFuncs_1.calcElapsedDays)(card.lastReview, now) : 0;
newCard.elapsedDays = elapsedDays;
const R = this.retrievability(elapsedDays, card.stability);
newCard.difficulty = this.nextDifficulty(card.difficulty, rating);
newCard.stability = this.nextStability(newCard.difficulty, card.stability, R, rating);
}
if (rating === types_1.Rating.Again) {
newCard.lapses += 1;
newCard.state = types_1.State.Relearning;
// Interval for "Again" is fixed according to the algorithm's forgetting curve
newCard.scheduledDays = this.nextInterval(newCard.stability);
}
else {
newCard.state = card.state === types_1.State.New ? types_1.State.Learning : types_1.State.Review;
newCard.scheduledDays = this.nextInterval(newCard.stability);
}
newCard.due = this.addDays(now, newCard.scheduledDays);
return newCard;
}
buildReviewLog(card, rating, now) {
return {
rating,
state: card.state,
due: new Date(card.due),
stability: card.stability,
difficulty: card.difficulty,
elapsedDays: card.lastReview ? (0, timeFuncs_1.calcElapsedDays)(card.lastReview, now) : 0,
lastElapsedDays: card.elapsedDays,
scheduledDays: card.scheduledDays,
review: new Date(now),
};
}
// --- FSRS-4.5 Formulas ---
initStability(rating) {
return Math.max(this.parameters.w[rating - 1], 0.1);
}
initDifficulty(rating) {
const difficulty = this.parameters.w[4] - (rating - 3) * this.parameters.w[5];
return Math.min(Math.max(difficulty, 1), 10);
}
nextDifficulty(difficulty, rating) {
const nextD = difficulty - this.parameters.w[6] * (rating - 3);
// Correction: The mean reversion target should be the initial 'Good' difficulty (w[4]).
const reversionTarget = this.parameters.w[4];
const revertedD = this.parameters.w[7] * reversionTarget + (1 - this.parameters.w[7]) * nextD;
return Math.min(Math.max(revertedD, 1), 10);
}
nextStability(difficulty, stability, retrievability, rating) {
if (rating === types_1.Rating.Again) {
return (this.parameters.w[11] *
Math.pow(difficulty, -this.parameters.w[12]) *
(Math.pow(stability + 1, this.parameters.w[13]) - 1) *
Math.exp((1 - retrievability) * this.parameters.w[14]));
}
else {
const hardPenalty = rating === types_1.Rating.Hard ? this.parameters.w[15] : 1;
const easyBonus = rating === types_1.Rating.Easy ? this.parameters.w[16] : 1;
return (stability *
(1 +
Math.exp(this.parameters.w[8]) *
(11 - difficulty) *
Math.pow(stability, -this.parameters.w[9]) *
(Math.exp((1 - retrievability) * this.parameters.w[10]) - 1)) *
hardPenalty *
easyBonus);
}
}
nextInterval(stability) {
// This formula calculates the interval where the probability of recall
// is `requestRetention`. The `9` is a magic number from the FSRS-4.5 formula.
const interval = stability * 9 * (1 / this.parameters.requestRetention - 1);
return Math.min(Math.max(Math.round(interval), 1), this.parameters.maximumInterval);
}
retrievability(elapsedDays, stability) {
// The probability of recalling a card after `elapsedDays` with a given `stability`.
// The `9` is a magic number from the FSRS-4.5 formula.
return Math.pow(1 + elapsedDays / (9 * stability), -1);
}
// --- Utility Functions ---
addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
getRatingKey(rating) {
switch (rating) {
case types_1.Rating.Again:
return "again";
case types_1.Rating.Hard:
return "hard";
case types_1.Rating.Good:
return "good";
case types_1.Rating.Easy:
return "easy";
}
}
// --- Parameter Management ---
updateParameters(newParameters) {
this.parameters = { ...this.parameters, ...newParameters };
}
getParameters() {
return { ...this.parameters };
}
}
exports.FSRS = FSRS;
//# sourceMappingURL=fsrs.js.map