From 606b940304d28b297c0428199cc594c0db348f9e Mon Sep 17 00:00:00 2001 From: ishiko Date: Sun, 22 Sep 2024 14:23:48 +0800 Subject: [PATCH 01/24] update typo --- src/fsrs/algorithm.ts | 9 ++------- src/fsrs/fsrs.ts | 6 ++---- src/fsrs/models.ts | 28 +++++++++++++++++++++++++--- src/fsrs/types.ts | 7 +++++++ 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/fsrs/algorithm.ts b/src/fsrs/algorithm.ts index a07c6127..04c9b79c 100644 --- a/src/fsrs/algorithm.ts +++ b/src/fsrs/algorithm.ts @@ -150,18 +150,13 @@ export class FSRSAlgorithm { * @see The formula used is : {@link FSRSAlgorithm.calculate_interval_modifier} * @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, - elapsed_days: number, - enable_fuzz: boolean = this.param.enable_fuzz - ): int { + next_interval(s: number, elapsed_days: number): int { const newInterval = Math.min( Math.max(1, Math.round(s * this.intervalModifier)), this.param.maximum_interval ) as int - return this.apply_fuzz(newInterval, elapsed_days, enable_fuzz) + return this.apply_fuzz(newInterval, elapsed_days, this.param.enable_fuzz) } /** diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index 00c55946..b3ad7771 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -8,12 +8,11 @@ import { Rating, RecordLog, RecordLogItem, - RescheduleOptions, ReviewLog, ReviewLogInput, State, } from './models' -import type { int, IPreview } from './types' +import type { int, IPreview, RescheduleOptions } from './types' import { FSRSAlgorithm } from './algorithm' import { TypeConvert } from './convert' import BasicScheduler from './impl/basic_scheduler' @@ -425,8 +424,7 @@ export class FSRS extends FSRSAlgorithm { const scheduled_days = Math.floor(card.scheduled_days) as int const next_ivl = this.next_interval( +card.stability.toFixed(2), - Math.round(card.elapsed_days), - options.enable_fuzz ?? true + Math.round(card.elapsed_days) ) if (next_ivl === scheduled_days || next_ivl === 0) continue diff --git a/src/fsrs/models.ts b/src/fsrs/models.ts index a4537749..24199d2e 100644 --- a/src/fsrs/models.ts +++ b/src/fsrs/models.ts @@ -77,7 +77,29 @@ export interface FSRSParameters { enable_short_term: boolean } -export type RescheduleOptions = { - enable_fuzz?: boolean - dateHandler?: (date: Date) => DateInput +export interface FSRSReview { + /** + * 0-4: Manual, Again, Hard, Good, Easy + * = revlog.rating + */ + rating: Rating + /** + * The number of days that passed + * = revlog.elapsed_days + * = round(revlog[-1].review - revlog[-2].review) + */ + delta_t: number +} + +export interface FSRSHistory { + /** + * 0-4: Manual, Again, Hard, Good, Easy + * = revlog.rating + */ + rating: Rating + /** + * The number of days that passed + * = revlog.review + */ + reviewed_at: DateInput } diff --git a/src/fsrs/types.ts b/src/fsrs/types.ts index 8f3ab179..85cf28c5 100644 --- a/src/fsrs/types.ts +++ b/src/fsrs/types.ts @@ -1,4 +1,5 @@ import type { + DateInput, Grade, RecordLog, RecordLogItem, @@ -16,3 +17,9 @@ export interface IScheduler { preview(): IPreview review(state: Grade): RecordLogItem } + + +export type RescheduleOptions = { + enable_fuzz?: boolean + dateHandler?: (date: Date) => DateInput +} From 7b03cf3916a248db6a03cf8a63785d5f294ad941 Mon Sep 17 00:00:00 2001 From: ishiko Date: Sun, 22 Sep 2024 15:29:06 +0800 Subject: [PATCH 02/24] update enable_fuzz option --- __tests__/algorithm.test.ts | 9 +++++---- src/fsrs/impl/basic_scheduler.ts | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/__tests__/algorithm.test.ts b/__tests__/algorithm.test.ts index 1e017bbd..f323c9f9 100644 --- a/__tests__/algorithm.test.ts +++ b/__tests__/algorithm.test.ts @@ -274,7 +274,7 @@ describe('next_interval', () => { (_, i) => (i + 1) / 10 ) const intervals: number[] = desired_retentions.map((r) => - fsrs({ request_retention: r }).next_interval(1.0, 0, false) + fsrs({ request_retention: r }).next_interval(1.0, 0) ) expect(intervals).toEqual([422, 102, 43, 22, 13, 8, 4, 2, 1, 1]) }) @@ -284,14 +284,15 @@ describe('next_interval', () => { const params = generatorParameters({ maximum_interval: 365 }) const intervalModifier = (Math.pow(params.request_retention, 1 / DECAY) - 1) / FACTOR - const f: FSRS = fsrs(params) + let f: FSRS = fsrs(params) const s = 737.47 - const next_ivl = f.next_interval(s, 0, false) + const next_ivl = f.next_interval(s, 0) expect(next_ivl).toEqual(params.maximum_interval) const t_fuzz = 98 - const next_ivl_fuzz = f.next_interval(s, t_fuzz, true) + f = fsrs({ ...params, enable_fuzz: true }) + const next_ivl_fuzz = fsrs(params).next_interval(s, t_fuzz) const { min_ivl, max_ivl } = get_fuzz_range( Math.round(s * intervalModifier), t_fuzz, diff --git a/src/fsrs/impl/basic_scheduler.ts b/src/fsrs/impl/basic_scheduler.ts index 5d3447e7..e3583c60 100644 --- a/src/fsrs/impl/basic_scheduler.ts +++ b/src/fsrs/impl/basic_scheduler.ts @@ -38,8 +38,7 @@ export default class BasicScheduler extends AbstractScheduler { case Rating.Easy: { const easy_interval = this.algorithm.next_interval( next.stability, - this.current.elapsed_days, - this.algorithm.parameters.enable_fuzz + this.current.elapsed_days ) next.scheduled_days = easy_interval next.due = this.review_time.scheduler(easy_interval as int, true) From 4dace1055faa849c591faecda90c63093414a26c Mon Sep 17 00:00:00 2001 From: ishiko Date: Sun, 22 Sep 2024 15:29:54 +0800 Subject: [PATCH 03/24] Refactor/reschedule --- src/fsrs/fsrs.ts | 99 +++++++++++++++++++++++++++++++++------------- src/fsrs/models.ts | 28 +++++++------ src/fsrs/types.ts | 12 +++--- 3 files changed, 94 insertions(+), 45 deletions(-) diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index b3ad7771..fcbbacfb 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -3,6 +3,7 @@ import { Card, CardInput, DateInput, + FSRSHistory, FSRSParameters, Grade, Rating, @@ -17,6 +18,7 @@ import { FSRSAlgorithm } from './algorithm' import { TypeConvert } from './convert' import BasicScheduler from './impl/basic_scheduler' import LongTermScheduler from './impl/long_term_scheduler' +import { createEmptyCard } from './default' export class FSRS extends FSRSAlgorithm { private Scheduler @@ -205,11 +207,17 @@ export class FSRS extends FSRSAlgorithm { card: CardInput | Card, now?: DateInput, format: T = true as T - ): (T extends true ? string : number) { + ): T extends true ? string : number { const processedCard = TypeConvert.card(card) now = now ? TypeConvert.time(now) : new Date() - const t = processedCard.state !== State.New ? Math.max(now.diff(processedCard.last_review as Date, 'days'), 0) : 0 - const r = processedCard.state !== State.New ? this.forgetting_curve(t, +processedCard.stability.toFixed(8)) : 0 + const t = + processedCard.state !== State.New + ? Math.max(now.diff(processedCard.last_review as Date, 'days'), 0) + : 0 + const r = + processedCard.state !== State.New + ? this.forgetting_curve(t, +processedCard.stability.toFixed(8)) + : 0 return (format ? `${(r * 100).toFixed(2)}%` : r) as T extends true ? string : number @@ -410,35 +418,70 @@ export class FSRS extends FSRSAlgorithm { * ``` * */ - reschedule( - cards: Array, - options: RescheduleOptions = {} + reschedule( + reviews: FSRSHistory[] = [], + options: Partial> = {} ): Array { - if (!Array.isArray(cards)) { - throw new Error('cards must be an array') + const { + recordLogHandler, + reviewsOrderBy, + skipManual: skip = true, + } = options + if (reviewsOrderBy && typeof reviewsOrderBy === 'function') { + reviews.sort(reviewsOrderBy) } - const processedCard: T[] = [] - for (const card of cards) { - if (TypeConvert.state(card.state) !== State.Review || !card.last_review) - continue - const scheduled_days = Math.floor(card.scheduled_days) as int - const next_ivl = this.next_interval( - +card.stability.toFixed(2), - Math.round(card.elapsed_days) - ) - if (next_ivl === scheduled_days || next_ivl === 0) continue - - const processCard: T = { ...card } - processCard.scheduled_days = next_ivl - const new_due = date_scheduler(processCard.last_review!, next_ivl, true) - if (options.dateHandler && typeof options.dateHandler === 'function') { - processCard.due = options.dateHandler(new_due) - } else { - processCard.due = new_due + if (skip) { + reviews = reviews.filter((review) => review.rating !== Rating.Manual) + } + const datum: T[] = [] + let card: Card | undefined = undefined + for (const [index, review] of reviews.entries()) { + card = (card || createEmptyCard(review.review)) + if (!skip && review.rating === Rating.Manual) { + if (!review.state) { + throw new Error('reschedule: state is required for manual rating') + } + if (review.state === State.New) { + card = createEmptyCard(review.review) + } else { + card = { + ...card, + state: review.state, + due: review.due, + last_review: review.review, + stability: review.stability || card.stability, + difficulty: review.difficulty || card.difficulty, + elapsed_days: review.elapsed_days || card.elapsed_days, + scheduled_days: review.scheduled_days || card.scheduled_days, + reps: index, + } + const log = { + rating: Rating.Manual, + state: review.state, + due: review.due, + stability: card.stability, + difficulty: card.difficulty, + elapsed_days: review.elapsed_days, + last_elapsed_days: card.elapsed_days, + scheduled_days: card.scheduled_days, + review: review.review, + } + datum.push( + ( + (recordLogHandler + ? recordLogHandler({ card, log }) + : { card, log }) + ) + ) + continue + } } - processedCard.push(processCard) + const item = this.next(card, review.review, review.rating) + card = item.card + datum.push((recordLogHandler ? recordLogHandler(item) : item)) } - return processedCard + + return datum } } diff --git a/src/fsrs/models.ts b/src/fsrs/models.ts index 24199d2e..353213f8 100644 --- a/src/fsrs/models.ts +++ b/src/fsrs/models.ts @@ -91,15 +91,19 @@ export interface FSRSReview { delta_t: number } -export interface FSRSHistory { - /** - * 0-4: Manual, Again, Hard, Good, Easy - * = revlog.rating - */ - rating: Rating - /** - * The number of days that passed - * = revlog.review - */ - reviewed_at: DateInput -} +export type FSRSHistory = Partial< + Exclude +> & + ( + | { + rating: Grade + review: DateInput + } + | { + rating: Rating.Manual + due: DateInput + state: State + review: DateInput + elapsed_days: number + } + ) diff --git a/src/fsrs/types.ts b/src/fsrs/types.ts index 85cf28c5..d21adfc8 100644 --- a/src/fsrs/types.ts +++ b/src/fsrs/types.ts @@ -1,8 +1,10 @@ import type { - DateInput, + Card, + FSRSHistory, Grade, RecordLog, RecordLogItem, + ReviewLog, } from './models' export type unit = 'days' | 'minutes' @@ -18,8 +20,8 @@ export interface IScheduler { review(state: Grade): RecordLogItem } - -export type RescheduleOptions = { - enable_fuzz?: boolean - dateHandler?: (date: Date) => DateInput +export type RescheduleOptions = { + recordLogHandler:(recordLog: RecordLogItem) => T + reviewsOrderBy: (a: FSRSHistory, b: FSRSHistory) => number + skipManual: boolean } From 8a89711f1d216203cfd402a308a0d971cbb01af3 Mon Sep 17 00:00:00 2001 From: ishiko Date: Sun, 22 Sep 2024 15:38:45 +0800 Subject: [PATCH 04/24] rename --- src/fsrs/fsrs.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index fcbbacfb..eeb17ef5 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -1,4 +1,3 @@ -import { date_scheduler } from './help' import { Card, CardInput, @@ -7,7 +6,6 @@ import { FSRSParameters, Grade, Rating, - RecordLog, RecordLogItem, ReviewLog, ReviewLogInput, @@ -425,19 +423,19 @@ export class FSRS extends FSRSAlgorithm { const { recordLogHandler, reviewsOrderBy, - skipManual: skip = true, + skipManual: skipManual = true, } = options if (reviewsOrderBy && typeof reviewsOrderBy === 'function') { reviews.sort(reviewsOrderBy) } - if (skip) { + if (skipManual) { reviews = reviews.filter((review) => review.rating !== Rating.Manual) } const datum: T[] = [] let card: Card | undefined = undefined for (const [index, review] of reviews.entries()) { card = (card || createEmptyCard(review.review)) - if (!skip && review.rating === Rating.Manual) { + if (!skipManual && review.rating === Rating.Manual) { if (!review.state) { throw new Error('reschedule: state is required for manual rating') } @@ -468,7 +466,7 @@ export class FSRS extends FSRSAlgorithm { } datum.push( ( - (recordLogHandler + (recordLogHandler && typeof recordLogHandler === 'function' ? recordLogHandler({ card, log }) : { card, log }) ) @@ -478,7 +476,13 @@ export class FSRS extends FSRSAlgorithm { } const item = this.next(card, review.review, review.rating) card = item.card - datum.push((recordLogHandler ? recordLogHandler(item) : item)) + datum.push( + ( + (recordLogHandler && typeof recordLogHandler === 'function' + ? recordLogHandler(item) + : item) + ) + ) } return datum From 2d33f9b5606ce1fe9bbf79d0c2d2377ce115d5a2 Mon Sep 17 00:00:00 2001 From: ishiko Date: Sun, 22 Sep 2024 17:34:31 +0800 Subject: [PATCH 05/24] fix apply_fuzz --- __tests__/algorithm.test.ts | 15 ++++++++++ src/fsrs/algorithm.ts | 6 ++-- src/fsrs/fsrs.ts | 55 +++++++++++++++++++++++-------------- 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/__tests__/algorithm.test.ts b/__tests__/algorithm.test.ts index f323c9f9..67269680 100644 --- a/__tests__/algorithm.test.ts +++ b/__tests__/algorithm.test.ts @@ -332,6 +332,21 @@ describe('FSRS apply_fuzz', () => { expect(fuzzedInterval).toBeGreaterThanOrEqual(min_ivl) expect(fuzzedInterval).toBeLessThanOrEqual(max_ivl) }) + + test('return original interval when ivl is less than 3', () => { + const ivl = 3 + const enable_fuzz = true + const algorithm = new FSRSAlgorithm({ enable_fuzz: enable_fuzz }) + algorithm.seed = 'NegativeS2Seed' + const { min_ivl, max_ivl } = get_fuzz_range( + Math.round(ivl), + 0, + default_maximum_interval + ) + const fuzzedInterval = algorithm.apply_fuzz(ivl, 0) + expect(fuzzedInterval).toBeGreaterThanOrEqual(min_ivl) + expect(fuzzedInterval).toBeLessThanOrEqual(max_ivl) + }) }) describe('change Params', () => { diff --git a/src/fsrs/algorithm.ts b/src/fsrs/algorithm.ts index 04c9b79c..bb38b7d4 100644 --- a/src/fsrs/algorithm.ts +++ b/src/fsrs/algorithm.ts @@ -134,8 +134,8 @@ export class FSRSAlgorithm { * @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, elapsed_days: number, enable_fuzz?: boolean): int { - if (!enable_fuzz || ivl < 2.5) return Math.round(ivl) as int + apply_fuzz(ivl: number, elapsed_days: number): int { + if (!this.param.enable_fuzz || ivl < 2.5) return Math.round(ivl) as int const generator = alea(this._seed) // I do not want others to directly access the seed externally. const fuzz_factor = generator() const { min_ivl, max_ivl } = get_fuzz_range( @@ -156,7 +156,7 @@ export class FSRSAlgorithm { Math.max(1, Math.round(s * this.intervalModifier)), this.param.maximum_interval ) as int - return this.apply_fuzz(newInterval, elapsed_days, this.param.enable_fuzz) + return this.apply_fuzz(newInterval, elapsed_days) } /** diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index eeb17ef5..87294ab0 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -11,7 +11,7 @@ import { ReviewLogInput, State, } from './models' -import type { int, IPreview, RescheduleOptions } from './types' +import type { IPreview, RescheduleOptions } from './types' import { FSRSAlgorithm } from './algorithm' import { TypeConvert } from './convert' import BasicScheduler from './impl/basic_scheduler' @@ -435,25 +435,27 @@ export class FSRS extends FSRSAlgorithm { let card: Card | undefined = undefined for (const [index, review] of reviews.entries()) { card = (card || createEmptyCard(review.review)) + let log: ReviewLog if (!skipManual && review.rating === Rating.Manual) { - if (!review.state) { + if (typeof review.state === 'undefined') { throw new Error('reschedule: state is required for manual rating') } if (review.state === State.New) { - card = createEmptyCard(review.review) - } else { - card = { - ...card, + log = { + rating: Rating.Manual, state: review.state, due: review.due, - last_review: review.review, - stability: review.stability || card.stability, - difficulty: review.difficulty || card.difficulty, - elapsed_days: review.elapsed_days || card.elapsed_days, - scheduled_days: review.scheduled_days || card.scheduled_days, - reps: index, + stability: card.stability, + difficulty: card.difficulty, + elapsed_days: review.elapsed_days, + last_elapsed_days: card.elapsed_days, + scheduled_days: card.scheduled_days, + review: review.review, } - const log = { + card = createEmptyCard(review.review) + card.last_review = review.review + } else { + log = { rating: Rating.Manual, state: review.state, due: review.due, @@ -464,15 +466,26 @@ export class FSRS extends FSRSAlgorithm { scheduled_days: card.scheduled_days, review: review.review, } - datum.push( - ( - (recordLogHandler && typeof recordLogHandler === 'function' - ? recordLogHandler({ card, log }) - : { card, log }) - ) - ) - continue + card = { + ...card, + state: review.state, + due: review.due, + last_review: review.review, + stability: review.stability || card.stability, + difficulty: review.difficulty || card.difficulty, + elapsed_days: review.elapsed_days || card.elapsed_days, + scheduled_days: review.scheduled_days || card.scheduled_days, + reps: index, + } } + datum.push( + ( + (recordLogHandler && typeof recordLogHandler === 'function' + ? recordLogHandler({ card, log }) + : { card, log }) + ) + ) + continue } const item = this.next(card, review.review, review.rating) card = item.card From 7830744d6203bb1440adadd2556ff905212455db Mon Sep 17 00:00:00 2001 From: ishiko Date: Sun, 22 Sep 2024 18:45:11 +0800 Subject: [PATCH 06/24] fix elapsed_days and scheduled_day --- src/fsrs/fsrs.ts | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index 87294ab0..c647fe32 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -455,13 +455,22 @@ export class FSRS extends FSRSAlgorithm { card = createEmptyCard(review.review) card.last_review = review.review } else { + if (typeof review.due === 'undefined') { + throw new Error('reschedule: due is required for manual rating') + } + const scheduled_days = + review.due.diff(review.review as Date, 'days') || 0 + const elapsed_days = + review.elapsed_days || + review.review.diff(card.last_review as Date, 'days') || + 0 log = { rating: Rating.Manual, state: review.state, - due: review.due, + due: card.last_review || card.due, stability: card.stability, difficulty: card.difficulty, - elapsed_days: review.elapsed_days, + elapsed_days: elapsed_days, last_elapsed_days: card.elapsed_days, scheduled_days: card.scheduled_days, review: review.review, @@ -473,9 +482,9 @@ export class FSRS extends FSRSAlgorithm { last_review: review.review, stability: review.stability || card.stability, difficulty: review.difficulty || card.difficulty, - elapsed_days: review.elapsed_days || card.elapsed_days, - scheduled_days: review.scheduled_days || card.scheduled_days, - reps: index, + elapsed_days: elapsed_days, + scheduled_days: scheduled_days, + reps: index + 1, } } datum.push( @@ -485,17 +494,17 @@ export class FSRS extends FSRSAlgorithm { : { card, log }) ) ) - continue - } - const item = this.next(card, review.review, review.rating) - card = item.card - datum.push( - ( - (recordLogHandler && typeof recordLogHandler === 'function' - ? recordLogHandler(item) - : item) + } else { + const item = this.next(card, review.review, review.rating) + card = item.card + datum.push( + ( + (recordLogHandler && typeof recordLogHandler === 'function' + ? recordLogHandler(item) + : item) + ) ) - ) + } } return datum From 344a7de4cefe8073f9af99f2cef4152db1e6d6f5 Mon Sep 17 00:00:00 2001 From: ishiko Date: Sun, 22 Sep 2024 19:13:05 +0800 Subject: [PATCH 07/24] update test --- __tests__/reschedule.test.ts | 483 +++++++++++++++++++++++++---------- src/fsrs/fsrs.ts | 32 +-- 2 files changed, 348 insertions(+), 167 deletions(-) diff --git a/__tests__/reschedule.test.ts b/__tests__/reschedule.test.ts index 7443924c..4026090a 100644 --- a/__tests__/reschedule.test.ts +++ b/__tests__/reschedule.test.ts @@ -1,168 +1,371 @@ import { - Card, createEmptyCard, - date_scheduler, - default_maximum_interval, - default_request_retention, - fsrs, + date_diff, FSRS, - get_fuzz_range, + fsrs, + Grade, + Grades, + Rating, + RecordLogItem, + RescheduleOptions, + ReviewLog, State, + TypeConvert, } from '../src/fsrs' +import { FSRSHistory } from '../src/fsrs/models' -describe('FSRS reschedule', () => { - const DECAY: number = -0.5 - const FACTOR: number = 19 / 81 - const request_retentions = [default_request_retention, 0.95, 0.85, 0.8] +type reviewState = { + difficulty: number + due: Date + rating: Rating + review?: Date + stability: number + state: State + reps: number + lapses: number + elapsed_days: number + scheduled_days: number +} - type CardType = Card & { - cid: number - } +const MOCK_NOW = new Date(2024, 7, 11, 1, 0, 0) - function cardHandler(card: Card) { - ;(card as CardType)['cid'] = 1 - return card as CardType - } +// https://github.com/open-spaced-repetition/ts-fsrs/issues/112#issuecomment-2286238381 - 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 experiment( + scheduler: FSRS, + reviews: Array, + skipManual: boolean = true +) { + if (skipManual) { + reviews = reviews.filter((review) => review.rating !== Rating.Manual) } + const output = reviews.reduce( + (state: reviewState[], review: FSRSHistory, index: number) => { + const currentCard = state[index - 1] + ? { + due: state[index - 1].due, + stability: state[index - 1].stability, + difficulty: state[index - 1].difficulty, + elapsed_days: + state[index - 2]?.review && state[index - 1]?.review + ? date_diff( + state[index - 1].review!, + state[index - 2].review!, + 'days' + ) + : 0, + scheduled_days: + state[index - 1].review && state[index - 1].due + ? date_diff( + state[index - 1].review!, + state[index - 1].due, + 'days' + ) + : 0, + reps: state[index - 1].reps, + lapses: state[index - 1].lapses, + state: state[index - 1].state, + last_review: state[index - 1].review, + } + : createEmptyCard(MOCK_NOW) + + if (review.review) { + let card = currentCard + let log: ReviewLog + if (review.rating) { + const item = scheduler.next(currentCard, review.review, review.rating) + card = item.card + log = item.log + } else { + log = state[index - 1] + ? { + rating: Rating.Manual, + state: State.New, + due: state[index - 1].due, + stability: state[index - 1].stability, + difficulty: state[index - 1].difficulty, + elapsed_days: state[index - 1].elapsed_days, + last_elapsed_days: state[index - 1].elapsed_days, + scheduled_days: state[index - 1].scheduled_days, + review: review.review, + } + : { + rating: Rating.Manual, + state: State.New, + due: MOCK_NOW, + stability: 0, + difficulty: 0, + elapsed_days: 0, + last_elapsed_days: 0, + scheduled_days: 0, + review: review.review, + } + card = createEmptyCard(review.review) + } - function dateHandler(date: Date) { - return date.getTime() + return [ + ...state, + { + difficulty: card.difficulty, + due: card.due, + rating: log.rating, + review: log.review, + stability: card.stability, + state: card.state, + reps: card.reps, + lapses: card.lapses, + elapsed_days: card.elapsed_days, + scheduled_days: card.scheduled_days, + } satisfies reviewState, + ] + } + + return state + }, + [] + ) + + return output +} + +function testReschedule( + scheduler: FSRS, + tests: number[][], + options: Partial> = {} +) { + for (const test of tests) { + const reviews = test.map((rating, index) => ({ + rating: rating, + review: new Date( + new Date(MOCK_NOW).valueOf() + 1000 * 60 * 60 * 24 * (index + 1) + ), + state: rating === Rating.Manual ? State.New : undefined, + })) + const control = scheduler.reschedule(reviews, options) + const experimentResult = experiment( + scheduler, + reviews, + options.skipManual ?? true + ) + for (const [index, controlItem] of control.entries()) { + const experimentItem = experimentResult[index] + // console.log(controlItem, experimentItem, index, test) + expect(controlItem.card.difficulty).toEqual(experimentItem.difficulty) + expect(controlItem.card.due).toEqual(experimentItem.due) + expect(controlItem.card.stability).toEqual(experimentItem.stability) + expect(controlItem.card.state).toEqual(experimentItem.state) + expect(controlItem.card.last_review?.getTime()).toEqual( + experimentItem.review?.getTime() + ) + expect(controlItem.card.reps).toEqual(experimentItem.reps) + expect(controlItem.card.lapses).toEqual(experimentItem.lapses) + + expect(controlItem.card.elapsed_days).toEqual(experimentItem.elapsed_days) + expect(controlItem.card.scheduled_days).toEqual( + experimentItem.scheduled_days + ) + } } +} + +describe('FSRS reschedule', () => { + const scheduler = fsrs() - 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( - Math.round(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('basic grade', () => { + const tests: number[][] = [] + for (let i = 0; i < Grades.length; i++) { + for (let j = 0; j < Grades.length; j++) { + for (let k = 0; k < Grades.length; k++) { + for (let l = 0; l < Grades.length; l++) { + tests.push([Grades[i], Grades[j], Grades[k], Grades[l]]) + } + } } } + testReschedule(scheduler, tests, { + reviewsOrderBy: (a, b) => a.review.getTime() - b.review.getTime(), + recordLogHandler: (recordLog) => recordLog, + }) }) - 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).toEqual(1) - const { min_ivl, max_ivl } = get_fuzz_range( - Math.round(reviewCard.stability * intervalModifier), - reviewCard.elapsed_days, - default_maximum_interval - ) - // reviewCard.stability * intervalModifier = 115.73208467290684 = ivl = 116 - // max_ivl=124 expected = 124 - - 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('case : include Manual rating -> set forget', () => { + const tests: number[][] = [] + const Ratings = [ + Rating.Manual, + Rating.Again, + Rating.Hard, + Rating.Good, + Rating.Easy, + ] + for (let i = 0; i < Ratings.length; i++) { + for (let j = 0; j < Ratings.length; j++) { + for (let k = 0; k < Ratings.length; k++) { + for (let l = 0; l < Ratings.length; l++) { + for (let m = 0; m < Ratings.length; m++) { + tests.push([ + Ratings[i], + Ratings[j], + Ratings[k], + Ratings[l], + Ratings[m], + ]) + } + } + } } } + console.debug('reschedule case size:', tests.length) + testReschedule(scheduler, tests, { + reviewsOrderBy: (a, b) => a.review.getTime() - b.review.getTime(), + recordLogHandler: (recordLog) => recordLog, + skipManual: false, + }) + }) + + it('case : include Manual rating -> state have not been provided', () => { + const test = [Rating.Easy, Rating.Good, Rating.Manual, Rating.Good] + const reviews = test.map((rating, index) => ({ + rating: rating, + review: new Date( + new Date(MOCK_NOW).valueOf() + 1000 * 60 * 60 * 24 * (index + 1) + ), + })) + expect(() => { + scheduler.reschedule(reviews, { skipManual: false }) + }).toThrow('reschedule: state is required for manual rating') }) - 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 } + it('case : include Manual rating -> due have not been provided', () => { + const test = [Rating.Easy, Rating.Good, Rating.Manual, Rating.Good] + const reviews = test.map((rating, index) => ({ + rating: rating, + review: new Date( + new Date(MOCK_NOW).valueOf() + 1000 * 60 * 60 * 24 * (index + 1) + ), + state: rating === Rating.Manual ? State.Review : undefined, + })) + expect(() => { + scheduler.reschedule(reviews, { skipManual: false }) + }).toThrow('reschedule: due is required for manual rating') + }) + + it('case : include Manual rating -> Manually configure the data', () => { + const test = [Rating.Easy, Rating.Good, Rating.Manual, Rating.Good] + const reviews = test.map( + (rating, index) => + ({ + rating: rating, + review: new Date( + new Date(MOCK_NOW).valueOf() + 1000 * 60 * 60 * 24 * (index + 1) + ), + state: rating === Rating.Manual ? State.Review : undefined, + difficulty: 3.2828565, + stability: 21.79806877, + due: new Date('2024-09-04T17:00:00.000Z'), + }) satisfies FSRSHistory ) - expect(reschedule_cards.length).toEqual(0) + const expected = { + card: { + due: TypeConvert.time('2024-09-04T17:00:00.000Z'), + stability: 21.79806877, + difficulty: 3.2828565, + elapsed_days: 1, + scheduled_days: 22, + reps: 3, + lapses: 0, + state: 2, + last_review: TypeConvert.time('2024-08-13T17:00:00.000Z'), + }, + log: { + rating: 0, + state: 2, + due: TypeConvert.time('2024-08-12T17:00:00.000Z'), + stability: 18.67917062, + difficulty: 3.2828565, + elapsed_days: 1, + last_elapsed_days: 1, + scheduled_days: 19, + review: TypeConvert.time('2024-08-13T17:00:00.000Z'), + }, + } + + const nextItemExpected = { + card: { + due: TypeConvert.time('2024-09-08T17:00:00.000Z'), + stability: 24.84609459, + difficulty: 3.2828565, + elapsed_days: 1, + scheduled_days: 25, + reps: 4, + lapses: 0, + state: State.Review, + last_review: TypeConvert.time('2024-08-14T17:00:00.000Z'), + }, + log: { + rating: Rating.Good, + state: State.Review, + due: TypeConvert.time('2024-08-13T17:00:00.000Z'), + stability: 21.79806877, + difficulty: 3.2828565, + elapsed_days: 1, + last_elapsed_days: 1, + scheduled_days: 22, + review: TypeConvert.time('2024-08-14T17:00:00.000Z'), + }, + } + + const control = scheduler.reschedule(reviews, { skipManual: false }) + expect(control[2]).toEqual(expected) + expect(control[3]).toEqual(nextItemExpected) }) - it('reschedule by empty array', () => { - const f: FSRS = fsrs() - const reschedule_cards = f.reschedule([]) - expect(reschedule_cards.length).toEqual(0) + it('case : include Manual rating -> Manually configure the data and ds have not been provided', () => { + const test = [Rating.Easy, Rating.Good, Rating.Manual, Rating.Good] + const reviews = test.map( + (rating, index) => + ({ + rating: rating, + review: new Date( + new Date(MOCK_NOW).valueOf() + 1000 * 60 * 60 * 24 * (index + 1) + ), + state: rating === Rating.Manual ? State.Review : undefined, + due: new Date('2024-09-04T17:00:00.000Z'), + }) satisfies FSRSHistory + ) + const expected = { + card: { + due: TypeConvert.time('2024-09-04T17:00:00.000Z'), + stability: 18.67917062, + difficulty: 3.2828565, + elapsed_days: 1, + scheduled_days: 22, + reps: 3, + lapses: 0, + state: State.Review, + last_review: TypeConvert.time('2024-08-13T17:00:00.000Z'), + }, + log: { + rating: Rating.Manual, + state: State.Review, + due: TypeConvert.time('2024-08-12T17:00:00.000Z'), + stability: 18.67917062, + difficulty: 3.2828565, + elapsed_days: 1, + last_elapsed_days: 1, + scheduled_days: 19, + review: TypeConvert.time('2024-08-13T17:00:00.000Z'), + }, + } + + const control = scheduler.reschedule(reviews, { skipManual: false }) + expect(control[2]).toEqual(expected) }) - it('reschedule by not array', () => { - const f: FSRS = fsrs() - expect(() => { - f.reschedule(createEmptyCard() as unknown as Card[]) - }).toThrow('cards must be an array') + it('Handling the case of an empty set.', () => { + const control = scheduler.reschedule([]) + expect(control).toEqual([]) + + const control2 = scheduler.reschedule() + expect(control2).toEqual([]) }) }) diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index c647fe32..4dea6380 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -392,28 +392,8 @@ export class FSRS extends FSRSAlgorithm { /** * - * @param cards scheduled card collection - * @param options Reschedule options,fuzz is enabled by default.If the type of due is not Date, please implement dataHandler. - * @example - * ```typescript - * type CardType = Card & { - * cid: number; - * }; - * 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 f = fsrs(); - * const reschedule_cards = f.reschedule([reviewCard]); - * ``` + * @param reviews Review history + * @param options Reschedule options (Optional) * */ reschedule( @@ -458,16 +438,14 @@ export class FSRS extends FSRSAlgorithm { if (typeof review.due === 'undefined') { throw new Error('reschedule: due is required for manual rating') } - const scheduled_days = - review.due.diff(review.review as Date, 'days') || 0 + const scheduled_days = review.due.diff(review.review as Date, 'days') const elapsed_days = review.elapsed_days || - review.review.diff(card.last_review as Date, 'days') || - 0 + review.review.diff(card.last_review as Date, 'days') log = { rating: Rating.Manual, state: review.state, - due: card.last_review || card.due, + due: card.last_review, stability: card.stability, difficulty: card.difficulty, elapsed_days: elapsed_days, From 0de4e9d69c8112e7ff8f5b5787e44c4f78fa5e27 Mon Sep 17 00:00:00 2001 From: ishiko Date: Sun, 22 Sep 2024 20:35:00 +0800 Subject: [PATCH 08/24] update test --- __tests__/reschedule.test.ts | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/__tests__/reschedule.test.ts b/__tests__/reschedule.test.ts index 4026090a..7457c8b7 100644 --- a/__tests__/reschedule.test.ts +++ b/__tests__/reschedule.test.ts @@ -10,7 +10,6 @@ import { RescheduleOptions, ReviewLog, State, - TypeConvert, } from '../src/fsrs' import { FSRSHistory } from '../src/fsrs/models' @@ -261,12 +260,12 @@ describe('FSRS reschedule', () => { state: rating === Rating.Manual ? State.Review : undefined, difficulty: 3.2828565, stability: 21.79806877, - due: new Date('2024-09-04T17:00:00.000Z'), + due: new Date(1725469200000 /**2024-09-04T17:00:00.000Z GMT+8*/), }) satisfies FSRSHistory ) const expected = { card: { - due: TypeConvert.time('2024-09-04T17:00:00.000Z'), + due: new Date(1725469200000 /**2024-09-04T17:00:00.000Z GMT+8*/), stability: 21.79806877, difficulty: 3.2828565, elapsed_days: 1, @@ -274,24 +273,26 @@ describe('FSRS reschedule', () => { reps: 3, lapses: 0, state: 2, - last_review: TypeConvert.time('2024-08-13T17:00:00.000Z'), + last_review: new Date( + 1723568400000 /**2024-08-13T17:00:00.000Z GMT+8*/ + ), }, log: { rating: 0, state: 2, - due: TypeConvert.time('2024-08-12T17:00:00.000Z'), + due: new Date(1723482000000 /**'2024-08-12T17:00:00.000Z GMT+8*/), stability: 18.67917062, difficulty: 3.2828565, elapsed_days: 1, last_elapsed_days: 1, scheduled_days: 19, - review: TypeConvert.time('2024-08-13T17:00:00.000Z'), + review: new Date(1723568400000 /**'2024-08-13T17:00:00.000Z GMT+8'*/), }, } const nextItemExpected = { card: { - due: TypeConvert.time('2024-09-08T17:00:00.000Z'), + due: new Date(1725814800000 /**2024-09-08T17:00:00.000Z*/), stability: 24.84609459, difficulty: 3.2828565, elapsed_days: 1, @@ -299,18 +300,18 @@ describe('FSRS reschedule', () => { reps: 4, lapses: 0, state: State.Review, - last_review: TypeConvert.time('2024-08-14T17:00:00.000Z'), + last_review: new Date(1723654800000 /**2024-08-14T17:00:00.000Z*/), }, log: { rating: Rating.Good, state: State.Review, - due: TypeConvert.time('2024-08-13T17:00:00.000Z'), + due: new Date(1723568400000 /**2024-08-13T17:00:00.000Z*/), stability: 21.79806877, difficulty: 3.2828565, elapsed_days: 1, last_elapsed_days: 1, scheduled_days: 22, - review: TypeConvert.time('2024-08-14T17:00:00.000Z'), + review: new Date(1723654800000 /**2024-08-14T17:00:00.000Z*/), }, } @@ -329,12 +330,12 @@ describe('FSRS reschedule', () => { new Date(MOCK_NOW).valueOf() + 1000 * 60 * 60 * 24 * (index + 1) ), state: rating === Rating.Manual ? State.Review : undefined, - due: new Date('2024-09-04T17:00:00.000Z'), + due: new Date(1725469200000 /**'2024-09-04T17:00:00.000Z GMT+8'*/), }) satisfies FSRSHistory ) const expected = { card: { - due: TypeConvert.time('2024-09-04T17:00:00.000Z'), + due: new Date(1725469200000 /**'2024-09-04T17:00:00.000Z' GMT+8*/), stability: 18.67917062, difficulty: 3.2828565, elapsed_days: 1, @@ -342,18 +343,20 @@ describe('FSRS reschedule', () => { reps: 3, lapses: 0, state: State.Review, - last_review: TypeConvert.time('2024-08-13T17:00:00.000Z'), + last_review: new Date( + 1723568400000 /**'2024-08-13T17:00:00.000Z GMT+8'*/ + ), }, log: { rating: Rating.Manual, state: State.Review, - due: TypeConvert.time('2024-08-12T17:00:00.000Z'), + due: new Date(1723482000000 /**'2024-08-12T17:00:00.000Z' GMT+8*/), stability: 18.67917062, difficulty: 3.2828565, elapsed_days: 1, last_elapsed_days: 1, scheduled_days: 19, - review: TypeConvert.time('2024-08-13T17:00:00.000Z'), + review: new Date(1723568400000 /**'2024-08-13T17:00:00.000Z' GMT+8*/), }, } From f2075d90f14803e4870b4373c8ef0eb85931d816 Mon Sep 17 00:00:00 2001 From: ishiko Date: Sun, 22 Sep 2024 14:23:40 +0000 Subject: [PATCH 09/24] fix timezone --- __tests__/reschedule.test.ts | 46 ++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/__tests__/reschedule.test.ts b/__tests__/reschedule.test.ts index 7457c8b7..8a47658a 100644 --- a/__tests__/reschedule.test.ts +++ b/__tests__/reschedule.test.ts @@ -26,7 +26,7 @@ type reviewState = { scheduled_days: number } -const MOCK_NOW = new Date(2024, 7, 11, 1, 0, 0) +const MOCK_NOW = new Date(1723338000000 /**2024, 7, 11, 1, 0, 0 UTC**/) // https://github.com/open-spaced-repetition/ts-fsrs/issues/112#issuecomment-2286238381 @@ -91,7 +91,7 @@ function experiment( : { rating: Rating.Manual, state: State.New, - due: MOCK_NOW, + due: new Date(MOCK_NOW), stability: 0, difficulty: 0, elapsed_days: 0, @@ -255,44 +255,42 @@ describe('FSRS reschedule', () => { ({ rating: rating, review: new Date( - new Date(MOCK_NOW).valueOf() + 1000 * 60 * 60 * 24 * (index + 1) + new Date(MOCK_NOW).getTime() + 1000 * 60 * 60 * 24 * (index + 1) ), state: rating === Rating.Manual ? State.Review : undefined, difficulty: 3.2828565, stability: 21.79806877, - due: new Date(1725469200000 /**2024-09-04T17:00:00.000Z GMT+8*/), + due: new Date(1725469200000 /**2024-09-04T17:00:00.000Z*/), }) satisfies FSRSHistory ) const expected = { card: { - due: new Date(1725469200000 /**2024-09-04T17:00:00.000Z GMT+8*/), + due: new Date(1725469200000 /**2024-09-04T17:00:00.000Z*/), stability: 21.79806877, difficulty: 3.2828565, elapsed_days: 1, - scheduled_days: 22, + scheduled_days: 21, reps: 3, lapses: 0, state: 2, - last_review: new Date( - 1723568400000 /**2024-08-13T17:00:00.000Z GMT+8*/ - ), + last_review: new Date(1723597200000 /**2024-08-14T01:00:00.000Z*/), }, log: { rating: 0, state: 2, - due: new Date(1723482000000 /**'2024-08-12T17:00:00.000Z GMT+8*/), + due: new Date(1723510800000 /**2024-08-13T01:00:00.000Z*/), stability: 18.67917062, difficulty: 3.2828565, elapsed_days: 1, last_elapsed_days: 1, scheduled_days: 19, - review: new Date(1723568400000 /**'2024-08-13T17:00:00.000Z GMT+8'*/), + review: new Date(1723597200000 /**2024-08-14T01:00:00.000Z*/), }, } const nextItemExpected = { card: { - due: new Date(1725814800000 /**2024-09-08T17:00:00.000Z*/), + due: new Date(1725843600000 /**2024-09-09T01:00:00.000Z*/), stability: 24.84609459, difficulty: 3.2828565, elapsed_days: 1, @@ -300,18 +298,18 @@ describe('FSRS reschedule', () => { reps: 4, lapses: 0, state: State.Review, - last_review: new Date(1723654800000 /**2024-08-14T17:00:00.000Z*/), + last_review: new Date(1723683600000 /**2024-08-15T01:00:00.000Z*/), }, log: { rating: Rating.Good, state: State.Review, - due: new Date(1723568400000 /**2024-08-13T17:00:00.000Z*/), + due: new Date(1723597200000 /**2024-08-14T01:00:00.000Z*/), stability: 21.79806877, difficulty: 3.2828565, elapsed_days: 1, last_elapsed_days: 1, - scheduled_days: 22, - review: new Date(1723654800000 /**2024-08-14T17:00:00.000Z*/), + scheduled_days: 21, + review: new Date(1723683600000 /**2024-08-15T01:00:00.000Z*/), }, } @@ -327,36 +325,34 @@ describe('FSRS reschedule', () => { ({ rating: rating, review: new Date( - new Date(MOCK_NOW).valueOf() + 1000 * 60 * 60 * 24 * (index + 1) + new Date(MOCK_NOW).getTime() + 1000 * 60 * 60 * 24 * (index + 1) ), state: rating === Rating.Manual ? State.Review : undefined, - due: new Date(1725469200000 /**'2024-09-04T17:00:00.000Z GMT+8'*/), + due: new Date(1725469200000 /**'2024-09-04T17:00:00.000Z'*/), }) satisfies FSRSHistory ) const expected = { card: { - due: new Date(1725469200000 /**'2024-09-04T17:00:00.000Z' GMT+8*/), + due: new Date(1725469200000 /**'2024-09-04T17:00:00.000Z'*/), stability: 18.67917062, difficulty: 3.2828565, elapsed_days: 1, - scheduled_days: 22, + scheduled_days: 21, reps: 3, lapses: 0, state: State.Review, - last_review: new Date( - 1723568400000 /**'2024-08-13T17:00:00.000Z GMT+8'*/ - ), + last_review: new Date(1723597200000 /**'2024-08-14T01:00:00.000Z'*/), }, log: { rating: Rating.Manual, state: State.Review, - due: new Date(1723482000000 /**'2024-08-12T17:00:00.000Z' GMT+8*/), + due: new Date(1723510800000 /**2024-08-13T01:00:00.000Z*/), stability: 18.67917062, difficulty: 3.2828565, elapsed_days: 1, last_elapsed_days: 1, scheduled_days: 19, - review: new Date(1723568400000 /**'2024-08-13T17:00:00.000Z' GMT+8*/), + review: new Date(1723597200000 /**'2024-08-14T01:00:00.000Z'*/), }, } From fe1ec6085e186162008cd181f2df46cc96250362 Mon Sep 17 00:00:00 2001 From: ishiko Date: Thu, 26 Sep 2024 23:57:42 +0800 Subject: [PATCH 10/24] pref/separate the code --- src/fsrs/fsrs.ts | 84 ++++--------------------------- src/fsrs/reschedule.ts | 110 +++++++++++++++++++++++++++++++++++++++++ src/fsrs/types.ts | 4 +- 3 files changed, 123 insertions(+), 75 deletions(-) create mode 100644 src/fsrs/reschedule.ts diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index 4dea6380..f4035f83 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -17,6 +17,7 @@ import { TypeConvert } from './convert' import BasicScheduler from './impl/basic_scheduler' import LongTermScheduler from './impl/long_term_scheduler' import { createEmptyCard } from './default' +import { Reschedule } from './reschedule' export class FSRS extends FSRSAlgorithm { private Scheduler @@ -411,81 +412,16 @@ export class FSRS extends FSRSAlgorithm { if (skipManual) { reviews = reviews.filter((review) => review.rating !== Rating.Manual) } - const datum: T[] = [] - let card: Card | undefined = undefined - for (const [index, review] of reviews.entries()) { - card = (card || createEmptyCard(review.review)) - let log: ReviewLog - if (!skipManual && review.rating === Rating.Manual) { - if (typeof review.state === 'undefined') { - throw new Error('reschedule: state is required for manual rating') - } - if (review.state === State.New) { - log = { - rating: Rating.Manual, - state: review.state, - due: review.due, - stability: card.stability, - difficulty: card.difficulty, - elapsed_days: review.elapsed_days, - last_elapsed_days: card.elapsed_days, - scheduled_days: card.scheduled_days, - review: review.review, - } - card = createEmptyCard(review.review) - card.last_review = review.review - } else { - if (typeof review.due === 'undefined') { - throw new Error('reschedule: due is required for manual rating') - } - const scheduled_days = review.due.diff(review.review as Date, 'days') - const elapsed_days = - review.elapsed_days || - review.review.diff(card.last_review as Date, 'days') - log = { - rating: Rating.Manual, - state: review.state, - due: card.last_review, - stability: card.stability, - difficulty: card.difficulty, - elapsed_days: elapsed_days, - last_elapsed_days: card.elapsed_days, - scheduled_days: card.scheduled_days, - review: review.review, - } - card = { - ...card, - state: review.state, - due: review.due, - last_review: review.review, - stability: review.stability || card.stability, - difficulty: review.difficulty || card.difficulty, - elapsed_days: elapsed_days, - scheduled_days: scheduled_days, - reps: index + 1, - } - } - datum.push( - ( - (recordLogHandler && typeof recordLogHandler === 'function' - ? recordLogHandler({ card, log }) - : { card, log }) - ) - ) - } else { - const item = this.next(card, review.review, review.rating) - card = item.card - datum.push( - ( - (recordLogHandler && typeof recordLogHandler === 'function' - ? recordLogHandler(item) - : item) - ) - ) - } - } + const rescheduleSvc = new Reschedule(this) - return datum + const collections = rescheduleSvc.reschedule( + options.card || createEmptyCard(), + reviews + ) + if (recordLogHandler && typeof recordLogHandler === 'function') { + return collections.map(recordLogHandler) + } + return collections as T[] } } diff --git a/src/fsrs/reschedule.ts b/src/fsrs/reschedule.ts new file mode 100644 index 00000000..d1250770 --- /dev/null +++ b/src/fsrs/reschedule.ts @@ -0,0 +1,110 @@ +import { TypeConvert } from './convert' +import { createEmptyCard } from './default' +import type { FSRS } from './fsrs' +import { + Card, + CardInput, + FSRSHistory, + Grade, + Rating, + RecordLogItem, + ReviewLog, + State, +} from './models' + +export class Reschedule { + private fsrs: FSRS + constructor(fsrs: FSRS) { + this.fsrs = fsrs + } + + replay(card: Card, reviewed: Date, rating: Grade): RecordLogItem { + return this.fsrs.next(card, reviewed, rating) + } + + processManual( + card: Card, + state: State, + reviewed: Date, + elapsed_days?: number, + stability?: number, + difficulty?: number, + due?: Date + ): RecordLogItem { + if (typeof state === 'undefined') { + throw new Error('reschedule: state is required for manual rating') + } + let log: ReviewLog + let next_card: Card + if (state === State.New) { + log = { + rating: Rating.Manual, + state: state, + due: due ?? reviewed, + stability: card.stability, + difficulty: card.difficulty, + elapsed_days: elapsed_days || 0, + last_elapsed_days: card.elapsed_days, + scheduled_days: card.scheduled_days, + review: reviewed, + } satisfies ReviewLog + next_card = createEmptyCard(reviewed) + next_card.last_review = reviewed + } else { + if (typeof due === 'undefined') { + throw new Error('reschedule: due is required for manual rating') + } + const scheduled_days = due.diff(reviewed as Date, 'days') + elapsed_days = + elapsed_days || reviewed.diff(card.last_review as Date, 'days') + log = { + rating: Rating.Manual, + state: state, + due: card.last_review, + stability: card.stability, + difficulty: card.difficulty, + elapsed_days: elapsed_days, + last_elapsed_days: card.elapsed_days, + scheduled_days: card.scheduled_days, + review: reviewed, + } satisfies ReviewLog + next_card = { + ...card, + state: state, + due: due, + last_review: reviewed, + stability: stability || card.stability, + difficulty: difficulty || card.difficulty, + elapsed_days: elapsed_days, + scheduled_days: scheduled_days, + reps: card.reps + 1, + } satisfies Card + } + + return { card: next_card, log } + } + + reschedule(card: CardInput, reviews: FSRSHistory[]) { + const collections: RecordLogItem[] = [] + let _card = TypeConvert.card(card) + for (const review of reviews) { + let item: RecordLogItem + if (review.rating === Rating.Manual) { + item = this.processManual( + _card, + review.state, + review.review, + review.elapsed_days, + review.stability, + review.difficulty, + review.due + ) + } else { + item = this.replay(_card, review.review, review.rating) + } + collections.push(item) + _card = item.card + } + return collections + } +} diff --git a/src/fsrs/types.ts b/src/fsrs/types.ts index d21adfc8..718feba2 100644 --- a/src/fsrs/types.ts +++ b/src/fsrs/types.ts @@ -1,5 +1,6 @@ import type { Card, + CardInput, FSRSHistory, Grade, RecordLog, @@ -21,7 +22,8 @@ export interface IScheduler { } export type RescheduleOptions = { - recordLogHandler:(recordLog: RecordLogItem) => T + recordLogHandler: (recordLog: RecordLogItem) => T reviewsOrderBy: (a: FSRSHistory, b: FSRSHistory) => number skipManual: boolean + card?: CardInput } From 21eba2f6bd9e55af9dfd7da752221f2db880e9a9 Mon Sep 17 00:00:00 2001 From: ishiko Date: Fri, 27 Sep 2024 00:17:43 +0800 Subject: [PATCH 11/24] remove require elapsed_days --- src/fsrs/models.ts | 3 +-- src/fsrs/reschedule.ts | 13 ++++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/fsrs/models.ts b/src/fsrs/models.ts index 353213f8..0017bb0d 100644 --- a/src/fsrs/models.ts +++ b/src/fsrs/models.ts @@ -92,7 +92,7 @@ export interface FSRSReview { } export type FSRSHistory = Partial< - Exclude + Exclude > & ( | { @@ -104,6 +104,5 @@ export type FSRSHistory = Partial< due: DateInput state: State review: DateInput - elapsed_days: number } ) diff --git a/src/fsrs/reschedule.ts b/src/fsrs/reschedule.ts index d1250770..bfdb2e2f 100644 --- a/src/fsrs/reschedule.ts +++ b/src/fsrs/reschedule.ts @@ -26,7 +26,7 @@ export class Reschedule { card: Card, state: State, reviewed: Date, - elapsed_days?: number, + elapsed_days: number, stability?: number, difficulty?: number, due?: Date @@ -43,7 +43,7 @@ export class Reschedule { due: due ?? reviewed, stability: card.stability, difficulty: card.difficulty, - elapsed_days: elapsed_days || 0, + elapsed_days: elapsed_days, last_elapsed_days: card.elapsed_days, scheduled_days: card.scheduled_days, review: reviewed, @@ -55,8 +55,6 @@ export class Reschedule { throw new Error('reschedule: due is required for manual rating') } const scheduled_days = due.diff(reviewed as Date, 'days') - elapsed_days = - elapsed_days || reviewed.diff(card.last_review as Date, 'days') log = { rating: Rating.Manual, state: state, @@ -90,11 +88,16 @@ export class Reschedule { for (const review of reviews) { let item: RecordLogItem if (review.rating === Rating.Manual) { + // ref: abstract_scheduler.ts#init + let interval = 0 + if (_card.state !== State.New && _card.last_review) { + interval = review.review.diff(_card.last_review as Date, 'days') + } item = this.processManual( _card, review.state, review.review, - review.elapsed_days, + interval, review.stability, review.difficulty, review.due From 52f01838b3627ea214a334f517cb2e6d1a7a2bf5 Mon Sep 17 00:00:00 2001 From: ishiko Date: Mon, 30 Sep 2024 16:26:25 +0800 Subject: [PATCH 12/24] update reschedule class --- src/fsrs/reschedule.ts | 79 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/src/fsrs/reschedule.ts b/src/fsrs/reschedule.ts index bfdb2e2f..b75b5386 100644 --- a/src/fsrs/reschedule.ts +++ b/src/fsrs/reschedule.ts @@ -2,27 +2,54 @@ import { TypeConvert } from './convert' import { createEmptyCard } from './default' import type { FSRS } from './fsrs' import { - Card, - CardInput, - FSRSHistory, - Grade, + type Card, + type CardInput, + type FSRSHistory, + type Grade, Rating, - RecordLogItem, - ReviewLog, + type RecordLogItem, + type ReviewLog, State, } from './models' +/** + * The `Reschedule` class provides methods to handle the rescheduling of cards based on their review history. + * determine the next review dates and update the card's state accordingly. + */ export class Reschedule { private fsrs: FSRS + /** + * Creates an instance of the `Reschedule` class. + * @param fsrs - An instance of the FSRS class used for scheduling. + */ constructor(fsrs: FSRS) { this.fsrs = fsrs } + /** + * Replays a review for a card and determines the next review date based on the given rating. + * @param card - The card being reviewed. + * @param reviewed - The date the card was reviewed. + * @param rating - The grade given to the card during the review. + * @returns A `RecordLogItem` containing the updated card and review log. + */ replay(card: Card, reviewed: Date, rating: Grade): RecordLogItem { return this.fsrs.next(card, reviewed, rating) } - processManual( + /** + * Processes a manual review for a card, allowing for custom state, stability, difficulty, and due date. + * @param card - The card being reviewed. + * @param state - The state of the card after the review. + * @param reviewed - The date the card was reviewed. + * @param elapsed_days - The number of days since the last review. + * @param stability - (Optional) The stability of the card. + * @param difficulty - (Optional) The difficulty of the card. + * @param due - (Optional) The due date for the next review. + * @returns A `RecordLogItem` containing the updated card and review log. + * @throws Will throw an error if the state or due date is not provided when required. + */ + handleManualRating( card: Card, state: State, reviewed: Date, @@ -82,9 +109,16 @@ export class Reschedule { return { card: next_card, log } } - reschedule(card: CardInput, reviews: FSRSHistory[]) { + /** + * Reschedules a card based on its review history. + * + * @param current_card - The card to be rescheduled. + * @param reviews - An array of review history objects. + * @returns An array of record log items representing the rescheduling process. + */ + reschedule(current_card: CardInput, reviews: FSRSHistory[]) { const collections: RecordLogItem[] = [] - let _card = TypeConvert.card(card) + let _card = createEmptyCard(current_card.due) for (const review of reviews) { let item: RecordLogItem if (review.rating === Rating.Manual) { @@ -93,7 +127,7 @@ export class Reschedule { if (_card.state !== State.New && _card.last_review) { interval = review.review.diff(_card.last_review as Date, 'days') } - item = this.processManual( + item = this.handleManualRating( _card, review.state, review.review, @@ -110,4 +144,29 @@ export class Reschedule { } return collections } + + calculateManualRecord( + current_card: CardInput, + record_log_item?: RecordLogItem, + update_memory?: boolean + ): RecordLogItem | null { + if (!record_log_item) { + return null + } + // if first_card === recordItem.card then return null + const { card: reschedule_card, log } = record_log_item + const cur_card = TypeConvert.card(current_card) // copy card + if (cur_card.due.getTime() === reschedule_card.due.getTime()) { + return null + } + return this.handleManualRating( + reschedule_card, + cur_card.state, + log.review, + log.elapsed_days, + update_memory ? reschedule_card.stability : undefined, + update_memory ? reschedule_card.difficulty : undefined, + reschedule_card.due + ) + } } From 66c0b794665b28fd8c7af6fcf6763f13bfefb052 Mon Sep 17 00:00:00 2001 From: ishiko Date: Mon, 30 Sep 2024 08:36:23 +0000 Subject: [PATCH 13/24] Feat/add reschedule item --- __tests__/reschedule.test.ts | 57 +++++++++++++++++++++++++++++------- src/fsrs/fsrs.ts | 26 ++++++++++++---- src/fsrs/types.ts | 10 +++++-- 3 files changed, 75 insertions(+), 18 deletions(-) diff --git a/__tests__/reschedule.test.ts b/__tests__/reschedule.test.ts index 8a47658a..b824a468 100644 --- a/__tests__/reschedule.test.ts +++ b/__tests__/reschedule.test.ts @@ -127,10 +127,10 @@ function experiment( return output } -function testReschedule( +function testReschedule( scheduler: FSRS, tests: number[][], - options: Partial> = {} + options: Partial = {} ) { for (const test of tests) { const reviews = test.map((rating, index) => ({ @@ -140,7 +140,11 @@ function testReschedule( ), state: rating === Rating.Manual ? State.New : undefined, })) - const control = scheduler.reschedule(reviews, options) + const { collections: control } = scheduler.reschedule( + createEmptyCard(), + reviews, + options + ) const experimentResult = experiment( scheduler, reviews, @@ -230,7 +234,7 @@ describe('FSRS reschedule', () => { ), })) expect(() => { - scheduler.reschedule(reviews, { skipManual: false }) + scheduler.reschedule(createEmptyCard(), reviews, { skipManual: false }) }).toThrow('reschedule: state is required for manual rating') }) @@ -244,7 +248,7 @@ describe('FSRS reschedule', () => { state: rating === Rating.Manual ? State.Review : undefined, })) expect(() => { - scheduler.reschedule(reviews, { skipManual: false }) + scheduler.reschedule(createEmptyCard(), reviews, { skipManual: false }) }).toThrow('reschedule: due is required for manual rating') }) @@ -313,7 +317,13 @@ describe('FSRS reschedule', () => { }, } - const control = scheduler.reschedule(reviews, { skipManual: false }) + const { collections: control } = scheduler.reschedule( + createEmptyCard(), + reviews, + { + skipManual: false, + } + ) expect(control[2]).toEqual(expected) expect(control[3]).toEqual(nextItemExpected) }) @@ -356,15 +366,40 @@ describe('FSRS reschedule', () => { }, } - const control = scheduler.reschedule(reviews, { skipManual: false }) + const current_card = { + due: new Date(1725584400000 /**'2024-09-06T01:00:00.000Z'*/), + stability: 21.79806877, + difficulty: 3.2828565, + elapsed_days: 1, + scheduled_days: 22, + reps: 4, + lapses: 0, + state: State.Review, + last_review: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), + } + + const { collections: control, reschedule_item } = scheduler.reschedule( + current_card, + reviews, + { + skipManual: false, + } + ) expect(control[2]).toEqual(expected) + expect(reschedule_item).toBeNull() }) it('Handling the case of an empty set.', () => { - const control = scheduler.reschedule([]) - expect(control).toEqual([]) + const control = scheduler.reschedule(createEmptyCard(), []) + expect(control).toEqual({ + collections: [], + reschedule_item: null, + }) - const control2 = scheduler.reschedule() - expect(control2).toEqual([]) + const control2 = scheduler.reschedule(createEmptyCard()) + expect(control2).toEqual({ + collections: [], + reschedule_item: null, + }) }) }) diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index f4035f83..be55160f 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -11,7 +11,7 @@ import { ReviewLogInput, State, } from './models' -import type { IPreview, RescheduleOptions } from './types' +import type { IPreview, IReschedule, RescheduleOptions } from './types' import { FSRSAlgorithm } from './algorithm' import { TypeConvert } from './convert' import BasicScheduler from './impl/basic_scheduler' @@ -398,13 +398,15 @@ export class FSRS extends FSRSAlgorithm { * */ reschedule( + current_card: CardInput | Card, reviews: FSRSHistory[] = [], options: Partial> = {} - ): Array { + ): IReschedule { const { recordLogHandler, reviewsOrderBy, skipManual: skipManual = true, + update_memory_state: updateMemoryState = false, } = options if (reviewsOrderBy && typeof reviewsOrderBy === 'function') { reviews.sort(reviewsOrderBy) @@ -415,13 +417,27 @@ export class FSRS extends FSRSAlgorithm { const rescheduleSvc = new Reschedule(this) const collections = rescheduleSvc.reschedule( - options.card || createEmptyCard(), + options.first_card || createEmptyCard(), reviews ) + const len = collections.length + const cur_card = TypeConvert.card(current_card) + const manual_item = rescheduleSvc.calculateManualRecord( + cur_card, + len ? collections[len - 1] : undefined, + updateMemoryState + ) + if (recordLogHandler && typeof recordLogHandler === 'function') { - return collections.map(recordLogHandler) + return { + collections: collections.map(recordLogHandler), + reschedule_item: manual_item ? recordLogHandler(manual_item) : null, + } } - return collections as T[] + return { + collections, + reschedule_item: manual_item, + } as IReschedule } } diff --git a/src/fsrs/types.ts b/src/fsrs/types.ts index 718feba2..5fdb1212 100644 --- a/src/fsrs/types.ts +++ b/src/fsrs/types.ts @@ -21,9 +21,15 @@ export interface IScheduler { review(state: Grade): RecordLogItem } -export type RescheduleOptions = { +export type RescheduleOptions = { recordLogHandler: (recordLog: RecordLogItem) => T reviewsOrderBy: (a: FSRSHistory, b: FSRSHistory) => number skipManual: boolean - card?: CardInput + update_memory_state: boolean + first_card?: CardInput +} + +export type IReschedule = { + collections: T[] + reschedule_item: T | null } From 027d1b5c3fad148153ff57779f25c390261e9de6 Mon Sep 17 00:00:00 2001 From: ishiko Date: Mon, 30 Sep 2024 17:21:01 +0800 Subject: [PATCH 14/24] update test --- __tests__/reschedule.test.ts | 66 ++++++++++++++++++++++++++++++++++++ src/fsrs/reschedule.ts | 12 ++++--- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/__tests__/reschedule.test.ts b/__tests__/reschedule.test.ts index b824a468..fda1ade6 100644 --- a/__tests__/reschedule.test.ts +++ b/__tests__/reschedule.test.ts @@ -389,6 +389,72 @@ describe('FSRS reschedule', () => { expect(reschedule_item).toBeNull() }) + it('case : get reschedule item', () => { + const test = [Rating.Easy, Rating.Good, Rating.Good, Rating.Good] + const reviews = test.map( + (rating, index) => + ({ + rating: rating, + review: new Date( + new Date(MOCK_NOW).getTime() + 1000 * 60 * 60 * 24 * (index + 1) + ), + state: rating === Rating.Manual ? State.Review : undefined, + due: new Date(1725469200000 /**'2024-09-04T17:00:00.000Z'*/), + }) satisfies FSRSHistory + ) + + const expected = { + card: { + due: new Date(1725843600000 /**'2024-09-09T01:00:00.000Z'*/), + stability: 24.84609459, + difficulty: 3.2828565, + elapsed_days: 1, + scheduled_days: 25, + reps: 4, + lapses: 0, + state: State.Review, + last_review: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), + }, + log: { + rating: Rating.Good, + state: State.Review, + due: new Date(1723597200000 /**2024-08-14T01:00:00.000Z*/), + stability: 21.79806877, + difficulty: 3.2828565, + elapsed_days: 1, + last_elapsed_days: 1, + scheduled_days: 22, + review: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), + }, + } + const cur_card = createEmptyCard(MOCK_NOW) + const { collections: control, reschedule_item } = scheduler.reschedule( + cur_card, + reviews, + { + skipManual: false, + update_memory_state: true, + } + ) + expect(control[control.length - 1]).toEqual(expected) + expect(reschedule_item).toEqual({ + card: { + ...expected.card, + last_review: MOCK_NOW, + reps: cur_card.reps + 1, + }, + log: { + ...expected.log, + rating: Rating.Manual, + state: cur_card.state, + due: cur_card.last_review || cur_card.due, + stability: cur_card.stability, + difficulty: cur_card.difficulty, + review: MOCK_NOW, + }, + } satisfies RecordLogItem) + }) + it('Handling the case of an empty set.', () => { const control = scheduler.reschedule(createEmptyCard(), []) expect(control).toEqual({ diff --git a/src/fsrs/reschedule.ts b/src/fsrs/reschedule.ts index b75b5386..894a3d24 100644 --- a/src/fsrs/reschedule.ts +++ b/src/fsrs/reschedule.ts @@ -84,8 +84,8 @@ export class Reschedule { const scheduled_days = due.diff(reviewed as Date, 'days') log = { rating: Rating.Manual, - state: state, - due: card.last_review, + state: card.state, + due: card.last_review || card.due, stability: card.stability, difficulty: card.difficulty, elapsed_days: elapsed_days, @@ -159,9 +159,13 @@ export class Reschedule { if (cur_card.due.getTime() === reschedule_card.due.getTime()) { return null } + let interval = 0 + if (cur_card.state !== State.New && cur_card.last_review) { + interval = log.review.diff(cur_card.last_review as Date, 'days') + } return this.handleManualRating( - reschedule_card, - cur_card.state, + cur_card, + reschedule_card.state, log.review, log.elapsed_days, update_memory ? reschedule_card.stability : undefined, From 033be58ec39d8092b7d3eb7213062a50a49929bd Mon Sep 17 00:00:00 2001 From: ishiko Date: Mon, 30 Sep 2024 17:32:47 +0800 Subject: [PATCH 15/24] add review time option --- __tests__/reschedule.test.ts | 5 +++-- src/fsrs/fsrs.ts | 2 ++ src/fsrs/reschedule.ts | 4 +++- src/fsrs/types.ts | 2 ++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/__tests__/reschedule.test.ts b/__tests__/reschedule.test.ts index fda1ade6..6d8b8fd2 100644 --- a/__tests__/reschedule.test.ts +++ b/__tests__/reschedule.test.ts @@ -434,13 +434,14 @@ describe('FSRS reschedule', () => { { skipManual: false, update_memory_state: true, + now: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), } ) expect(control[control.length - 1]).toEqual(expected) expect(reschedule_item).toEqual({ card: { ...expected.card, - last_review: MOCK_NOW, + last_review: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), reps: cur_card.reps + 1, }, log: { @@ -450,7 +451,7 @@ describe('FSRS reschedule', () => { due: cur_card.last_review || cur_card.due, stability: cur_card.stability, difficulty: cur_card.difficulty, - review: MOCK_NOW, + review: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), }, } satisfies RecordLogItem) }) diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index be55160f..2c3000d4 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -406,6 +406,7 @@ export class FSRS extends FSRSAlgorithm { recordLogHandler, reviewsOrderBy, skipManual: skipManual = true, + now: now = new Date(), update_memory_state: updateMemoryState = false, } = options if (reviewsOrderBy && typeof reviewsOrderBy === 'function') { @@ -424,6 +425,7 @@ export class FSRS extends FSRSAlgorithm { const cur_card = TypeConvert.card(current_card) const manual_item = rescheduleSvc.calculateManualRecord( cur_card, + now, len ? collections[len - 1] : undefined, updateMemoryState ) diff --git a/src/fsrs/reschedule.ts b/src/fsrs/reschedule.ts index 894a3d24..3a08adf8 100644 --- a/src/fsrs/reschedule.ts +++ b/src/fsrs/reschedule.ts @@ -4,6 +4,7 @@ import type { FSRS } from './fsrs' import { type Card, type CardInput, + DateInput, type FSRSHistory, type Grade, Rating, @@ -147,6 +148,7 @@ export class Reschedule { calculateManualRecord( current_card: CardInput, + now: DateInput, record_log_item?: RecordLogItem, update_memory?: boolean ): RecordLogItem | null { @@ -166,7 +168,7 @@ export class Reschedule { return this.handleManualRating( cur_card, reschedule_card.state, - log.review, + TypeConvert.time(now), log.elapsed_days, update_memory ? reschedule_card.stability : undefined, update_memory ? reschedule_card.difficulty : undefined, diff --git a/src/fsrs/types.ts b/src/fsrs/types.ts index 5fdb1212..74375424 100644 --- a/src/fsrs/types.ts +++ b/src/fsrs/types.ts @@ -1,6 +1,7 @@ import type { Card, CardInput, + DateInput, FSRSHistory, Grade, RecordLog, @@ -26,6 +27,7 @@ export type RescheduleOptions = { reviewsOrderBy: (a: FSRSHistory, b: FSRSHistory) => number skipManual: boolean update_memory_state: boolean + now: DateInput first_card?: CardInput } From 88c3ce9b203ca206366e2440bc9fbfaaa9bc0013 Mon Sep 17 00:00:00 2001 From: ishiko Date: Mon, 30 Sep 2024 17:40:39 +0800 Subject: [PATCH 16/24] fix scheduled_days --- __tests__/reschedule.test.ts | 3 +++ src/fsrs/reschedule.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/__tests__/reschedule.test.ts b/__tests__/reschedule.test.ts index 6d8b8fd2..21cac6f0 100644 --- a/__tests__/reschedule.test.ts +++ b/__tests__/reschedule.test.ts @@ -437,6 +437,7 @@ describe('FSRS reschedule', () => { now: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), } ) + const scheduled_days = reschedule_item!.card.due.diff(cur_card.due, 'days') expect(control[control.length - 1]).toEqual(expected) expect(reschedule_item).toEqual({ card: { @@ -449,6 +450,8 @@ describe('FSRS reschedule', () => { rating: Rating.Manual, state: cur_card.state, due: cur_card.last_review || cur_card.due, + last_elapsed_days: cur_card.elapsed_days, + scheduled_days: scheduled_days, stability: cur_card.stability, difficulty: cur_card.difficulty, review: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), diff --git a/src/fsrs/reschedule.ts b/src/fsrs/reschedule.ts index 3a08adf8..02ba6634 100644 --- a/src/fsrs/reschedule.ts +++ b/src/fsrs/reschedule.ts @@ -165,6 +165,10 @@ export class Reschedule { if (cur_card.state !== State.New && cur_card.last_review) { interval = log.review.diff(cur_card.last_review as Date, 'days') } + cur_card.scheduled_days = reschedule_card.due.diff( + cur_card.due as Date, + 'days' + ) return this.handleManualRating( cur_card, reschedule_card.state, From 16fc31496041489be14160ad860728f87c658524 Mon Sep 17 00:00:00 2001 From: ishiko Date: Mon, 30 Sep 2024 17:51:40 +0800 Subject: [PATCH 17/24] update test --- __tests__/reschedule.test.ts | 69 ++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/__tests__/reschedule.test.ts b/__tests__/reschedule.test.ts index 21cac6f0..1795edac 100644 --- a/__tests__/reschedule.test.ts +++ b/__tests__/reschedule.test.ts @@ -427,36 +427,45 @@ describe('FSRS reschedule', () => { review: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), }, } - const cur_card = createEmptyCard(MOCK_NOW) - const { collections: control, reschedule_item } = scheduler.reschedule( - cur_card, - reviews, - { - skipManual: false, - update_memory_state: true, - now: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), - } - ) - const scheduled_days = reschedule_item!.card.due.diff(cur_card.due, 'days') - expect(control[control.length - 1]).toEqual(expected) - expect(reschedule_item).toEqual({ - card: { - ...expected.card, - last_review: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), - reps: cur_card.reps + 1, - }, - log: { - ...expected.log, - rating: Rating.Manual, - state: cur_card.state, - due: cur_card.last_review || cur_card.due, - last_elapsed_days: cur_card.elapsed_days, - scheduled_days: scheduled_days, - stability: cur_card.stability, - difficulty: cur_card.difficulty, - review: new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/), - }, - } satisfies RecordLogItem) + let cur_card = createEmptyCard(MOCK_NOW) + let index = 0 + const review_at = new Date(1723683600000 /**'2024-08-15T01:00:00.000Z'*/) + for (const _ of test) { + const { collections: control, reschedule_item } = scheduler.reschedule( + cur_card, + reviews, + { + skipManual: false, + update_memory_state: true, + now: review_at, + } + ) + const scheduled_days = reschedule_item!.card.due.diff( + cur_card.due, + 'days' + ) + expect(control[control.length - 1]).toEqual(expected) + expect(reschedule_item).toEqual({ + card: { + ...expected.card, + last_review: review_at, + reps: cur_card.reps + 1, + }, + log: { + ...expected.log, + rating: Rating.Manual, + state: cur_card.state, + due: cur_card.last_review || cur_card.due, + last_elapsed_days: cur_card.elapsed_days, + scheduled_days: scheduled_days, + stability: cur_card.stability, + difficulty: cur_card.difficulty, + review: review_at, + }, + } satisfies RecordLogItem) + cur_card = control[index++].card + // index++ + } }) it('Handling the case of an empty set.', () => { From f90ecdf0a279f72d505b819082add5c3f2f985d4 Mon Sep 17 00:00:00 2001 From: ishiko732 Date: Sun, 6 Oct 2024 16:04:56 +0800 Subject: [PATCH 18/24] add more example --- __tests__/reschedule.test.ts | 58 ++++++++++++++++++++++++++++++++++++ src/fsrs/fsrs.ts | 38 +++++++++++++++++++++-- src/fsrs/types.ts | 35 ++++++++++++++++++++++ 3 files changed, 128 insertions(+), 3 deletions(-) diff --git a/__tests__/reschedule.test.ts b/__tests__/reschedule.test.ts index 1795edac..ad4082b0 100644 --- a/__tests__/reschedule.test.ts +++ b/__tests__/reschedule.test.ts @@ -481,4 +481,62 @@ describe('FSRS reschedule', () => { reschedule_item: null, }) }) + + it('case : basic test', () => { + const f = fsrs() + const grades: Grade[] = [Rating.Good, Rating.Good, Rating.Good, Rating.Good] + const reviews_at = [ + new Date(2024, 8, 13), + new Date(2024, 8, 13), + new Date(2024, 8, 17), + new Date(2024, 8, 28), + ] + + const reviews: FSRSHistory[] = [] + for (let i = 0; i < grades.length; i++) { + reviews.push({ + rating: grades[i], + review: reviews_at[i], + }) + } + + const results_short = scheduler.reschedule( + createEmptyCard(), + reviews, + { + skipManual: false, + } + ) + const ivl_history_short = results_short.collections.map((item) => item.card.scheduled_days) + const s_history_short = results_short.collections.map((item) => item.card.stability) + const d_history_short = results_short.collections.map((item) => item.card.difficulty) + + expect(results_short.reschedule_item).not.toBeNull() + expect(results_short.collections.length).toEqual(4) + expect(ivl_history_short).toEqual([0, 4, 15, 40]) + expect(s_history_short).toEqual([3.1262, 4.35097949, 14.94870008, 39.68105285]) + expect(d_history_short).toEqual([5.31457783, 5.26703555, 5.22060576, 5.17526243]) + + // switch long-term scheduler + f.parameters.enable_short_term = false + const results = f.reschedule(createEmptyCard(), reviews, { + skipManual: false, + }) + const ivl_history_long = results.collections.map( + (item) => item.card.scheduled_days + ) + const s_history_long = results.collections.map( + (item) => item.card.stability + ) + const d_history_long = results.collections.map( + (item) => item.card.difficulty + ) + expect(results.reschedule_item).not.toBeNull() + expect(results.collections.length).toEqual(4) + expect(ivl_history_long).toEqual([3, 4, 14, 39]) + expect(s_history_long).toEqual([3.1262, 3.1262, 13.89723677, 38.7694699]) + expect(d_history_long).toEqual([ + 5.31457783, 5.26703555, 5.22060576, 5.17526243, + ]) + }) }) diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index 2c3000d4..06387c71 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -392,10 +392,42 @@ export class FSRS extends FSRSAlgorithm { } /** + * Reschedules the current card and returns the rescheduled collections and reschedule item. * - * @param reviews Review history - * @param options Reschedule options (Optional) - * + * @template T - The type of the record log item. + * @param {CardInput | Card} current_card - The current card to be rescheduled. + * @param {Array} reviews - The array of FSRSHistory objects representing the reviews. + * @param {Partial>} options - The optional reschedule options. + * @returns {IReschedule} - The rescheduled collections and reschedule item. + * + * @example + * ``` + const f = fsrs() + const grades: Grade[] = [Rating.Good, Rating.Good, Rating.Good, Rating.Good] + const reviews_at = [ + new Date(2024, 8, 13), + new Date(2024, 8, 13), + new Date(2024, 8, 17), + new Date(2024, 8, 28), + ] + + const reviews: FSRSHistory[] = [] + for (let i = 0; i < grades.length; i++) { + reviews.push({ + rating: grades[i], + review: reviews_at[i], + }) + } + + const results_short = scheduler.reschedule( + createEmptyCard(), + reviews, + { + skipManual: false, + } + ) + console.log(results_short) + * ``` */ reschedule( current_card: CardInput | Card, diff --git a/src/fsrs/types.ts b/src/fsrs/types.ts index 74375424..0d5dbaa2 100644 --- a/src/fsrs/types.ts +++ b/src/fsrs/types.ts @@ -22,12 +22,47 @@ export interface IScheduler { review(state: Grade): RecordLogItem } +/** + * Options for rescheduling. + * + * @template T - The type of the result returned by the `recordLogHandler` function. + */ export type RescheduleOptions = { + /** + * A function that handles recording the log. + * + * @param recordLog - The log to be recorded. + * @returns The result of recording the log. + */ recordLogHandler: (recordLog: RecordLogItem) => T + + /** + * A function that defines the order of reviews. + * + * @param a - The first FSRSHistory object. + * @param b - The second FSRSHistory object. + * @returns A negative number if `a` should be ordered before `b`, a positive number if `a` should be ordered after `b`, or 0 if they have the same order. + */ reviewsOrderBy: (a: FSRSHistory, b: FSRSHistory) => number + + /** + * Indicating whether to skip manual steps. + */ skipManual: boolean + + /** + * Indicating whether to update the FSRS memory state. + */ update_memory_state: boolean + + /** + * The current date and time. + */ now: DateInput + + /** + * The input for the first card. + */ first_card?: CardInput } From a8ca0fad9d43a1c2dc7d76e550fdddb2ad21736e Mon Sep 17 00:00:00 2001 From: ishiko732 Date: Sun, 6 Oct 2024 17:27:49 +0800 Subject: [PATCH 19/24] update typo --- src/fsrs/fsrs.ts | 1 + src/fsrs/models.ts | 8 ++++---- src/fsrs/reschedule.ts | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index 06387c71..72f69b31 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -395,6 +395,7 @@ export class FSRS extends FSRSAlgorithm { * Reschedules the current card and returns the rescheduled collections and reschedule item. * * @template T - The type of the record log item. + * @template D - The type of the date input. * @param {CardInput | Card} current_card - The current card to be rescheduled. * @param {Array} reviews - The array of FSRSHistory objects representing the reviews. * @param {Partial>} options - The optional reschedule options. diff --git a/src/fsrs/models.ts b/src/fsrs/models.ts index 0017bb0d..9ab21c84 100644 --- a/src/fsrs/models.ts +++ b/src/fsrs/models.ts @@ -92,17 +92,17 @@ export interface FSRSReview { } export type FSRSHistory = Partial< - Exclude + Omit > & ( | { rating: Grade - review: DateInput + review: DateInput | Date } | { rating: Rating.Manual - due: DateInput + due: DateInput | Date state: State - review: DateInput + review: DateInput | Date } ) diff --git a/src/fsrs/reschedule.ts b/src/fsrs/reschedule.ts index 02ba6634..93af3f9d 100644 --- a/src/fsrs/reschedule.ts +++ b/src/fsrs/reschedule.ts @@ -122,6 +122,7 @@ export class Reschedule { let _card = createEmptyCard(current_card.due) for (const review of reviews) { let item: RecordLogItem + review.review = TypeConvert.time(review.review) if (review.rating === Rating.Manual) { // ref: abstract_scheduler.ts#init let interval = 0 @@ -135,7 +136,7 @@ export class Reschedule { interval, review.stability, review.difficulty, - review.due + review.due ? TypeConvert.time(review.due) : undefined ) } else { item = this.replay(_card, review.review, review.rating) From 62c41710aceb49e39308fe7be1f335ae5349adbd Mon Sep 17 00:00:00 2001 From: ishiko732 Date: Sun, 6 Oct 2024 17:40:36 +0800 Subject: [PATCH 20/24] add more tests --- __tests__/reschedule.test.ts | 129 +++++++++++++++++++++++++++++++---- 1 file changed, 115 insertions(+), 14 deletions(-) diff --git a/__tests__/reschedule.test.ts b/__tests__/reschedule.test.ts index ad4082b0..ac352939 100644 --- a/__tests__/reschedule.test.ts +++ b/__tests__/reschedule.test.ts @@ -10,8 +10,9 @@ import { RescheduleOptions, ReviewLog, State, + TypeConvert, } from '../src/fsrs' -import { FSRSHistory } from '../src/fsrs/models' +import { Card, DateInput, FSRSHistory } from '../src/fsrs/models' type reviewState = { difficulty: number @@ -76,6 +77,7 @@ function experiment( card = item.card log = item.log } else { + review.review = TypeConvert.time(review.review) log = state[index - 1] ? { rating: Rating.Manual, @@ -186,7 +188,8 @@ describe('FSRS reschedule', () => { } } testReschedule(scheduler, tests, { - reviewsOrderBy: (a, b) => a.review.getTime() - b.review.getTime(), + reviewsOrderBy: (a: FSRSHistory, b: FSRSHistory) => + date_diff(a.review, b.review, 'days'), recordLogHandler: (recordLog) => recordLog, }) }) @@ -219,7 +222,8 @@ describe('FSRS reschedule', () => { } console.debug('reschedule case size:', tests.length) testReschedule(scheduler, tests, { - reviewsOrderBy: (a, b) => a.review.getTime() - b.review.getTime(), + reviewsOrderBy: (a: FSRSHistory, b: FSRSHistory) => + date_diff(a.review, b.review, 'days'), recordLogHandler: (recordLog) => recordLog, skipManual: false, }) @@ -500,22 +504,28 @@ describe('FSRS reschedule', () => { }) } - const results_short = scheduler.reschedule( - createEmptyCard(), - reviews, - { - skipManual: false, - } + const results_short = f.reschedule(createEmptyCard(), reviews, { + skipManual: false, + }) + const ivl_history_short = results_short.collections.map( + (item) => item.card.scheduled_days + ) + const s_history_short = results_short.collections.map( + (item) => item.card.stability + ) + const d_history_short = results_short.collections.map( + (item) => item.card.difficulty ) - const ivl_history_short = results_short.collections.map((item) => item.card.scheduled_days) - const s_history_short = results_short.collections.map((item) => item.card.stability) - const d_history_short = results_short.collections.map((item) => item.card.difficulty) expect(results_short.reschedule_item).not.toBeNull() expect(results_short.collections.length).toEqual(4) expect(ivl_history_short).toEqual([0, 4, 15, 40]) - expect(s_history_short).toEqual([3.1262, 4.35097949, 14.94870008, 39.68105285]) - expect(d_history_short).toEqual([5.31457783, 5.26703555, 5.22060576, 5.17526243]) + expect(s_history_short).toEqual([ + 3.1262, 4.35097949, 14.94870008, 39.68105285, + ]) + expect(d_history_short).toEqual([ + 5.31457783, 5.26703555, 5.22060576, 5.17526243, + ]) // switch long-term scheduler f.parameters.enable_short_term = false @@ -539,4 +549,95 @@ describe('FSRS reschedule', () => { 5.31457783, 5.26703555, 5.22060576, 5.17526243, ]) }) + + it('case : current card = reschedule card', () => { + const grades: Grade[] = [Rating.Good, Rating.Good, Rating.Good, Rating.Good] + const reviews_at: number[] = [ + Date.UTC(2024, 8, 13, 0, 0, 0), + Date.UTC(2024, 8, 13, 0, 0, 0), + Date.UTC(2024, 8, 17, 0, 0, 0), + Date.UTC(2024, 8, 28, 0, 0, 0), + ] + + const reviews: FSRSHistory[] = [] + for (let i = 0; i < grades.length; i++) { + reviews.push({ + rating: grades[i], + review: reviews_at[i], + }) + } + const current_card = { + due: new Date(1730937600000 /** 2024-11-07T00:00:00.000Z */), + stability: 39.68105285, + difficulty: 5.17526243, + elapsed_days: 11, + scheduled_days: 40, + reps: 4, + lapses: 0, + state: State.Review, + last_review: Date.UTC(2024, 9, 27, 0, 0, 0), + } + + const results_short = scheduler.reschedule(current_card, reviews, { + recordLogHandler: (recordLog) => { + return recordLog + }, + skipManual: false, + first_card: createEmptyCard(Date.UTC(2024, 8, 13, 0, 0, 0)), + update_memory_state: true, + now: Date.UTC(2024, 9, 27, 0, 0, 0), + }) + expect(results_short.reschedule_item).toBeNull() + }) + + it('case : forget', () => { + const grades: Grade[] = [Rating.Good, Rating.Good, Rating.Good, Rating.Good] + const reviews_at: number[] = [ + Date.UTC(2024, 8, 13, 0, 0, 0), + Date.UTC(2024, 8, 13, 0, 0, 0), + Date.UTC(2024, 8, 17, 0, 0, 0), + Date.UTC(2024, 8, 28, 0, 0, 0), + ] + + const reviews: FSRSHistory[] = [] + for (let i = 0; i < grades.length; i++) { + reviews.push({ + rating: grades[i], + review: reviews_at[i], + }) + } + const first_card = createEmptyCard(Date.UTC(2024, 8, 28, 0, 0, 0)) + let current_card: Card = createEmptyCard() + const history_card: Card[] = [] + for (const review of reviews) { + const item = scheduler.next( + current_card, + review.review, + review.rating + ) + current_card = item.card + history_card.push(current_card) + } + const { card: forget_card } = scheduler.forget( + current_card, + Date.UTC(2024, 9, 27, 0, 0, 0) + ) + current_card = forget_card + + const { reschedule_item } = scheduler.reschedule(current_card!, reviews, { + first_card: first_card, + update_memory_state: true, + now: Date.UTC(2024, 9, 27, 0, 0, 0), + }) + expect(reschedule_item).not.toBeNull() + expect(reschedule_item!.card.due).toEqual( + history_card[history_card.length - 1].due + ) + expect(reschedule_item!.card.stability).toEqual( + history_card[history_card.length - 1].stability + ) + expect(reschedule_item!.card.difficulty).toEqual( + history_card[history_card.length - 1].difficulty + ) + }) }) From 71a678917f3ed19a8d1313624021ab66cfbcd120 Mon Sep 17 00:00:00 2001 From: ishiko732 Date: Sun, 6 Oct 2024 18:21:29 +0800 Subject: [PATCH 21/24] remove note --- src/fsrs/fsrs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index 72f69b31..06387c71 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -395,7 +395,6 @@ export class FSRS extends FSRSAlgorithm { * Reschedules the current card and returns the rescheduled collections and reschedule item. * * @template T - The type of the record log item. - * @template D - The type of the date input. * @param {CardInput | Card} current_card - The current card to be rescheduled. * @param {Array} reviews - The array of FSRSHistory objects representing the reviews. * @param {Partial>} options - The optional reschedule options. From dd006557a9e779232ad4a427ec905cd4f8e18801 Mon Sep 17 00:00:00 2001 From: ishiko Date: Tue, 8 Oct 2024 12:18:17 +0800 Subject: [PATCH 22/24] _card -> cur_card --- src/fsrs/reschedule.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/fsrs/reschedule.ts b/src/fsrs/reschedule.ts index 93af3f9d..90475b8e 100644 --- a/src/fsrs/reschedule.ts +++ b/src/fsrs/reschedule.ts @@ -119,18 +119,18 @@ export class Reschedule { */ reschedule(current_card: CardInput, reviews: FSRSHistory[]) { const collections: RecordLogItem[] = [] - let _card = createEmptyCard(current_card.due) + let cur_card = createEmptyCard(current_card.due) for (const review of reviews) { let item: RecordLogItem review.review = TypeConvert.time(review.review) if (review.rating === Rating.Manual) { // ref: abstract_scheduler.ts#init let interval = 0 - if (_card.state !== State.New && _card.last_review) { - interval = review.review.diff(_card.last_review as Date, 'days') + if (cur_card.state !== State.New && cur_card.last_review) { + interval = review.review.diff(cur_card.last_review as Date, 'days') } item = this.handleManualRating( - _card, + cur_card, review.state, review.review, interval, @@ -139,10 +139,10 @@ export class Reschedule { review.due ? TypeConvert.time(review.due) : undefined ) } else { - item = this.replay(_card, review.review, review.rating) + item = this.replay(cur_card, review.review, review.rating) } collections.push(item) - _card = item.card + cur_card = item.card } return collections } From 464cddcbf446185aaaec8b1472b3f3b8304e8eb7 Mon Sep 17 00:00:00 2001 From: ishiko Date: Tue, 8 Oct 2024 12:19:40 +0800 Subject: [PATCH 23/24] pref code --- src/fsrs/reschedule.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/fsrs/reschedule.ts b/src/fsrs/reschedule.ts index 90475b8e..e1c0a7ed 100644 --- a/src/fsrs/reschedule.ts +++ b/src/fsrs/reschedule.ts @@ -162,10 +162,6 @@ export class Reschedule { if (cur_card.due.getTime() === reschedule_card.due.getTime()) { return null } - let interval = 0 - if (cur_card.state !== State.New && cur_card.last_review) { - interval = log.review.diff(cur_card.last_review as Date, 'days') - } cur_card.scheduled_days = reschedule_card.due.diff( cur_card.due as Date, 'days' From 594d6584198fc94e2d579d651e17ff755cfb9f0e Mon Sep 17 00:00:00 2001 From: ishiko Date: Tue, 8 Oct 2024 23:52:45 +0800 Subject: [PATCH 24/24] bump version to 4.4.0 --- package.json | 2 +- src/fsrs/default.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index abff1d1b..1d4ecf0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-fsrs", - "version": "4.3.1", + "version": "4.4.0", "description": "ts-fsrs is a versatile package based on TypeScript that supports ES modules, CommonJS, and UMD. It implements the Free Spaced Repetition Scheduler (FSRS) algorithm, enabling developers to integrate FSRS into their flashcard applications to enhance the user learning experience.", "main": "dist/index.cjs", "umd": "dist/index.umd.js", diff --git a/src/fsrs/default.ts b/src/fsrs/default.ts index f727a9ec..936a22b0 100644 --- a/src/fsrs/default.ts +++ b/src/fsrs/default.ts @@ -11,7 +11,7 @@ export const default_w = [ export const default_enable_fuzz = false export const default_enable_short_term = true -export const FSRSVersion: string = 'v4.3.1 using FSRS V5.0' +export const FSRSVersion: string = 'v4.4.0 using FSRS V5.0' export const generatorParameters = ( props?: Partial