Skip to content

Commit

Permalink
Feat/reschedule (#72)
Browse files Browse the repository at this point in the history
* Feat/reschedule

* Test/update test cases

* Feat/update fuzz

Ref:#72 (comment)

* Chore/update export

* Pref/elapsed_days

* Docs/reschedule example

* 3.5.0
  • Loading branch information
ishiko732 committed Mar 12, 2024
1 parent d337069 commit 5373a26
Show file tree
Hide file tree
Showing 9 changed files with 308 additions and 55 deletions.
18 changes: 11 additions & 7 deletions __tests__/FSRSV4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Grades, default_request_retention, default_maximum_interval, default_enable_fuzz, default_w,
} from "../src/fsrs";
import { FSRSAlgorithm } from "../src/fsrs/algorithm";
import { get_fuzz_range } from "../src/fsrs";

describe("initial FSRS V4", () => {
const params = generatorParameters();
Expand Down Expand Up @@ -55,26 +56,29 @@ describe("initial FSRS V4", () => {

describe("FSRS apply_fuzz", () => {
test("return original interval when fuzzing is disabled", () => {
const ivl = 3.0;
const ivl = 3.2;
const enable_fuzz = false;
const algorithm = new FSRS({ enable_fuzz: enable_fuzz });
expect(algorithm.apply_fuzz(ivl)).toBe(3);
expect(algorithm.apply_fuzz(ivl, 0)).toBe(3);
});

test("return original interval when ivl is less than 2.5", () => {
const ivl = 2.0;
const ivl = 2.3;
const enable_fuzz = true;
const algorithm = new FSRS({ enable_fuzz: enable_fuzz });
expect(algorithm.apply_fuzz(ivl)).toBe(2);
expect(algorithm.apply_fuzz(ivl, 0)).toBe(2);
});

test("return original interval when ivl is less than 2.5", () => {
const ivl = 2.5;
const enable_fuzz = true;
const algorithm = new FSRSAlgorithm({ enable_fuzz: enable_fuzz });
const min_ivl = Math.max(2, Math.round(ivl * 0.95 - 1));
const max_ivl = Math.round(ivl * 1.05 + 1);
const fuzzedInterval = algorithm.apply_fuzz(ivl);
const { min_ivl, max_ivl } = get_fuzz_range(
ivl,
0,
default_maximum_interval,
);
const fuzzedInterval = algorithm.apply_fuzz(ivl, 0);
expect(fuzzedInterval).toBeGreaterThanOrEqual(min_ivl);
expect(fuzzedInterval).toBeLessThanOrEqual(max_ivl);
});
Expand Down
153 changes: 153 additions & 0 deletions __tests__/reschedule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {
Card,
createEmptyCard,
date_scheduler,
default_maximum_interval,
default_request_retention,
fsrs,
FSRS,
State,
} from "../src/fsrs";
import { get_fuzz_range } from "../src/fsrs";

describe("FSRS reschedule", () => {
const DECAY: number = -0.5;
const FACTOR: number = Math.pow(0.9, 1 / DECAY) - 1;
const request_retentions = [default_request_retention, 0.95, 0.85, 0.8];

type CardType = Card & {
cid: number;
};

function cardHandler(card: Card) {
(card as CardType)["cid"] = 1;
return card as CardType;
}

const newCard = createEmptyCard(undefined, cardHandler);
const learningCard: CardType = {
cid: 1,
due: new Date(),
stability: 0.6,
difficulty: 5.87,
elapsed_days: 0,
scheduled_days: 0,
reps: 1,
lapses: 0,
state: State.Learning,
last_review: new Date("2024-03-08 05:00:00"),
};
const reviewCard: CardType = {
cid: 1,
due: new Date("2024-03-17 04:43:02"),
stability: 48.26139059062234,
difficulty: 5.67,
elapsed_days: 18,
scheduled_days: 51,
reps: 8,
lapses: 1,
state: State.Review,
last_review: new Date("2024-01-26 04:43:02"),
};
const relearningCard: CardType = {
cid: 1,
due: new Date("2024-02-15 08:43:05"),
stability: 0.27,
difficulty: 10,
elapsed_days: 2,
scheduled_days: 0,
reps: 42,
lapses: 8,
state: State.Relearning,
last_review: new Date("2024-02-15 08:38:05"),
};

function dateHandler(date: Date) {
return date.getTime();
}

const cards = [newCard, learningCard, reviewCard, relearningCard];
it("reschedule", () => {
for (const requestRetention of request_retentions) {
const f: FSRS = fsrs({ request_retention: requestRetention });
const intervalModifier =
(Math.pow(requestRetention, 1 / DECAY) - 1) / FACTOR;
const reschedule_cards = f.reschedule(cards);
if (reschedule_cards.length > 0) {
// next_ivl !== scheduled_days
expect(reschedule_cards.length).toBeGreaterThanOrEqual(1);
expect(reschedule_cards[0].cid).toBeGreaterThanOrEqual(1);

const { min_ivl, max_ivl } = get_fuzz_range(
reviewCard.stability * intervalModifier,
reviewCard.elapsed_days,
default_maximum_interval,
);
expect(reschedule_cards[0].scheduled_days).toBeGreaterThanOrEqual(
min_ivl,
);
expect(reschedule_cards[0].scheduled_days).toBeLessThanOrEqual(max_ivl);
expect(reschedule_cards[0].due).toEqual(
date_scheduler(
reviewCard.last_review!,
reschedule_cards[0].scheduled_days,
true,
),
);
}
}
});

it("reschedule[dateHandler]", () => {
for (const requestRetention of request_retentions) {
const f: FSRS = fsrs({ request_retention: requestRetention });
const intervalModifier =
(Math.pow(requestRetention, 1 / DECAY) - 1) / FACTOR;
const [rescheduleCard] = f.reschedule([reviewCard], {
dateHandler,
});
if (rescheduleCard) {
// next_ivl !== scheduled_days
expect(rescheduleCard.cid).toBeGreaterThanOrEqual(1);
const { min_ivl, max_ivl } = get_fuzz_range(
reviewCard.stability * intervalModifier,
reviewCard.elapsed_days,
default_maximum_interval,
);

expect(rescheduleCard.scheduled_days).toBeGreaterThanOrEqual(min_ivl);
expect(rescheduleCard.scheduled_days).toBeLessThanOrEqual(max_ivl);
expect(rescheduleCard.due as unknown as number).toEqual(
date_scheduler(
reviewCard.last_review!,
rescheduleCard.scheduled_days,
true,
).getTime(),
);
expect(typeof rescheduleCard.due).toEqual("number");
}
}
});

it("reschedule[next_ivl === scheduled_days]", () => {
const f: FSRS = fsrs();
const reschedule_cards = f.reschedule(
[
{
cid: 1,
due: new Date("2024-03-13 04:43:02"),
stability: 48.26139059062234,
difficulty: 5.67,
elapsed_days: 18,
scheduled_days: 48,
reps: 8,
lapses: 1,
state: State.Review,
last_review: new Date("2024-01-26 04:43:02"),
},
],
{ enable_fuzz: false },
);
expect(reschedule_cards.length).toEqual(0);
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-fsrs",
"version": "3.4.1",
"version": "3.5.0",
"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",
Expand Down
33 changes: 22 additions & 11 deletions src/fsrs/algorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { generatorParameters } from "./default";
import {SchedulingCard} from './scheduler'
import {FSRSParameters, Grade, Rating} from "./models";
import type { int } from "./type";
import { get_fuzz_range } from "./help";

// Ref: https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm#fsrs-v4
export class FSRSAlgorithm {
Expand Down Expand Up @@ -102,30 +103,40 @@ export class FSRSAlgorithm {
/**
* If fuzzing is disabled or ivl is less than 2.5, it returns the original interval.
* @param {number} ivl - The interval to be fuzzed.
* @param {number} elapsed_days t days since the last review
* @param {number} enable_fuzz - This adds a small random delay to the new interval time to prevent cards from sticking together and always being reviewed on the same day.
* @return {number} - The fuzzed interval.
**/
apply_fuzz(ivl: number): number {
if (!this.param.enable_fuzz || ivl < 2.5) return ivl;
apply_fuzz(ivl: number, elapsed_days: number, enable_fuzz?: boolean): int {
if (!enable_fuzz || ivl < 2.5) return Math.round(ivl) as int;
const generator = pseudorandom(this.seed);
const fuzz_factor = generator();
ivl = Math.round(ivl);
const min_ivl = Math.max(2, Math.round(ivl * 0.95 - 1));
const max_ivl = Math.round(ivl * 1.05 + 1);
return Math.floor(fuzz_factor * (max_ivl - min_ivl + 1) + min_ivl);
const { min_ivl, max_ivl } = get_fuzz_range(
ivl,
elapsed_days,
this.param.maximum_interval,
);
return Math.floor(fuzz_factor * (max_ivl - min_ivl + 1) + min_ivl) as int;
}

/**
* Ref:
* constructor(param: Partial<FSRSParameters>)
* this.intervalModifier = 9 * (1 / this.param.request_retention - 1);
* @param {number} s - Stability (interval when R=90%)
* @param {number} elapsed_days t days since the last review
* @param {number} enable_fuzz - This adds a small random delay to the new interval time to prevent cards from sticking together and always being reviewed on the same day.
*/
next_interval(s: number): int {
const newInterval = this.apply_fuzz(s * this.intervalModifier);
return Math.min(
Math.max(Math.round(newInterval), 1),
this.param.maximum_interval,
next_interval(
s: number,
elapsed_days: number,
enable_fuzz: boolean = this.param.enable_fuzz,
): int {
const newInterval = Math.max(
1,
Math.round(s * this.intervalModifier),
) as int;
return this.apply_fuzz(newInterval, elapsed_days, enable_fuzz);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/fsrs/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const default_w = [
];
export const default_enable_fuzz = false;

export const FSRSVersion: string = "3.4.1";
export const FSRSVersion: string = "3.5.0";

export const generatorParameters = (
props?: Partial<FSRSParameters>,
Expand Down
Loading

0 comments on commit 5373a26

Please sign in to comment.