Skip to content

Commit

Permalink
Feat/fsrs v4 (#31)
Browse files Browse the repository at this point in the history
* Feat/FSRS_V4
* Test/FSRS_V4
* ts-fsrs v3.0.0
  • Loading branch information
ishiko732 authored Oct 3, 2023
1 parent 3881ec2 commit b28299c
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 59 deletions.
84 changes: 84 additions & 0 deletions __tests__/FSRSV4.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
fsrs,
Rating,
generatorParameters,
FSRS,
createEmptyCard,
} from "../src/fsrs";

describe("initial FSRS V4", () => {
const params = generatorParameters();
const f: FSRS = fsrs(params);
const Ratings = Object.keys(Rating)
.filter((key) => !isNaN(Number(key)))
.map((key) => Number(key) as Rating);
it("initial stability ", () => {
Ratings.forEach((grade) => {
const s = f.init_stability(grade);
expect(s).toEqual(params.w[grade - 1]);
});
});
it("again s0(1) ", () => {
expect(f.init_stability(Rating.Again)).toEqual(params.w[0]);
});
it("initial s0(4) ", () => {
expect(f.init_stability(Rating.Easy)).toEqual(params.w[3]);
});

it("initial difficulty ", () => {
Ratings.forEach((grade) => {
const s = f.init_difficulty(grade);
expect(s).toEqual(params.w[4] - (grade - 3) * params.w[5]);
});
});
it("good D0(3) ", () => {
expect(f.init_difficulty(Rating.Good)).toEqual(params.w[4]);
});

it("retrievability t=s ", () => {
expect(Number(f.current_retrievability(5, 5).toFixed(2))).toEqual(0.9);
});
});

// Ref: https://github.com/open-spaced-repetition/py-fsrs/blob/ecd68e453611eb808c7367c7a5312d7cadeedf5c/tests/test_fsrs.py#L1
describe("FSRS V4 AC by py-fsrs", () => {
const f: FSRS = fsrs({
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,
],
enable_fuzz: false,
});
it("ivl_history", () => {
let card = createEmptyCard();
let now = new Date(2022, 11, 29, 12, 30, 0, 0);
let scheduling_cards = f.repeat(card, now);
const ratings = [
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Again,
Rating.Again,
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Good,
];
const ivl_history: number[] = [];
for (const rating of ratings) {
card = scheduling_cards[rating].card;
const ivl = card.scheduled_days;
ivl_history.push(ivl);
now = card.due;
scheduling_cards = f.repeat(card, now);
}

expect(ivl_history).toEqual([
0, 5, 16, 43, 106, 236, 0, 0, 12, 25, 47, 85, 147,
]);
});
});
132 changes: 132 additions & 0 deletions __tests__/models.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {
envParams,
Rating,
RatingType,
State,
StateType,
generatorParameters,
createEmptyCard,
} from "../src/fsrs";

describe("State", () => {
it("use State.New", () => {
expect(State.New).toEqual(0);
expect(0).toEqual(State.New);
expect(State[State.New]).toEqual("New");
expect((0 as State).valueOf()).toEqual(0);
expect(State["New" as StateType]).toEqual(0);
});

it("use State.Learning", () => {
expect(State.Learning).toEqual(1);
expect(1).toEqual(State.Learning);
expect(State[State.Learning]).toEqual("Learning");
expect((1 as State).valueOf()).toEqual(1);
expect(State["Learning" as StateType]).toEqual(1);
});

it("use State.Review", () => {
expect(State.Review).toEqual(2);
expect(2).toEqual(State.Review);
expect(State[State.Review]).toEqual("Review");
expect((2 as State).valueOf()).toEqual(2);
expect(State["Review" as StateType]).toEqual(2);
});

it("use State.Relearning", () => {
expect(State.Relearning).toEqual(3);
expect(3).toEqual(State.Relearning);
expect(State[State.Relearning]).toEqual("Relearning");
expect((3 as State).valueOf()).toEqual(3);
expect(State["Relearning" as StateType]).toEqual(3);
});
});

describe("Rating", () => {
it("use Rating.Again", () => {
expect(Rating.Again).toEqual(1);
expect(1).toEqual(Rating.Again);
expect(Rating[Rating.Again]).toEqual("Again");
expect((1 as Rating).valueOf()).toEqual(1);
expect(Rating["Again" as RatingType]).toEqual(1);
});

it("use Rating.Hard", () => {
expect(Rating.Hard).toEqual(2);
expect(2).toEqual(Rating.Hard);
expect(Rating[Rating.Hard]).toEqual("Hard");
expect((2 as Rating).valueOf()).toEqual(2);
expect(Rating["Hard" as RatingType]).toEqual(2);
});

it("use Rating.Good", () => {
expect(Rating.Good).toEqual(3);
expect(3).toEqual(Rating.Good);
expect(Rating[Rating.Good]).toEqual("Good");
expect((3 as Rating).valueOf()).toEqual(3);
expect(Rating["Good" as RatingType]).toEqual(3);
});

it("use Rating.Easy", () => {
expect(Rating.Easy).toEqual(4);
expect(4).toEqual(Rating.Easy);
expect(Rating[Rating.Easy]).toEqual("Easy");
expect((4 as Rating).valueOf()).toEqual(4);
expect(Rating["Easy" as RatingType]).toEqual(4);
});
});

describe("default FSRSParameters", () => {
const env = envParams;
const params = generatorParameters();
const w_v4 = [
0.4, 0.6, 2.4, 5.8, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18, 0.05,
0.34, 1.26, 0.29, 2.61,
];
it("default_request_retention", () => {
expect([0.9, env.FSRS_REQUEST_RETENTION]).toContainEqual(
params.request_retention,
);
});
it("default_maximum_interval", () => {
expect([36500, env.FSRS_MAXIMUM_INTERVAL]).toContainEqual(
params.maximum_interval,
);
});
it("default_w ", () => {
expect([w_v4, env.FSRS_W]).toContainEqual(params.w);
if (env.FSRS_W) {
expect(env.FSRS_W.length).toEqual(w_v4.length);
}
});
it("default_enable_fuzz ", () => {
expect([false, env.FSRS_ENABLE_FUZZ]).toContainEqual(params.enable_fuzz);
});
});

describe("default Card", () => {
it("empty card", () => {
const now = new Date();
const card = createEmptyCard(now);
expect(card.due).toEqual(now);
expect(card.stability).toEqual(0);
expect(card.difficulty).toEqual(0);
expect(card.elapsed_days).toEqual(0);
expect(card.scheduled_days).toEqual(0);
expect(card.reps).toEqual(0);
expect(card.lapses).toEqual(0);
expect(card.state).toEqual(0);
});
it("empty card", () => {
const now = new Date("2023-10-3 00:00:00");
const card = createEmptyCard();
expect(card.due).not.toEqual(now);
expect(card.stability).toEqual(0);
expect(card.difficulty).toEqual(0);
expect(card.elapsed_days).toEqual(0);
expect(card.scheduled_days).toEqual(0);
expect(card.reps).toEqual(0);
expect(card.lapses).toEqual(0);
expect(card.state).toEqual(0);
});
});
4 changes: 1 addition & 3 deletions example/.env.local.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
FSRS_REQUEST_RETENTION=0.8
FSRS_MAXIMUM_INTERVAL=36500
FSRS_EASY_BOUND=1.3
FSRS_HARD_FACTOR=1.2
FSRS_W='[1, 1, 5, -0.5, -0.5, 0.2, 1.4, -0.12, 0.8, 2, -0.2, 0.2, 1]'
FSRS_W='[0.4, 0.6, 2.4, 5.8, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18, 0.05, 0.34, 1.26, 0.29, 2.61]'
FSRS_ENABLE_FUZZ=true
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": "2.2.0",
"version": "3.0.0",
"description": "ts-fsrs is a TypeScript package used to implement the Free Spaced Repetition Scheduler (FSRS) algorithm. It helps developers apply FSRS to their flashcard applications, thereby improving the user learning experience.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
19 changes: 6 additions & 13 deletions src/fsrs/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,32 @@ dotenv.config({ path: `./.env.development` });
export const envParams: EnvParams = {
FSRS_REQUEST_RETENTION: Number(process.env.FSRS_REQUEST_RETENTION),
FSRS_MAXIMUM_INTERVAL: Number(process.env.FSRS_MAXIMUM_INTERVAL),
FSRS_EASY_BOUND: Number(process.env.FSRS_EASY_BOUND),
FSRS_HARD_FACTOR: Number(process.env.FSRS_HARD_FACTOR),
FSRS_W: process.env.FSRS_W
? JSON.parse(process.env.FSRS_W as string)
: undefined,
FSRS_ENABLE_FUZZ: Boolean(process.env.FSRS_ENABLE_FUZZ),
};

export const default_request_retention = !isNaN(envParams.FSRS_REQUEST_RETENTION)
export const default_request_retention = !isNaN(
envParams.FSRS_REQUEST_RETENTION,
)
? envParams.FSRS_REQUEST_RETENTION
: 0.9;
export const default_maximum_interval = !isNaN(envParams.FSRS_MAXIMUM_INTERVAL)
? envParams.FSRS_MAXIMUM_INTERVAL
: 36500;
export const default_easy_bonus = !isNaN(envParams.FSRS_EASY_BOUND)
? envParams.FSRS_EASY_BOUND
: 1.3;
export const default_hard_factor = !isNaN(envParams.FSRS_HARD_FACTOR)
? envParams.FSRS_HARD_FACTOR
: 1.2;
export const default_w = envParams.FSRS_W || [
1, 1, 5, -0.5, -0.5, 0.2, 1.4, -0.12, 0.8, 2, -0.2, 0.2, 1,
0.4, 0.6, 2.4, 5.8, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18, 0.05,
0.34, 1.26, 0.29, 2.61,
];
export const default_enable_fuzz = envParams.FSRS_ENABLE_FUZZ || false;

export const FSRSVersion: string = "2.2.0";
export const FSRSVersion: string = "3.0.0";

export const generatorParameters = (props?: Partial<FSRSParameters>) => {
return {
request_retention: props?.request_retention || default_request_retention,
maximum_interval: props?.maximum_interval || default_maximum_interval,
easy_bonus: props?.easy_bonus || default_easy_bonus,
hard_factor: props?.hard_factor || default_hard_factor,
w: props?.w || default_w,
enable_fuzz: props?.enable_fuzz || default_enable_fuzz,
};
Expand Down
Loading

0 comments on commit b28299c

Please sign in to comment.