diff --git a/__tests__/algorithm.test.ts b/__tests__/algorithm.test.ts index 90bd1d9..00f0ca8 100644 --- a/__tests__/algorithm.test.ts +++ b/__tests__/algorithm.test.ts @@ -1,6 +1,9 @@ import { DECAY, + default_enable_fuzz, default_maximum_interval, + default_request_retention, + default_w, FACTOR, fsrs, FSRS, @@ -296,3 +299,60 @@ describe("FSRS apply_fuzz", () => { expect(fuzzedInterval).toBeLessThanOrEqual(max_ivl); }); }); + +describe("change Params", () => { + test("change FSRSParameters", () => { + const f = fsrs(); + // I(r,s),r=0.9 then I(r,s)=s + expect(f.interval_modifier).toEqual(1); + expect(f.parameters).toEqual(generatorParameters()); + + const request_retention = 0.8; + const update_w = [ + 1.14, 1.01, 5.44, 14.67, 5.3024, 1.5662, 1.2503, 0.0028, 1.5489, 0.1763, + 0.9953, 2.7473, 0.0179, 0.3105, 0.3976, 0.0, 2.0902, + ]; + f.parameters = generatorParameters({ + request_retention: request_retention, + w: update_w, + enable_fuzz: true, + }); + expect(f.parameters.request_retention).toEqual(request_retention); + expect(f.parameters.w).toEqual(update_w); + expect(f.parameters.enable_fuzz).toEqual(true); + expect(f.interval_modifier).toEqual( + f.calculate_interval_modifier(request_retention), + ); + + f.parameters.request_retention = default_request_retention; + expect(f.interval_modifier).toEqual( + f.calculate_interval_modifier(default_request_retention), + ); + + f.parameters.w = default_w; + expect(f.parameters.w).toEqual(default_w); + + f.parameters.maximum_interval = 365; + expect(f.parameters.maximum_interval).toEqual(365); + + f.parameters.enable_fuzz = default_enable_fuzz; + expect(f.parameters.enable_fuzz).toEqual(default_enable_fuzz); + + f.parameters = {} // check default values + expect(f.parameters).toEqual(generatorParameters()); + + }); + + test("calculate_interval_modifier", () => { + const f = new FSRSAlgorithm(generatorParameters()); + expect(f.interval_modifier).toEqual( + f.calculate_interval_modifier(default_request_retention), + ); + expect(() => { + f.parameters.request_retention = 1.2; + }).toThrow("Requested retention rate should be in the range (0,1]"); + expect(() => { + f.parameters.request_retention = -0.2; + }).toThrow("Requested retention rate should be in the range (0,1]"); + }); +}); diff --git a/package.json b/package.json index 7ff0c0b..6964868 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-fsrs", - "version": "3.5.3", + "version": "3.5.4", "description": "ts-fsrs is a ES modules package based on TypeScript, used to implement the Free Spaced Repetition Scheduler (FSRS) algorithm. It helps developers apply FSRS to their flashcard applications, there by improving the user learning experience.", "main": "dist/index.cjs", "module": "dist/index.mjs", diff --git a/src/fsrs/algorithm.ts b/src/fsrs/algorithm.ts index 0380c6d..dacb1c8 100644 --- a/src/fsrs/algorithm.ts +++ b/src/fsrs/algorithm.ts @@ -11,16 +11,69 @@ export const DECAY: number = -0.5; export const FACTOR: number = 19 / 81; export class FSRSAlgorithm { - protected param: FSRSParameters; - private readonly intervalModifier: number; + protected param!: FSRSParameters; + protected intervalModifier!: number; protected seed?: string; - constructor(param: Partial) { - this.param = generatorParameters(param); - // Ref: https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm#fsrs-45 - // The formula used is : I(r,s)= (r^{\frac{1}{DECAY}-1}) \times \frac{s}{FACTOR} - this.intervalModifier = - (Math.pow(this.param.request_retention, 1 / DECAY) - 1) / FACTOR; + constructor(params: Partial) { + this.param = new Proxy( + generatorParameters(params), + this.params_handler_proxy(), + ); + this.intervalModifier = this.calculate_interval_modifier( + this.param.request_retention, + ); + } + + get interval_modifier(): number { + return this.intervalModifier; + } + + /** + * Ref: https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm#fsrs-45 + * The formula used is: I(r,s) = (r^(1/DECAY) - 1) * s / FACTOR + * @param request_retention 0 1) { + throw new Error("Requested retention rate should be in the range (0,1]"); + } + return +((Math.pow(request_retention, 1 / DECAY) - 1) / FACTOR).toFixed(8); + } + + get parameters(): FSRSParameters { + return this.param; + } + + set parameters(params: Partial) { + this.update_parameters(params); + } + + private params_handler_proxy(): ProxyHandler { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const _this: FSRSAlgorithm = this; + return { + set: function (target, prop, value) { + if (prop === "request_retention" && Number.isFinite(value)) { + _this.intervalModifier = _this.calculate_interval_modifier( + Number(value), + ); + } + // @ts-ignore + target[prop] = value; + return true; + }, + }; + } + + private update_parameters(params: Partial): void { + const _params = generatorParameters(params); + for (const key in _params) { + if (key in this.param) { + const paramKey = key as keyof FSRSParameters; + this.param[paramKey] = _params[paramKey] as never; + } + } } init_ds(s: SchedulingCard): void { diff --git a/src/fsrs/default.ts b/src/fsrs/default.ts index 10c45e4..35bfe54 100644 --- a/src/fsrs/default.ts +++ b/src/fsrs/default.ts @@ -9,7 +9,7 @@ export const default_w = [ ]; export const default_enable_fuzz = false; -export const FSRSVersion: string = "3.5.3"; +export const FSRSVersion: string = "3.5.4"; export const generatorParameters = ( props?: Partial,