diff --git a/__tests__/FSRSV4.test.ts b/__tests__/FSRSV4.test.ts index e38b86b..92d3e69 100644 --- a/__tests__/FSRSV4.test.ts +++ b/__tests__/FSRSV4.test.ts @@ -4,6 +4,7 @@ import { generatorParameters, FSRS, createEmptyCard, + State, } from "../src/fsrs"; describe("initial FSRS V4", () => { @@ -49,6 +50,7 @@ describe("FSRS V4 AC by py-fsrs", () => { ], enable_fuzz: false, }); + const 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); @@ -70,6 +72,13 @@ describe("FSRS V4 AC by py-fsrs", () => { ]; const ivl_history: number[] = []; for (const rating of ratings) { + for (const check of grade) { + const rollbackCard = f.rollback( + scheduling_cards[check].card, + scheduling_cards[check].log, + ); + expect(rollbackCard).toEqual(card); + } card = scheduling_cards[rating].card; const ivl = card.scheduled_days; ivl_history.push(ivl); @@ -81,4 +90,41 @@ describe("FSRS V4 AC by py-fsrs", () => { 0, 5, 16, 43, 106, 236, 0, 0, 12, 25, 47, 85, 147, ]); }); + + it("first repeat", () => { + 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 stability: number[] = []; + const difficulty: number[] = []; + const elapsed_days: number[] = []; + const scheduled_days: number[] = []; + const reps: number[] = []; + const lapses: number[] = []; + const states: State[] = []; + for (const rating of grades) { + const first_card = scheduling_cards[rating].card; + stability.push(first_card.stability); + difficulty.push(first_card.difficulty); + reps.push(first_card.reps); + lapses.push(first_card.lapses); + elapsed_days.push(first_card.elapsed_days); + scheduled_days.push(first_card.scheduled_days); + states.push(first_card.state); + } + expect(stability).toEqual([1.14, 1.01, 5.44, 14.67]); + expect(difficulty).toEqual([8.4348, 6.8686, 5.3024, 3.7361999999999993]); + expect(reps).toEqual([1, 1, 1, 1]); + expect(lapses).toEqual([1, 0, 0, 0]); + expect(elapsed_days).toEqual([0, 0, 0, 0]); + expect(scheduled_days).toEqual([0, 0, 0, 15]); + expect(states).toEqual([ + State.Learning, + State.Learning, + State.Learning, + State.Review, + ]); + }); }); diff --git a/__tests__/rollback.test.ts b/__tests__/rollback.test.ts new file mode 100644 index 0000000..a1bdaf6 --- /dev/null +++ b/__tests__/rollback.test.ts @@ -0,0 +1,41 @@ +import { createEmptyCard, fsrs, FSRS, Rating } from "../src/fsrs"; + +describe("FSRS rollback", () => { + 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("first 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]; + for (const rating of grade) { + const rollbackCard = f.rollback( + scheduling_cards[rating].card, + scheduling_cards[rating].log, + ); + expect(rollbackCard).toEqual(card); + } + }); + + it("rollback 2", () => { + let card = createEmptyCard(); + let now = new Date(2022, 11, 29, 12, 30, 0, 0); + let scheduling_cards = f.repeat(card, now); + card = scheduling_cards["4"].card; + now = card.due; + scheduling_cards = f.repeat(card, now); + const grade = [Rating.Again, Rating.Hard, Rating.Good,Rating.Easy]; + for (const rating of grade) { + const rollbackCard = f.rollback( + scheduling_cards[rating].card, + scheduling_cards[rating].log, + ); + expect(rollbackCard).toEqual(card); + } + }); +}); diff --git a/package.json b/package.json index b89b22c..015e23e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-fsrs", - "version": "3.1.0-beta0", + "version": "3.1.0-beta1", "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", diff --git a/src/fsrs/default.ts b/src/fsrs/default.ts index 4b2f1fc..279308b 100644 --- a/src/fsrs/default.ts +++ b/src/fsrs/default.ts @@ -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-beta0"; +export const FSRSVersion: string = "3.1.0-beta1"; export const generatorParameters = (props?: Partial): FSRSParameters => { return { @@ -51,6 +51,7 @@ export const createEmptyCard = (now?: Date): Card => { reps: 0, lapses: 0, state: State.New, + last_review: undefined, }; }; diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index 46fbb23..7b6b5eb 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -1,6 +1,16 @@ -import { SchedulingCard } from "./index"; -import { fixDate, fixState } from "./help"; -import { FSRSParameters, Card, State, CardInput, DateInput, RecordLog } from "./models"; +import { SchedulingCard } from "./scheduler"; +import { fixDate, fixRating, fixState } from "./help"; +import { + Card, + CardInput, + DateInput, + FSRSParameters, + Rating, + RecordLog, + ReviewLog, + ReviewLogInput, + State, +} from "./models"; import type { int } from "./type"; import { FSRSAlgorithm } from "./algorithm"; @@ -9,27 +19,33 @@ export class FSRS extends FSRSAlgorithm { super(param); } - preProcess(_card: CardInput, _now: DateInput) { - const card: Card = { + private preProcessCard(_card: CardInput): Card { + return { ..._card, state: fixState(_card.state), due: fixDate(_card.due), last_review: _card.last_review ? fixDate(_card.last_review) : undefined, }; - const now = fixDate(_now); - return { card, now }; + } + + private preProcessDate(_date: DateInput): Date { + return fixDate(_date); + } + + private preProcessLog(_log: ReviewLogInput): ReviewLog { + return { + ..._log, + rating: fixRating(_log.rating), + state: fixState(_log.state), + review: fixDate(_log.review), + }; } repeat = (card: CardInput, now: DateInput): RecordLog => { - const process = this.preProcess(card, now); - card = process.card; - now = process.now; - card.elapsed_days = - card.state === State.New ? 0 : now.diff(card.last_review as Date, "days"); //相距时间 - card.last_review = now; // 上次复习时间 - card.reps += 1; - const s = new SchedulingCard(card).update_state(card.state); - this.seed = String(card.last_review.getTime()) + String(card.elapsed_days); + card = this.preProcessCard(card); + now = this.preProcessDate(now); + const s = new SchedulingCard(card, now).update_state(card.state); + this.seed = String(now.getTime()) + String(card.reps); let easy_interval, good_interval, hard_interval; switch (card.state) { case State.New: @@ -73,9 +89,8 @@ export class FSRS extends FSRSAlgorithm { }; get_retrievability = (card: Card, now: Date): undefined | string => { - const process = this.preProcess(card, now); - card = process.card; - now = process.now; + card = this.preProcessCard(card); + now = this.preProcessDate(now); if (card.state !== State.Review) { return undefined; } @@ -84,4 +99,40 @@ export class FSRS extends FSRSAlgorithm { (this.current_retrievability(t, card.stability) * 100).toFixed(2) + "%" ); }; + + rollback = (card: CardInput, log: ReviewLogInput): Card => { + card = this.preProcessCard(card); + log = this.preProcessLog(log); + + let last_due, last_review, last_lapses; + switch (log.state) { + case State.New: + last_due = log.due; + last_review = undefined; + last_lapses = 0; + break; + case State.Learning: + case State.Relearning: + case State.Review: + last_due = log.review; + last_review = log.due; + last_lapses = + card.lapses - + (log.rating === Rating.Again && log.state === State.Review ? 1 : 0); + break; + } + + return { + ...card, + due: last_due, + stability: log.stability, + difficulty: log.difficulty, + elapsed_days: log.elapsed_days, + scheduled_days: log.scheduled_days, + reps: Math.max(0, card.reps - 1), + lapses: Math.max(0, last_lapses), + state: log.state, + last_review: last_review, + }; + }; } diff --git a/src/fsrs/models.ts b/src/fsrs/models.ts index c0516e2..7d9f6fd 100644 --- a/src/fsrs/models.ts +++ b/src/fsrs/models.ts @@ -19,6 +19,9 @@ export enum Rating { export interface ReviewLog { rating: Rating; state: State; + due:Date; + stability: number; + difficulty: number; elapsed_days: number; scheduled_days: number; review: Date; @@ -44,6 +47,10 @@ export interface Card { export type CardInput = Card & { state: StateType | State }; export type DateInput = Date | number | string; +export type ReviewLogInput = ReviewLog & { + rating: RatingType | Rating; + state: StateType | State; +}; export interface FSRSParameters { request_retention: number; diff --git a/src/fsrs/scheduler.ts b/src/fsrs/scheduler.ts index 493531a..8bd50be 100644 --- a/src/fsrs/scheduler.ts +++ b/src/fsrs/scheduler.ts @@ -6,6 +6,8 @@ export class SchedulingCard { hard: Card; good: Card; easy: Card; + last_review: Date; + elapsed_days: number; private copy(card: Card): Card { return { @@ -13,7 +15,13 @@ export class SchedulingCard { }; } - constructor(card: Card) { + constructor(card: Card, now: Date) { + this.last_review = card.last_review || card.due; + this.elapsed_days = card.elapsed_days; + card.elapsed_days = + card.state === State.New ? 0 : now.diff(card.last_review as Date, "days"); //相距时间 + card.last_review = now; // 上次复习时间 + card.reps += 1; this.again = this.copy(card); this.hard = this.copy(card); this.good = this.copy(card); @@ -69,8 +77,11 @@ export class SchedulingCard { log: { rating: Rating.Again, state: card.state, - elapsed_days: this.again.scheduled_days, - scheduled_days: card.elapsed_days, + due: this.last_review, + stability: card.stability, + difficulty: card.difficulty, + elapsed_days: this.elapsed_days, + scheduled_days: card.scheduled_days, review: now, }, }, @@ -79,8 +90,11 @@ export class SchedulingCard { log: { rating: Rating.Hard, state: card.state, - elapsed_days: this.hard.scheduled_days, - scheduled_days: card.elapsed_days, + due: this.last_review, + stability: card.stability, + difficulty: card.difficulty, + elapsed_days: this.elapsed_days, + scheduled_days: card.scheduled_days, review: now, }, }, @@ -89,8 +103,11 @@ export class SchedulingCard { log: { rating: Rating.Good, state: card.state, - elapsed_days: this.good.scheduled_days, - scheduled_days: card.elapsed_days, + due: this.last_review, + stability: card.stability, + difficulty: card.difficulty, + elapsed_days: this.elapsed_days, + scheduled_days: card.scheduled_days, review: now, }, }, @@ -99,8 +116,11 @@ export class SchedulingCard { log: { rating: Rating.Easy, state: card.state, - elapsed_days: this.easy.scheduled_days, - scheduled_days: card.elapsed_days, + due: this.last_review, + stability: card.stability, + difficulty: card.difficulty, + elapsed_days: this.elapsed_days, + scheduled_days: card.scheduled_days, review: now, }, },