fsrs-algorithm
Version:
Free Spaced Repetition Scheduler (FSRS) algorithm implementation in TypeScript
175 lines • 6.22 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CardValidator = void 0;
const types_1 = require("../types");
// State mapping from string to enum
const STATE_MAP = {
NEW: types_1.State.New,
LEARNING: types_1.State.Learning,
REVIEW: types_1.State.Review,
RELEARNING: types_1.State.Relearning,
};
class CardValidator {
/**
* Validates and converts raw card data to a Card object
* @param rawData - The raw card data from database/API
* @returns A validated Card object
* @throws Error if validation fails
*/
static validateAndConvert(rawData) {
if (!rawData || typeof rawData !== "object") {
throw new Error("Invalid card data: must be an object");
}
// Validate and convert state
const state = this.validateState(rawData.state);
// Validate and convert dates
const due = this.validateDate(rawData.due, "due");
const lastReview = rawData.lastReview
? this.validateDate(rawData.lastReview, "lastReview")
: undefined;
// Validate and convert numbers
const stability = this.validateNumber(rawData.stability, "stability", 0.1, Infinity);
const difficulty = this.validateNumber(rawData.difficulty, "difficulty", 1, 10);
const elapsedDays = this.validateNumber(rawData.elapsedDays, "elapsedDays", 0, Infinity);
const scheduledDays = rawData.scheduledDays !== undefined
? this.validateNumber(rawData.scheduledDays, "scheduledDays", 0, Infinity)
: 0; // Default to 0 if not provided
const reps = this.validateInteger(rawData.reps, "reps", 0, Infinity);
const lapses = this.validateInteger(rawData.lapses, "lapses", 0, Infinity);
// Additional validation rules
if (state === types_1.State.New && reps > 0) {
throw new Error("New cards should have 0 reps");
}
if (state === types_1.State.New && lastReview) {
throw new Error("New cards should not have a lastReview date");
}
if (reps > 0 && !lastReview) {
throw new Error("Cards with reps > 0 must have a lastReview date");
}
if (lapses > reps) {
throw new Error("Lapses cannot be greater than reps");
}
// Construct the validated Card object
const card = {
due,
stability,
difficulty,
elapsedDays,
scheduledDays,
reps,
lapses,
state,
lastReview,
};
return card;
}
/**
* Validates and converts a state string to State enum
*/
static validateState(state) {
if (typeof state !== "string") {
throw new Error(`Invalid state: must be a string, got ${typeof state}`);
}
const upperState = state.toUpperCase();
const mappedState = STATE_MAP[upperState];
if (mappedState === undefined) {
const validStates = Object.keys(STATE_MAP).join(", ");
throw new Error(`Invalid state: "${state}". Must be one of: ${validStates}`);
}
return mappedState;
}
/**
* Validates and converts a date string/Date to Date object
*/
static validateDate(value, fieldName) {
if (!value) {
throw new Error(`Invalid ${fieldName}: value is required`);
}
let date;
if (value instanceof Date) {
date = value;
}
else if (typeof value === "string") {
date = new Date(value);
}
else {
throw new Error(`Invalid ${fieldName}: must be a Date or string, got ${typeof value}`);
}
if (isNaN(date.getTime())) {
throw new Error(`Invalid ${fieldName}: "${value}" is not a valid date`);
}
return date;
}
/**
* Validates and converts a number
*/
static validateNumber(value, fieldName, min = -Infinity, max = Infinity) {
let num;
if (typeof value === "number") {
num = value;
}
else if (typeof value === "string") {
num = parseFloat(value);
}
else {
throw new Error(`Invalid ${fieldName}: must be a number or string, got ${typeof value}`);
}
if (isNaN(num)) {
throw new Error(`Invalid ${fieldName}: "${value}" is not a valid number`);
}
if (num < min || num > max) {
throw new Error(`Invalid ${fieldName}: ${num} must be between ${min} and ${max}`);
}
return num;
}
/**
* Validates and converts an integer
*/
static validateInteger(value, fieldName, min = -Infinity, max = Infinity) {
const num = this.validateNumber(value, fieldName, min, max);
if (!Number.isInteger(num)) {
throw new Error(`Invalid ${fieldName}: ${num} must be an integer`);
}
return num;
}
/**
* Batch validate and convert multiple cards
*/
static validateAndConvertBatch(rawDataArray) {
const valid = [];
const errors = [];
rawDataArray.forEach((rawData, index) => {
try {
const card = this.validateAndConvert(rawData);
valid.push(card);
}
catch (error) {
errors.push({
index,
error: error instanceof Error ? error.message : "Unknown error",
data: rawData,
});
}
});
return { valid, errors };
}
/**
* Checks if a raw object might be valid card data (loose validation)
*/
static isCardDataShape(obj) {
if (obj === null || obj === undefined) {
return false;
}
const result = obj &&
typeof obj === "object" &&
"due" in obj &&
"stability" in obj &&
"difficulty" in obj &&
"state" in obj &&
"reps" in obj &&
"lapses" in obj;
return result;
}
}
exports.CardValidator = CardValidator;
//# sourceMappingURL=cardValidator.js.map