From b4162cc5433e0b4c71f5298b60ece3223ce65302 Mon Sep 17 00:00:00 2001 From: ishiko Date: Tue, 28 Nov 2023 20:15:46 +0800 Subject: [PATCH] Test/coverage (#58) --- __tests__/FSRSV4.test.ts | 61 ++++++++++- __tests__/forget.test.ts | 27 ++++- __tests__/help.test.ts | 180 +++++++++++++++++++++++++++++++++ __tests__/help.tsts.ts | 5 - __tests__/show_diff_message.ts | 44 +++++++- jest.config.js | 5 + package.json | 3 +- src/fsrs/default.ts | 2 +- src/fsrs/help.ts | 22 +++- 9 files changed, 330 insertions(+), 19 deletions(-) create mode 100644 __tests__/help.test.ts delete mode 100644 __tests__/help.tsts.ts diff --git a/__tests__/FSRSV4.test.ts b/__tests__/FSRSV4.test.ts index b578324..295ddf6 100644 --- a/__tests__/FSRSV4.test.ts +++ b/__tests__/FSRSV4.test.ts @@ -6,8 +6,9 @@ import { createEmptyCard, State, Grade, - Grades, + Grades, default_request_retention, default_maximum_interval, default_enable_fuzz, default_w, } from "../src/fsrs"; +import { FSRSAlgorithm } from "../src/fsrs/algorithm"; describe("initial FSRS V4", () => { const params = generatorParameters(); @@ -38,6 +39,45 @@ describe("initial FSRS V4", () => { it("retrievability t=s ", () => { expect(Number(f.current_retrievability(5, 5).toFixed(2))).toEqual(0.9); }); + + it("default params",()=>{ + const expected_w = [ + 0.4, 0.6, 2.4, 5.8, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18, 0.05, + 0.34, 1.26, 0.29, 2.61, + ]; + expect(default_request_retention).toEqual(0.9); + expect(default_maximum_interval).toEqual(36500); + expect(default_enable_fuzz).toEqual(false) + expect(default_w).toEqual(expected_w); + expect(default_w.length).toBe(expected_w.length); + }) +}); + +describe("FSRS apply_fuzz", () => { + test("return original interval when fuzzing is disabled", () => { + const ivl = 3.0; + const enable_fuzz = false; + const algorithm = new FSRS({ enable_fuzz: enable_fuzz }); + expect(algorithm.apply_fuzz(ivl)).toBe(3); + }); + + test("return original interval when ivl is less than 2.5", () => { + const ivl = 2.0; + const enable_fuzz = true; + const algorithm = new FSRS({ enable_fuzz: enable_fuzz }); + expect(algorithm.apply_fuzz(ivl)).toBe(2); + }); + + test("return original interval when ivl is less than 2.5", () => { + const ivl = 2.5; + const enable_fuzz = true; + const algorithm = new FSRSAlgorithm({ enable_fuzz: enable_fuzz }); + const min_ivl = Math.max(2, Math.round(ivl * 0.95 - 1)); + const max_ivl = Math.round(ivl * 1.05 + 1); + const fuzzedInterval = algorithm.apply_fuzz(ivl); + expect(fuzzedInterval).toBeGreaterThanOrEqual(min_ivl); + expect(fuzzedInterval).toBeLessThanOrEqual(max_ivl); + }); }); // Ref: https://github.com/open-spaced-repetition/py-fsrs/blob/ecd68e453611eb808c7367c7a5312d7cadeedf5c/tests/test_fsrs.py#L1 @@ -135,3 +175,22 @@ describe("FSRS V4 AC by py-fsrs", () => { ]); }); }); + +describe("get retrievability", () => { + const fsrs = new FSRS({}); + test("return undefined for non-review cards", () => { + const card = createEmptyCard(); + const now = new Date(); + const expected = undefined; + expect(fsrs.get_retrievability(card, now)).toBe(expected); + }); + + test("return retrievability percentage for review cards", () => { + const card = createEmptyCard(); + const sc = fsrs.repeat(card, new Date()); + const r = [undefined, undefined, undefined, "100.00%"]; + Grades.forEach((grade,index) => { + expect(fsrs.get_retrievability(sc[grade].card, new Date())).toBe(r[index]); + }); + }); +}); diff --git a/__tests__/forget.test.ts b/__tests__/forget.test.ts index cc59163..a69d3d8 100644 --- a/__tests__/forget.test.ts +++ b/__tests__/forget.test.ts @@ -39,11 +39,7 @@ describe("FSRS forget", () => { ); } for (const grade of grades) { - const forgetCard = f.forget( - scheduling_cards[grade].card, - forget_now, - false, - ); + const forgetCard = f.forget(scheduling_cards[grade].card, forget_now); expect(forgetCard.card).toEqual({ ...card, due: forget_now, @@ -57,4 +53,25 @@ describe("FSRS forget", () => { ); } }); + + it("new card forget[reset true]", () => { + const card = createEmptyCard(); + const forget_now = new Date(2023, 11, 30, 12, 30, 0, 0); + const forgetCard = f.forget(card, forget_now, true); + expect(forgetCard.card).toEqual({ + ...card, + due: forget_now, + lapses: 0, + reps: 0, + }); + }); + it("new card forget[reset true]", () => { + const card = createEmptyCard(); + const forget_now = new Date(2023, 11, 30, 12, 30, 0, 0); + const forgetCard = f.forget(card, forget_now); + expect(forgetCard.card).toEqual({ + ...card, + due: forget_now, + }); + }); }); diff --git a/__tests__/help.test.ts b/__tests__/help.test.ts new file mode 100644 index 0000000..5369314 --- /dev/null +++ b/__tests__/help.test.ts @@ -0,0 +1,180 @@ +import { + date_diff, date_scheduler, + fixDate, + fixRating, + fixState, + formatDate, + Grades, + Rating, + State, +} from "../src/fsrs"; + +test("FSRS-Grades", () => { + expect(Grades).toStrictEqual([ + Rating.Again, + Rating.Hard, + Rating.Good, + Rating.Easy, + ]); +}); + +test("Date.prototype.format", () => { + const now = new Date(2022, 11, 30, 12, 30, 0, 0); + const last_review = new Date(2022, 11, 29, 12, 30, 0, 0); + expect(now.format()).toEqual("2022-12-30 12:30:00"); + expect(formatDate(now)).toEqual("2022-12-30 12:30:00"); + const TIMEUNITFORMAT_TEST = ["秒", "分", "时", "天", "月", "年"]; + expect(now.dueFormat(last_review)).toEqual("1"); + expect(now.dueFormat(last_review, true)).toEqual("1day"); + expect(now.dueFormat(last_review, true, TIMEUNITFORMAT_TEST)).toEqual("1天"); +}); + +describe("date_scheduler", () => { + test("offset by minutes", () => { + const now = new Date("2023-01-01T12:00:00Z"); + const t = 30; + const expected = new Date("2023-01-01T12:30:00Z"); + + expect(date_scheduler(now, t)).toEqual(expected); + }); + + test("offset by days", () => { + const now = new Date("2023-01-01T12:00:00Z"); + const t = 3; + const expected = new Date("2023-01-04T12:00:00Z"); + + expect(date_scheduler(now, t, true)).toEqual(expected); + }); + + test("negative offset", () => { + const now = new Date("2023-01-01T12:00:00Z"); + const t = -15; + const expected = new Date("2023-01-01T11:45:00Z"); + + expect(date_scheduler(now, t)).toEqual(expected); + }); + + test("offset with isDay parameter", () => { + const now = new Date("2023-01-01T12:00:00Z"); + const t = 2; + const expected = new Date("2023-01-03T12:00:00Z"); + + expect(date_scheduler(now, t, true)).toEqual(expected); + }); +}); + +describe("date_diff", () => { + test("wrong fix", () => { + const now = new Date(2022, 11, 30, 12, 30, 0, 0); + const last_review = new Date(2022, 11, 29, 12, 30, 0, 0); + + expect(() => date_diff(now, null as unknown as Date, "days")).toThrowError( + "Invalid date", + ); + expect(() => + date_diff(now, null as unknown as Date, "minutes"), + ).toThrowError("Invalid date"); + expect(() => + date_diff(null as unknown as Date, last_review, "days"), + ).toThrowError("Invalid date"); + expect(() => + date_diff(null as unknown as Date, last_review, "minutes"), + ).toThrowError("Invalid date"); + }); + + test("calculate difference in minutes", () => { + const now = new Date("2023-11-25T12:30:00Z"); + const pre = new Date("2023-11-25T12:00:00Z"); + const unit = "minutes"; + const expected = 30; + expect(date_diff(now, pre, unit)).toBe(expected); + }); + + test("calculate difference in minutes for negative time difference", () => { + const now = new Date("2023-11-25T12:00:00Z"); + const pre = new Date("2023-11-25T12:30:00Z"); + const unit = "minutes"; + const expected = -30; + expect(date_diff(now, pre, unit)).toBe(expected); + }); +}); + +describe("fixDate", () => { + test("throw error for invalid date value", () => { + const input = "invalid-date"; + expect(() => fixDate(input)).toThrowError("Invalid date:[invalid-date]"); + }); + + test("throw error for unsupported value type", () => { + const input = true; + expect(() => fixDate(input)).toThrowError("Invalid date:[true]"); + }); + + test("throw error for undefined value", () => { + const input = undefined; + expect(() => fixDate(input)).toThrowError("Invalid date:[undefined]"); + }); + + test("throw error for null value", () => { + const input = null; + expect(() => fixDate(input)).toThrowError("Invalid date:[null]"); + }); +}); + +describe("fixState", () => { + test("fix state value", () => { + const newState = "New"; + expect(fixState("new")).toEqual(State.New); + expect(fixState(newState)).toEqual(State.New); + + const learning = "Learning"; + expect(fixState("learning")).toEqual(State.Learning); + expect(fixState(learning)).toEqual(State.Learning); + + const relearning = "Relearning"; + expect(fixState("relearning")).toEqual(State.Relearning); + expect(fixState(relearning)).toEqual(State.Relearning); + + const review = "Review"; + expect(fixState("review")).toEqual(State.Review); + expect(fixState(review)).toEqual(State.Review); + }); + + test("throw error for invalid state value", () => { + const input = "invalid-state"; + expect(() => fixState(input)).toThrowError("Invalid state:[invalid-state]"); + expect(() => fixState(null)).toThrowError("Invalid state:[null]"); + expect(() => fixState(undefined)).toThrowError("Invalid state:[undefined]"); + }); +}); + +describe("fixRating", () => { + test("fix Rating value", () => { + const again = "Again"; + expect(fixRating("again")).toEqual(Rating.Again); + expect(fixRating(again)).toEqual(Rating.Again); + + const hard = "Hard"; + expect(fixRating("hard")).toEqual(Rating.Hard); + expect(fixRating(hard)).toEqual(Rating.Hard); + + const good = "Good"; + expect(fixRating("good")).toEqual(Rating.Good); + expect(fixRating(good)).toEqual(Rating.Good); + + const easy = "Easy"; + expect(fixRating("easy")).toEqual(Rating.Easy); + expect(fixRating(easy)).toEqual(Rating.Easy); + }); + + test("throw error for invalid rating value", () => { + const input = "invalid-rating"; + expect(() => fixRating(input)).toThrowError( + "Invalid rating:[invalid-rating]", + ); + expect(() => fixRating(null)).toThrowError("Invalid rating:[null]"); + expect(() => fixRating(undefined)).toThrowError( + "Invalid rating:[undefined]", + ); + }); +}); diff --git a/__tests__/help.tsts.ts b/__tests__/help.tsts.ts deleted file mode 100644 index 0875100..0000000 --- a/__tests__/help.tsts.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Grades, Rating } from "../src/fsrs"; - -test("FSRS-Grades", () => { - expect(Grades).toStrictEqual([Rating.Again, Rating.Hard, Rating.Good, Rating.Easy]); -}); diff --git a/__tests__/show_diff_message.ts b/__tests__/show_diff_message.ts index a1bf0e4..b117363 100644 --- a/__tests__/show_diff_message.ts +++ b/__tests__/show_diff_message.ts @@ -1,6 +1,7 @@ -import { show_diff_message } from "../src/fsrs"; +import {fixDate, show_diff_message} from "../src/fsrs"; test("show_diff_message_bad_type", () => { + const TIMEUNITFORMAT_TEST = ["秒", "分", "时", "天", "月", "年"]; //https://github.com/ishiko732/ts-fsrs/issues/19 const t1 = "1970-01-01T00:00:00.000Z"; const t2 = "1970-01-02T00:00:00.000Z"; @@ -13,42 +14,56 @@ test("show_diff_message_bad_type", () => { expect(show_diff_message(t2, t1)).toBe("1"); // @ts-ignore expect(show_diff_message(t2, t1, true)).toEqual("1day"); + expect(fixDate(t2).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("1天"); // @ts-ignore expect(show_diff_message(t4, t3)).toBe("1"); // @ts-ignore expect(show_diff_message(t4, t3, true)).toEqual("1day"); + expect(fixDate(t4).dueFormat(fixDate(t3),true,TIMEUNITFORMAT_TEST)).toEqual("1天"); // @ts-ignore expect(show_diff_message(t6, t5)).toBe("1"); // @ts-ignore expect(show_diff_message(t6, t5, true)).toEqual("1day"); + expect(fixDate(t6).dueFormat(fixDate(t5),true,TIMEUNITFORMAT_TEST)).toEqual("1天"); }); test("show_diff_message_min", () => { + const TIMEUNITFORMAT_TEST = ["秒", "分", "时", "天", "月", "年"]; //https://github.com/ishiko732/ts-fsrs/issues/19 const t1 = new Date(); const t2 = new Date(t1.getTime() + 1000 * 60); const t3 = new Date(t1.getTime() + 1000 * 60 * 59); expect(show_diff_message(t2, t1)).toBe("1"); expect(show_diff_message(t2, t1, true)).toEqual("1min"); + expect(fixDate(t2).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("1分"); + expect(show_diff_message(t3, t1, true)).toEqual("59min"); + expect(fixDate(t3).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("59分"); }); test("show_diff_message_hour", () => { + const TIMEUNITFORMAT_TEST = ["秒", "分", "小时", "天", "月", "年"]; //https://github.com/ishiko732/ts-fsrs/issues/19 const t1 = new Date(); const t2 = new Date(t1.getTime() + 1000 * 60 * 60); const t3 = new Date(t1.getTime() + 1000 * 60 * 60 * 59); expect(show_diff_message(t2, t1)).toBe("1"); + expect(show_diff_message(t2, t1, true)).toEqual("1hour"); + expect(fixDate(t2).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("1小时"); expect(show_diff_message(t3, t1, true)).not.toBe("59hour"); + expect(fixDate(t3).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).not.toEqual("59小时"); + expect(show_diff_message(t3, t1, true)).toBe("2day"); + expect(fixDate(t3).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("2天"); }); test("show_diff_message_day", () => { + const TIMEUNITFORMAT_TEST = ["秒", "分", "小时", "天", "个月", "年"]; //https://github.com/ishiko732/ts-fsrs/issues/19 const t1 = new Date(); const t2 = new Date(t1.getTime() + 1000 * 60 * 60 * 24); @@ -56,13 +71,19 @@ test("show_diff_message_day", () => { const t4 = new Date(t1.getTime() + 1000 * 60 * 60 * 24 * 31); expect(show_diff_message(t2, t1)).toBe("1"); expect(show_diff_message(t2, t1, true)).toEqual("1day"); + expect(fixDate(t2).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("1天"); + expect(show_diff_message(t3, t1)).toBe("30"); expect(show_diff_message(t3, t1, true)).toEqual("30day"); + expect(fixDate(t3).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("30天"); + expect(show_diff_message(t4, t1)).not.toBe("31"); expect(show_diff_message(t4, t1, true)).toEqual("1month"); + expect(fixDate(t4).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("1个月"); }); test("show_diff_message_month", () => { + const TIMEUNITFORMAT_TEST = ["秒", "分", "小时", "天", "个月", "年"]; //https://github.com/ishiko732/ts-fsrs/issues/19 const t1 = new Date(); const t2 = new Date(t1.getTime() + 1000 * 60 * 60 * 24 * 31); @@ -70,13 +91,19 @@ test("show_diff_message_month", () => { const t4 = new Date(t1.getTime() + 1000 * 60 * 60 * 24 * 31 * 13); expect(show_diff_message(t2, t1)).toBe("1"); expect(show_diff_message(t2, t1, true)).toEqual("1month"); + expect(fixDate(t2).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("1个月"); + expect(show_diff_message(t3, t1)).not.toBe("12"); expect(show_diff_message(t3, t1, true)).not.toEqual("12month"); + expect(fixDate(t3).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).not.toEqual("12个月"); + expect(show_diff_message(t4, t1)).not.toBe("13"); expect(show_diff_message(t4, t1, true)).toEqual("1year"); + expect(fixDate(t4).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("1年"); }); test("show_diff_message_year", () => { + const TIMEUNITFORMAT_TEST = ["秒", "分", "小时", "天", "个月", "年"]; //https://github.com/ishiko732/ts-fsrs/issues/19 const t1 = new Date(); const t2 = new Date(t1.getTime() + 1000 * 60 * 60 * 24 * 31 * 13); @@ -88,8 +115,23 @@ test("show_diff_message_year", () => { ); expect(show_diff_message(t2, t1)).toBe("1"); expect(show_diff_message(t2, t1, true)).toEqual("1year"); + expect(fixDate(t2).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("1年"); + expect(show_diff_message(t3, t1)).toBe("1"); expect(show_diff_message(t3, t1, true)).toEqual("1year"); + expect(fixDate(t3).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("1年"); + expect(show_diff_message(t4, t1)).toBe("2"); expect(show_diff_message(t4, t1, true)).toEqual("2year"); + expect(fixDate(t4).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("2年"); }); + +test("wrong timeUnit length", () => { + const TIMEUNITFORMAT_TEST = ["年"]; + const t1 = new Date(); + const t2 = new Date(t1.getTime() + 1000 * 60 * 60 * 24 * 31 * 13); + expect(show_diff_message(t2, t1)).toBe("1"); + expect(show_diff_message(t2, t1, true)).toEqual("1year"); + expect(fixDate(t2).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).not.toEqual("1年"); + expect(fixDate(t2).dueFormat(fixDate(t1),true,TIMEUNITFORMAT_TEST)).toEqual("1year"); +}); \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 356c1c3..8a6e035 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,4 +6,9 @@ export default { '**/__tests__/*.js?(x)', '**/__tests__/*.ts?(x)', ], + coverageThreshold: { + global: { + lines: 80, + }, + }, }; \ No newline at end of file diff --git a/package.json b/package.json index e5b85cd..c9303e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-fsrs", - "version": "3.1.2", + "version": "3.1.3", "description": "ts-fsrs is a TypeScript package used to implement the Free Spaced Repetition Scheduler (FSRS) algorithm. It helps developers apply FSRS to their flashcard applications, thereby improving the user learning experience.", "main": "dist/ts-fsrs.js", "module": "dist/ts-fsrs.mjs", @@ -44,6 +44,7 @@ "lint": "eslint --fix src/ && prettier --write src/", "dev": "rollup -c -w", "test": "jest --passWithNoTests", + "test:coverage": "jest --coverage", "prebuild": "rimraf ./dist", "build": "rollup --c ", "build:types": "tsc --project ./tsconfig.json --declaration true", diff --git a/src/fsrs/default.ts b/src/fsrs/default.ts index eccbf13..a870d34 100644 --- a/src/fsrs/default.ts +++ b/src/fsrs/default.ts @@ -9,7 +9,7 @@ export const default_w = [ ]; export const default_enable_fuzz = false; -export const FSRSVersion: string = "3.1.2"; +export const FSRSVersion: string = "3.1.3"; export const generatorParameters = ( props?: Partial, diff --git a/src/fsrs/help.ts b/src/fsrs/help.ts index c422e11..315af74 100644 --- a/src/fsrs/help.ts +++ b/src/fsrs/help.ts @@ -9,7 +9,7 @@ declare global { format(): string; - dueFormat(last_review: Date, unit?: boolean): string; + dueFormat(last_review: Date, unit?: boolean,timeUnit?: string[]): string; } } @@ -30,8 +30,8 @@ Date.prototype.format = function (): string { return formatDate(this); }; -Date.prototype.dueFormat = function (last_review: Date, unit?: boolean) { - return show_diff_message(this, last_review, unit); +Date.prototype.dueFormat = function (last_review: Date, unit?: boolean,timeUnit?: string[]) { + return show_diff_message(this, last_review, unit, timeUnit); }; /** @@ -128,7 +128,13 @@ export function fixDate(value: unknown) { export function fixState(value: unknown): State { if (typeof value === "string") { - return State[value as keyof typeof State]; + const firstLetter = value.charAt(0).toUpperCase(); + const restOfString = value.slice(1).toLowerCase(); + const ret= State[`${firstLetter}${restOfString}` as keyof typeof State] + if(ret === undefined){ + throw new Error(`Invalid state:[${value}]`); + } + return ret; } else if (typeof value === "number") { return value as State; } @@ -137,7 +143,13 @@ export function fixState(value: unknown): State { export function fixRating(value: unknown): Rating { if (typeof value === "string") { - return Rating[value as keyof typeof Rating]; + const firstLetter = value.charAt(0).toUpperCase(); + const restOfString = value.slice(1).toLowerCase(); + const ret = Rating[`${firstLetter}${restOfString}` as keyof typeof Rating] + if(ret === undefined){ + throw new Error(`Invalid rating:[${value}]`); + } + return ret; } else if (typeof value === "number") { return value as Rating; }