Skip to content

Commit

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

* cherry-pick Feat/grades

* 3.1.0-beta2
  • Loading branch information
ishiko732 authored Nov 4, 2023
1 parent 86b4cda commit 8154795
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 48 deletions.
41 changes: 19 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ cp .env.local.example .env.local
# Example

```typescript
import {createEmptyCard, formatDate, fsrs, generatorParameters, Rating} from 'ts-fsrs';
import {createEmptyCard, formatDate, fsrs, generatorParameters, Rating, Grades} from 'ts-fsrs';

const params = generatorParameters({ enable_fuzz: true });
const f = fsrs(params);
Expand All @@ -30,28 +30,25 @@ const now = new Date('2022-2-2 10:00:00');// new Date();
const scheduling_cards = f.repeat(card, now);

// console.log(scheduling_cards);
Object.keys(Rating)
.filter(key => !isNaN(Number(key)))
.map(key => Number(key) as Rating) // [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy]
.forEach(grade => {
const { log, card } = scheduling_cards[grade];
console.group(`${Rating[grade]}`);
console.table({
[`card_${Rating[grade]}`]: {
...card,
due: formatDate(card.due),
last_review: formatDate(card.last_review as Date),
},
});
console.table({
[`log_${Rating[grade]}`]: {
...log,
review: formatDate(log.review),
},
});
console.groupEnd();
console.log('----------------------------------------------------------------');
Grades.forEach(grade => { // [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy]
const { log, card } = scheduling_cards[grade];
console.group(`${Rating[grade]}`);
console.table({
[`card_${Rating[grade]}`]: {
...card,
due: formatDate(card.due),
last_review: formatDate(card.last_review as Date),
},
});
console.table({
[`log_${Rating[grade]}`]: {
...log,
review: formatDate(log.review),
},
});
console.groupEnd();
console.log('----------------------------------------------------------------');
});
```

> More examples refer to the [Example](https://github.com/ishiko732/ts-fsrs/blob/master/example/index.ts)
Expand Down
24 changes: 14 additions & 10 deletions __tests__/FSRSV4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ import {
FSRS,
createEmptyCard,
State,
Grade,
Grades
} 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) => {
Grades.forEach((grade) => {
const s = f.init_stability(grade);
expect(s).toEqual(params.w[grade - 1]);
});
Expand All @@ -27,7 +26,7 @@ describe("initial FSRS V4", () => {
});

it("initial difficulty ", () => {
Ratings.forEach((grade) => {
Grades.forEach((grade) => {
const s = f.init_difficulty(grade);
expect(s).toEqual(params.w[4] - (grade - 3) * params.w[5]);
});
Expand All @@ -50,12 +49,12 @@ describe("FSRS V4 AC by py-fsrs", () => {
],
enable_fuzz: false,
});
const grade = [Rating.Again, Rating.Hard, Rating.Good,Rating.Easy];
const grade: Grade[] = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy];
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 = [
const ratings: Grade[] = [
Rating.Good,
Rating.Good,
Rating.Good,
Expand All @@ -74,8 +73,8 @@ describe("FSRS V4 AC by py-fsrs", () => {
for (const rating of ratings) {
for (const check of grade) {
const rollbackCard = f.rollback(
scheduling_cards[check].card,
scheduling_cards[check].log,
scheduling_cards[check].card,
scheduling_cards[check].log,
);
expect(rollbackCard).toEqual(card);
}
Expand All @@ -95,7 +94,12 @@ describe("FSRS V4 AC by py-fsrs", () => {
const card = createEmptyCard();
const now = new Date(2022, 11, 29, 12, 30, 0, 0);
const scheduling_cards = f.repeat(card, now);
const grades = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy];
const grades: Grade[] = [
Rating.Again,
Rating.Hard,
Rating.Good,
Rating.Easy,
];

const stability: number[] = [];
const difficulty: number[] = [];
Expand Down
60 changes: 60 additions & 0 deletions __tests__/forget.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createEmptyCard, fsrs, FSRS, Rating } from "../src/fsrs";
import { Grade } from "../src/fsrs/models";

describe("FSRS forget", () => {
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("forget", () => {
const card = createEmptyCard();
const now = new Date(2022, 11, 29, 12, 30, 0, 0);
const forget_now = new Date(2023, 11, 30, 12, 30, 0, 0);
const scheduling_cards = f.repeat(card, now);
const grades: Grade[] = [
Rating.Again,
Rating.Hard,
Rating.Good,
Rating.Easy,
];
for (const grade of grades) {
const forgetCard = f.forget(
scheduling_cards[grade].card,
forget_now,
true,
);
expect(forgetCard.card).toEqual({
...card,
due: forget_now,
lapses: 0,
reps: 0,
last_review: scheduling_cards[grade].card.last_review,
});
expect(forgetCard.log.rating).toEqual(Rating.Manual);
expect(() => f.rollback(forgetCard.card, forgetCard.log)).toThrowError(
"Cannot rollback a manual rating",
);
}
for (const grade of grades) {
const forgetCard = f.forget(
scheduling_cards[grade].card,
forget_now,
false,
);
expect(forgetCard.card).toEqual({
...card,
due: forget_now,
lapses: scheduling_cards[grade].card.lapses,
reps: scheduling_cards[grade].card.reps,
last_review: scheduling_cards[grade].card.last_review,
});
expect(forgetCard.log.rating).toEqual(Rating.Manual);
expect(() => f.rollback(forgetCard.card, forgetCard.log)).toThrowError(
"Cannot rollback a manual rating",
);
}
});
});
5 changes: 5 additions & 0 deletions __tests__/help.tsts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Grades, Rating } from "../src/fsrs";

test("FSRS-Grades", () => {
expect(Grades).toStrictEqual([Rating.Again, Rating.Hard, Rating.Good, Rating.Easy]);
});
6 changes: 3 additions & 3 deletions __tests__/rollback.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createEmptyCard, fsrs, FSRS, Rating } from "../src/fsrs";
import {createEmptyCard, fsrs, FSRS, Grade, Rating} from "../src/fsrs";

describe("FSRS rollback", () => {
const f: FSRS = fsrs({
Expand All @@ -12,7 +12,7 @@ describe("FSRS rollback", () => {
const card = createEmptyCard();
const now = new Date(2022, 11, 29, 12, 30, 0, 0);
const scheduling_cards = f.repeat(card, now);
const grade = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy];
const grade:Grade[] = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy];
for (const rating of grade) {
const rollbackCard = f.rollback(
scheduling_cards[rating].card,
Expand All @@ -29,7 +29,7 @@ describe("FSRS rollback", () => {
card = scheduling_cards["4"].card;
now = card.due;
scheduling_cards = f.repeat(card, now);
const grade = [Rating.Again, Rating.Hard, Rating.Good,Rating.Easy];
const grade:Grade[] = [Rating.Again, Rating.Hard, Rating.Good,Rating.Easy];
for (const rating of grade) {
const rollbackCard = f.rollback(
scheduling_cards[rating].card,
Expand Down
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.1.0-beta1",
"version": "3.1.0-beta2",
"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
16 changes: 8 additions & 8 deletions src/fsrs/algorithm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pseudorandom from "seedrandom";
import { generatorParameters, SchedulingCard } from "./index";
import { FSRSParameters, Rating } from "./models";
import {FSRSParameters, Grade, Rating} from "./models";
import type { int } from "./type";

// Ref: https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm#fsrs-v4
Expand Down Expand Up @@ -77,7 +77,7 @@ export class FSRSAlgorithm {
* @param g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
* @return Stability (interval when R=90%)
*/
init_stability(g: number): number {
init_stability(g: Grade): number {
return Math.max(this.param.w[g - 1], 0.1);
}

Expand All @@ -86,10 +86,10 @@ export class FSRSAlgorithm {
* $$D_0(G) = w_4 - (G-3) \cdot w_5$$
* $$\min \{\max \{D_0(G),1\},10\}$$
* where the D_0(3)=w_4 when the first rating is good.
* @param {number} g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
* @param {Grade} g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
* @return {number} Difficulty D \in [1,10]
*/
init_difficulty(g: number): number {
init_difficulty(g: Grade): number {
return Math.min(
Math.max(this.param.w[4] - (g - 3) * this.param.w[5], 1),
10,
Expand Down Expand Up @@ -130,10 +130,10 @@ export class FSRSAlgorithm {
* $$next_d = D - w_6 \cdot (R - 2)$$
* $$D^\prime(D,R) = w_5 \cdot D_0(2) +(1 - w_5) \cdot next_d$$
* @param {number} d Difficulty D \in [1,10]
* @param {Rating} g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
* @param {Grade} g Grade (rating at Anki) [1.again,2.hard,3.good,4.easy]
* @return {number} next_D
*/
next_difficulty(d: number, g: number): number {
next_difficulty(d: number, g: Grade): number {
const next_d = d - this.param.w[6] * (g - 3);
return this.constrain_difficulty(
this.mean_reversion(this.param.w[4], next_d),
Expand Down Expand Up @@ -166,10 +166,10 @@ export class FSRSAlgorithm {
* @param {number} d Difficulty D \in [1,10]
* @param {number} s Stability (interval when R=90%)
* @param {number} r Retrievability (probability of recall)
* @param {Rating} g Grade (Rating[0.again,1.hard,2.good,3.easy])
* @param {Grade} g Grade (Rating[0.again,1.hard,2.good,3.easy])
* @return {number} S^\prime_r new stability after recall
*/
next_recall_stability(d: number, s: number, r: number, g: Rating): number {
next_recall_stability(d: number, s: number, r: number, g: Grade): number {
const hard_penalty = Rating.Hard === g ? this.param.w[15] : 1;
const easy_bound = Rating.Easy === g ? this.param.w[16] : 1;
return (
Expand Down
2 changes: 1 addition & 1 deletion src/fsrs/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const default_w = envParams.FSRS_W || [
];
export const default_enable_fuzz = envParams.FSRS_ENABLE_FUZZ || false;

export const FSRSVersion: string = "3.1.0-beta1";
export const FSRSVersion: string = "3.1.0-beta2";

export const generatorParameters = (props?: Partial<FSRSParameters>): FSRSParameters => {
return {
Expand Down
37 changes: 36 additions & 1 deletion src/fsrs/fsrs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FSRSParameters,
Rating,
RecordLog,
RecordLogItem,
ReviewLog,
ReviewLogInput,
State,
Expand Down Expand Up @@ -103,7 +104,9 @@ export class FSRS extends FSRSAlgorithm {
rollback = (card: CardInput, log: ReviewLogInput): Card => {
card = this.preProcessCard(card);
log = this.preProcessLog(log);

if (log.rating === Rating.Manual) {
throw new Error("Cannot rollback a manual rating");
}
let last_due, last_review, last_lapses;
switch (log.state) {
case State.New:
Expand Down Expand Up @@ -135,4 +138,36 @@ export class FSRS extends FSRSAlgorithm {
last_review: last_review,
};
};

forget = (
card: CardInput,
now: DateInput,
reset_count: boolean = false,
): RecordLogItem => {
card = this.preProcessCard(card);
now = this.preProcessDate(now);
const forget_log: ReviewLog = {
rating: Rating.Manual,
state: card.state,
due: card.due,
stability: card.stability,
difficulty: card.difficulty,
elapsed_days: card.elapsed_days,
scheduled_days: card.scheduled_days,
review: now,
};
const forget_card: Card = {
...card,
due: now,
stability: 0,
difficulty: 0,
elapsed_days: 0,
scheduled_days: 0,
reps: reset_count ? 0 : card.reps,
lapses: reset_count ? 0 : card.lapses,
state: State.New,
last_review: card.last_review,
};
return { card: forget_card, log: forget_log };
};
}
4 changes: 3 additions & 1 deletion src/fsrs/help.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { int, unit } from "./type";
import { Rating, State } from "./models";
import {Grade, Rating, State} from "./models";

declare global {
export interface Date {
Expand Down Expand Up @@ -143,3 +143,5 @@ export function fixRating(value: unknown): Rating {
}
throw new Error(`Invalid rating:[${value}]`);
}

export const Grades: Grade[] = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy];
2 changes: 2 additions & 0 deletions src/fsrs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export {
date_diff,
formatDate,
show_diff_message,
Grades
} from "./help";

export type { int, double } from "./type";
Expand All @@ -26,6 +27,7 @@ export type {
RecordLogItem,
StateType,
RatingType,
Grade,
CardInput,
DateInput
} from "./models";
Expand Down
Loading

0 comments on commit 8154795

Please sign in to comment.