From adb03e75121051aeb74c3fcc312e61c1eb697783 Mon Sep 17 00:00:00 2001 From: ishiko Date: Sun, 21 Jul 2024 22:48:50 +0800 Subject: [PATCH] Feat/add the next method to return card and log results (#101) * add next method * add test&doc --- __tests__/FSRSV5.test.ts | 27 +++++++++----- __tests__/handler.test.ts | 46 ++++++++++++++++++++++++ src/fsrs/fsrs.ts | 76 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 8 deletions(-) diff --git a/__tests__/FSRSV5.test.ts b/__tests__/FSRSV5.test.ts index 4421e69..93129a9 100644 --- a/__tests__/FSRSV5.test.ts +++ b/__tests__/FSRSV5.test.ts @@ -9,14 +9,12 @@ import { } from '../src/fsrs' describe('FSRS V5 ', () => { - const f: FSRS = fsrs({ - w: [ - 0.4197, 1.1869, 3.0412, 15.2441, 7.1434, 0.6477, 1.0007, 0.0674, 1.6597, - 0.1712, 1.1178, 2.0225, 0.0904, 0.3025, 2.1214, 0.2498, 2.9466, 0.4891, - 0.6468, - ], - enable_fuzz: false, - }) + const w = [ + 0.4197, 1.1869, 3.0412, 15.2441, 7.1434, 0.6477, 1.0007, 0.0674, 1.6597, + 0.1712, 1.1178, 2.0225, 0.0904, 0.3025, 2.1214, 0.2498, 2.9466, 0.4891, + 0.6468, + ] + const f: FSRS = fsrs({ w }) const grade: Grade[] = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy] it('ivl_history', () => { let card = createEmptyCard() @@ -48,6 +46,9 @@ describe('FSRS V5 ', () => { expect(scheduling_cards[check].log.elapsed_days).toEqual( card.last_review ? now.diff(card.last_review as Date, 'days') : 0 ) + const _f = fsrs({ w }) + const next = _f.next(card, now, check) + expect(scheduling_cards[check]).toEqual(next) } card = scheduling_cards[rating].card const ivl = card.scheduled_days @@ -127,3 +128,13 @@ describe('get retrievability', () => { }) }) }) + +describe('fsrs.next method', () => { + const fsrs = new FSRS({}) + test('invalid grade', () => { + const card = createEmptyCard() + const now = new Date() + const g = Rating.Manual as unknown as Grade + expect(() => fsrs.next(card, now, g)).toThrow('Cannot review a manual rating') + }) +}) diff --git a/__tests__/handler.test.ts b/__tests__/handler.test.ts index 39c6a5a..4664357 100644 --- a/__tests__/handler.test.ts +++ b/__tests__/handler.test.ts @@ -101,6 +101,27 @@ describe('afterHandler', () => { // } // return record; // } + function nextAfterHandler(recordLogItem: RecordLogItem) { + const recordItem = { + card: { + ...(recordLogItem.card as Card & { cid: string }), + due: recordLogItem.card.due.getTime(), + state: State[recordLogItem.card.state] as StateType, + last_review: recordLogItem.card.last_review + ? recordLogItem.card.last_review!.getTime() + : null, + }, + log: { + ...recordLogItem.log, + cid: (recordLogItem.card as Card & { cid: string }).cid, + due: recordLogItem.log.due.getTime(), + review: recordLogItem.log.review.getTime(), + state: State[recordLogItem.log.state] as StateType, + rating: Rating[recordLogItem.log.rating] as RatingType, + }, + } + return recordItem + } function forgetAfterHandler(recordLogItem: RecordLogItem): RepeatRecordLog { return { @@ -159,6 +180,31 @@ describe('afterHandler', () => { } }) + it('next[afterHandler]', () => { + const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler) + for (const grade of Grades) { + const next = f.next( + emptyCardFormAfterHandler, + now, + grade, + nextAfterHandler + ) + expect('card' in next).toEqual(true) + expect('log' in next).toEqual(true) + + expect(Number.isSafeInteger(next.card.due)).toEqual(true) + expect(typeof next.card.state === 'string').toEqual(true) + expect(Number.isSafeInteger(next.card.last_review)).toEqual(true) + + expect(Number.isSafeInteger(next.log.due)).toEqual(true) + expect(Number.isSafeInteger(next.log.review)).toEqual(true) + expect(typeof next.log.state === 'string').toEqual(true) + expect(typeof next.log.rating === 'string').toEqual(true) + expect(next.card.cid).toEqual('test001') + expect(next.log.cid).toEqual(next.card.cid) + } + }) + it('rollback[afterHandler]', () => { const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler) const repeatFormAfterHandler = f.repeat( diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index fb7ac2c..23eba43 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -4,6 +4,7 @@ import { CardInput, DateInput, FSRSParameters, + Grade, Rating, RecordLog, RecordLogItem, @@ -25,6 +26,7 @@ export class FSRS extends FSRSAlgorithm { } /** + * Display the collection of cards and logs for the four scenarios after scheduling the card at the current time. * @param card Card to be processed * @param now Current time or scheduled time * @param afterHandler Convert the result to another type. (Optional) @@ -96,6 +98,80 @@ export class FSRS extends FSRSAlgorithm { } } + /** + * Display the collection of cards and logs for the card scheduled at the current time, after applying a specific grade rating. + * @param card Card to be processed + * @param now Current time or scheduled time + * @param grade Rating of the review (Again, Hard, Good, Easy) + * @param afterHandler Convert the result to another type. (Optional) + * @example + * ``` + * const card: Card = createEmptyCard(new Date()); + * const f = fsrs(); + * const recordLogItem = f.next(card, new Date(), Rating.Again); + * ``` + * @example + * ``` + * interface RevLogUnchecked + * extends Omit { + * cid: string; + * due: Date | number; + * state: StateType; + * review: Date | number; + * rating: RatingType; + * } + * + * interface NextRecordLog { + * card: CardUnChecked; //see method: createEmptyCard + * log: RevLogUnchecked; + * } + * + function nextAfterHandler(recordLogItem: RecordLogItem) { + const recordItem = { + card: { + ...(recordLogItem.card as Card & { cid: string }), + due: recordLogItem.card.due.getTime(), + state: State[recordLogItem.card.state] as StateType, + last_review: recordLogItem.card.last_review + ? recordLogItem.card.last_review!.getTime() + : null, + }, + log: { + ...recordLogItem.log, + cid: (recordLogItem.card as Card & { cid: string }).cid, + due: recordLogItem.log.due.getTime(), + review: recordLogItem.log.review.getTime(), + state: State[recordLogItem.log.state] as StateType, + rating: Rating[recordLogItem.log.rating] as RatingType, + }, + }; + return recordItem + } + * const card: Card = createEmptyCard(new Date(), cardAfterHandler); //see method: createEmptyCard + * const f = fsrs(); + * const recordLogItem = f.repeat(card, new Date(), Rating.Again, nextAfterHandler); + * ``` + */ + next( + card: CardInput | Card, + now: DateInput, + grade: Grade, + afterHandler?: (recordLog: RecordLogItem) => R + ): R { + const Schduler = this.Schduler + const instace = new Schduler(card, now, this satisfies FSRSAlgorithm) + const g = TypeConvert.rating(grade) + if (g === Rating.Manual) { + throw new Error('Cannot review a manual rating') + } + const recordLogItem = instace.review(g) + if (afterHandler && typeof afterHandler === 'function') { + return afterHandler(recordLogItem) + } else { + return recordLogItem as R + } + } + /** * Get the retrievability of the card * @param card Card to be processed