UNPKG

fsrs-algorithm

Version:

Free Spaced Repetition Scheduler (FSRS) algorithm implementation in TypeScript

257 lines 10.6 kB
"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