diff --git a/.prettierrc.json b/.prettierrc.json index 0e0dcd2..fbe0e55 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,3 +1,6 @@ { - + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true } \ No newline at end of file diff --git a/__tests__/FSRSV4.test.ts b/__tests__/FSRSV4.test.ts index 0101350..8deb837 100644 --- a/__tests__/FSRSV4.test.ts +++ b/__tests__/FSRSV4.test.ts @@ -6,22 +6,22 @@ import { State, Grade, Grades, -} from "../src/fsrs"; +} from '../src/fsrs' // Ref: https://github.com/open-spaced-repetition/py-fsrs/blob/ecd68e453611eb808c7367c7a5312d7cadeedf5c/tests/test_fsrs.py#L1 -describe("FSRS V4 AC by py-fsrs", () => { +describe('FSRS V4 AC by py-fsrs', () => { const f: FSRS = fsrs({ w: [ 1.14, 1.01, 5.44, 14.67, 5.3024, 1.5662, 1.2503, 0.0028, 1.5489, 0.1763, 0.9953, 2.7473, 0.0179, 0.3105, 0.3976, 0.0, 2.0902, ], enable_fuzz: false, - }); - const grade: Grade[] = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy]; - it("ivl_history", () => { - let card = createEmptyCard(); - let now = new Date(2022, 11, 29, 12, 30, 0, 0); - let scheduling_cards = f.repeat(card, now); + }) + const grade: Grade[] = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy] + it('ivl_history', () => { + let card = createEmptyCard() + let now = new Date(2022, 11, 29, 12, 30, 0, 0) + let scheduling_cards = f.repeat(card, now) const ratings: Grade[] = [ Rating.Good, Rating.Good, @@ -36,95 +36,95 @@ describe("FSRS V4 AC by py-fsrs", () => { Rating.Good, Rating.Good, Rating.Good, - ]; - const ivl_history: number[] = []; + ] + const ivl_history: number[] = [] for (const rating of ratings) { for (const check of grade) { const rollbackCard = f.rollback( scheduling_cards[check].card, - scheduling_cards[check].log, - ); - expect(rollbackCard).toEqual(card); + scheduling_cards[check].log + ) + expect(rollbackCard).toEqual(card) expect(scheduling_cards[check].log.elapsed_days).toEqual( - card.last_review ? now.diff(card.last_review as Date, "days") : 0, - ); + card.last_review ? now.diff(card.last_review as Date, 'days') : 0 + ) } - card = scheduling_cards[rating].card; - const ivl = card.scheduled_days; - ivl_history.push(ivl); - now = card.due; - scheduling_cards = f.repeat(card, now); + card = scheduling_cards[rating].card + const ivl = card.scheduled_days + ivl_history.push(ivl) + now = card.due + scheduling_cards = f.repeat(card, now) } expect(ivl_history).toEqual([ 0, 5, 16, 43, 106, 236, 0, 0, 12, 25, 47, 85, 147, - ]); - }); + ]) + }) - it("first repeat", () => { - const card = createEmptyCard(); - const now = new Date(2022, 11, 29, 12, 30, 0, 0); - const scheduling_cards = f.repeat(card, now); + it('first repeat', () => { + const card = createEmptyCard() + const now = new Date(2022, 11, 29, 12, 30, 0, 0) + const scheduling_cards = f.repeat(card, now) const grades: Grade[] = [ Rating.Again, Rating.Hard, Rating.Good, Rating.Easy, - ]; + ] - const stability: number[] = []; - const difficulty: number[] = []; - const elapsed_days: number[] = []; - const scheduled_days: number[] = []; - const reps: number[] = []; - const lapses: number[] = []; - const states: State[] = []; + const stability: number[] = [] + const difficulty: number[] = [] + const elapsed_days: number[] = [] + const scheduled_days: number[] = [] + const reps: number[] = [] + const lapses: number[] = [] + const states: State[] = [] for (const rating of grades) { - const first_card = scheduling_cards[rating].card; - stability.push(first_card.stability); - difficulty.push(first_card.difficulty); - reps.push(first_card.reps); - lapses.push(first_card.lapses); - elapsed_days.push(first_card.elapsed_days); - scheduled_days.push(first_card.scheduled_days); - states.push(first_card.state); + const first_card = scheduling_cards[rating].card + stability.push(first_card.stability) + difficulty.push(first_card.difficulty) + reps.push(first_card.reps) + lapses.push(first_card.lapses) + elapsed_days.push(first_card.elapsed_days) + scheduled_days.push(first_card.scheduled_days) + states.push(first_card.state) } - expect(stability).toEqual([1.14, 1.01, 5.44, 14.67]); - expect(difficulty).toEqual([8.4348, 6.8686, 5.3024, 3.7362]); - expect(reps).toEqual([1, 1, 1, 1]); - expect(lapses).toEqual([0, 0, 0, 0]); - expect(elapsed_days).toEqual([0, 0, 0, 0]); - expect(scheduled_days).toEqual([0, 0, 0, 15]); + expect(stability).toEqual([1.14, 1.01, 5.44, 14.67]) + expect(difficulty).toEqual([8.4348, 6.8686, 5.3024, 3.7362]) + expect(reps).toEqual([1, 1, 1, 1]) + expect(lapses).toEqual([0, 0, 0, 0]) + expect(elapsed_days).toEqual([0, 0, 0, 0]) + expect(scheduled_days).toEqual([0, 0, 0, 15]) expect(states).toEqual([ State.Learning, State.Learning, State.Learning, State.Review, - ]); - }); -}); + ]) + }) +}) -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); - }); +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("2023-12-01 04:00:00"); - const sc = fsrs.repeat(card, "2023-12-01 04:05:00"); - const r = [undefined, undefined, undefined, "90.00%"]; - const r_number = [undefined, undefined, undefined, 0.9]; + test('return retrievability percentage for review cards', () => { + const card = createEmptyCard('2023-12-01 04:00:00') + const sc = fsrs.repeat(card, '2023-12-01 04:05:00') + const r = [undefined, undefined, undefined, '90.00%'] + const r_number = [undefined, undefined, undefined, 0.9] Grades.forEach((grade, index) => { expect(fsrs.get_retrievability(sc[grade].card, sc[grade].card.due)).toBe( - r[index], - ); + r[index] + ) expect( - fsrs.get_retrievability(sc[grade].card, sc[grade].card.due, false), - ).toBe(r_number[index]); - }); - }); -}); + fsrs.get_retrievability(sc[grade].card, sc[grade].card.due, false) + ).toBe(r_number[index]) + }) + }) +}) diff --git a/__tests__/algorithm.test.ts b/__tests__/algorithm.test.ts index 00f0ca8..bfca2e1 100644 --- a/__tests__/algorithm.test.ts +++ b/__tests__/algorithm.test.ts @@ -12,148 +12,148 @@ import { get_fuzz_range, Grades, Rating, -} from "../src/fsrs"; -import Decimal from "decimal.js"; +} from '../src/fsrs' +import Decimal from 'decimal.js' -describe("FACTOR[DECAY = -0.5]", () => { - it("FACTOR", () => { - expect(19 / 81).toEqual(new Decimal(19).div(81).toNumber()); - expect(FACTOR).toEqual(new Decimal(19).div(81).toNumber()); +describe('FACTOR[DECAY = -0.5]', () => { + it('FACTOR', () => { + expect(19 / 81).toEqual(new Decimal(19).div(81).toNumber()) + expect(FACTOR).toEqual(new Decimal(19).div(81).toNumber()) expect(FACTOR).toEqual( - new Decimal(0.9).pow(new Decimal(1).div(DECAY)).sub(1).toNumber(), - ); + new Decimal(0.9).pow(new Decimal(1).div(DECAY)).sub(1).toNumber() + ) expect(new Decimal(19).div(81).toNumber()).toEqual( - new Decimal(0.9).pow(new Decimal(1).div(DECAY)).sub(1).toNumber(), - ); - }); -}); + new Decimal(0.9).pow(new Decimal(1).div(DECAY)).sub(1).toNumber() + ) + }) +}) -describe("forgetting_curve", () => { - const params = generatorParameters(); +describe('forgetting_curve', () => { + const params = generatorParameters() //w=[ // 0.5701, 1.4436, 4.1386, 10.9355, 5.1443, 1.2006, 0.8627, 0.0362, 1.629, // 0.1342, 1.0166, 2.1174, 0.0839, 0.3204, 1.4676, 0.219, 2.8237, // ]; - const algorithm: FSRSAlgorithm = new FSRSAlgorithm(params); + const algorithm: FSRSAlgorithm = new FSRSAlgorithm(params) function forgetting_curve(elapsed_days: number, stability: number): number { return +new Decimal( new Decimal(1) .add(new Decimal(FACTOR).mul(elapsed_days).div(stability)) - .pow(DECAY), - ).toFixed(8); + .pow(DECAY) + ).toFixed(8) } - const delta_t = [0, 1, 2, 3, 4, 5]; - const s = [1.0, 2.0, 3.0, 4.0, 4.0, 2.0]; - const collection: number[] = []; - const expected: number[] = []; - it("retrievability", () => { + const delta_t = [0, 1, 2, 3, 4, 5] + const s = [1.0, 2.0, 3.0, 4.0, 4.0, 2.0] + const collection: number[] = [] + const expected: number[] = [] + it('retrievability', () => { for (let i = 0; i < delta_t.length; i++) { - collection.push(algorithm.forgetting_curve(delta_t[i], s[i])); - expected.push(forgetting_curve(delta_t[i], s[i])); + collection.push(algorithm.forgetting_curve(delta_t[i], s[i])) + expected.push(forgetting_curve(delta_t[i], s[i])) } - expect(collection).toEqual(expected); + expect(collection).toEqual(expected) expect(collection).toEqual([ 1.0, 0.946059, 0.9299294, 0.92216794, 0.9, 0.79394596, - ]); - }); -}); + ]) + }) +}) -describe("init_ds", () => { - const params = generatorParameters(); +describe('init_ds', () => { + const params = generatorParameters() //w=[ // 0.5701, 1.4436, 4.1386, 10.9355, 5.1443, 1.2006, 0.8627, 0.0362, 1.629, // 0.1342, 1.0166, 2.1174, 0.0839, 0.3204, 1.4676, 0.219, 2.8237, // ]; - const algorithm: FSRSAlgorithm = new FSRSAlgorithm(params); - it("initial stability ", () => { - const collection: number[] = []; + const algorithm: FSRSAlgorithm = new FSRSAlgorithm(params) + it('initial stability ', () => { + const collection: number[] = [] Grades.forEach((grade) => { - const s = algorithm.init_stability(grade); - collection.push(s); - }); + const s = algorithm.init_stability(grade) + collection.push(s) + }) expect(collection).toEqual([ params.w[0], params.w[1], params.w[2], params.w[3], - ]); - }); - it("initial difficulty ", () => { - const collection: number[] = []; - const expected: number[] = []; + ]) + }) + it('initial difficulty ', () => { + const collection: number[] = [] + const expected: number[] = [] Grades.forEach((grade) => { - const d = algorithm.init_difficulty(grade); - collection.push(d); + const d = algorithm.init_difficulty(grade) + collection.push(d) expected.push( new Decimal(params.w[4]) .sub(new Decimal(grade - 3).mul(new Decimal(params.w[5]))) - .toNumber(), - ); - }); - expect(collection).toEqual(expected); + .toNumber() + ) + }) + expect(collection).toEqual(expected) // again: w[4]-w[5]*(-2) // hard: w[4]-w[5]*(-1) // good: w[4]-w[5]*(0) // easy: w[4]-w[5]*(1) - }); -}); + }) +}) -describe("next_ds", () => { - const params = generatorParameters(); +describe('next_ds', () => { + const params = generatorParameters() //w=[ // 0.5701, 1.4436, 4.1386, 10.9355, 5.1443, 1.2006, 0.8627, 0.0362, 1.629, // 0.1342, 1.0166, 2.1174, 0.0839, 0.3204, 1.4676, 0.219, 2.8237, // ]; - const algorithm: FSRSAlgorithm = new FSRSAlgorithm(params); - it("next_difficulty", () => { + const algorithm: FSRSAlgorithm = new FSRSAlgorithm(params) + it('next_difficulty', () => { function next_d(d: number, g: number) { function mean_reversion(init: number, current: number): number { - const f1 = new Decimal(params.w[7]).mul(init); - const f2 = new Decimal(1).sub(new Decimal(params.w[7])).mul(current); - return f1.add(f2).toNumber(); + const f1 = new Decimal(params.w[7]).mul(init) + const f2 = new Decimal(1).sub(new Decimal(params.w[7])).mul(current) + return f1.add(f2).toNumber() } function constrain_difficulty(difficulty: number): number { - return Math.min(Math.max(+new Decimal(difficulty).toFixed(8), 1), 10); + return Math.min(Math.max(+new Decimal(difficulty).toFixed(8), 1), 10) } const next_d = new Decimal(d) .sub(new Decimal(params.w[6]).mul(new Decimal(g - 3))) - .toNumber(); - return constrain_difficulty(mean_reversion(params.w[4], next_d)); + .toNumber() + return constrain_difficulty(mean_reversion(params.w[4], next_d)) } - const collection: number[] = []; - const expected: number[] = []; + const collection: number[] = [] + const expected: number[] = [] Grades.forEach((grade) => { - const d = algorithm.next_difficulty(5.0, grade); - const expected_d = next_d(5.0, grade); - collection.push(d); - expected.push(expected_d); - }); - expect(collection).toEqual([6.66816418, 5.83669392, 5.00522366, 4.1737534]); - expect(collection).toEqual(expected); - }); + const d = algorithm.next_difficulty(5.0, grade) + const expected_d = next_d(5.0, grade) + collection.push(d) + expected.push(expected_d) + }) + expect(collection).toEqual([6.66816418, 5.83669392, 5.00522366, 4.1737534]) + expect(collection).toEqual(expected) + }) - it("next_stability", () => { + it('next_stability', () => { function next_forget_stability(d: number, s: number, r: number): number { return +new Decimal(params.w[11]) .mul(new Decimal(d).pow(-params.w[12])) .mul(new Decimal(s + 1).pow(params.w[13]).sub(1)) .mul(new Decimal(Math.exp((1 - r) * params.w[14]))) - .toFixed(8); + .toFixed(8) } function next_recall_stability( d: number, s: number, r: number, - g: number, + g: number ): number { - const hard_penalty = Rating.Hard === g ? params.w[15] : 1; - const easy_bound = Rating.Easy === g ? params.w[16] : 1; + const hard_penalty = Rating.Hard === g ? params.w[15] : 1 + const easy_bound = Rating.Easy === g ? params.w[16] : 1 return +new Decimal(s) .mul( new Decimal(1).add( @@ -165,194 +165,193 @@ describe("next_ds", () => { new Decimal(params.w[10]) .mul(new Decimal(1).sub(r)) .exp() - .sub(1), + .sub(1) ) .mul(hard_penalty) - .mul(easy_bound), - ), + .mul(easy_bound) + ) ) - .toFixed(8); + .toFixed(8) } function next_s(d: number, s: number, r: number, g: number) { if (g < 1 || g > 4) { - throw new Error("Invalid grade"); + throw new Error('Invalid grade') } else if (g === Rating.Again) { - return next_forget_stability(d, s, r); + return next_forget_stability(d, s, r) } else { - return next_recall_stability(d, s, r, g); + return next_recall_stability(d, s, r, g) } } - const s_recall_collection: number[] = []; - const s_fail_collection: number[] = []; - const next_s_collection: number[] = []; - const expected_s_recall: number[] = []; - const expected_s_fail: number[] = []; - const expected_next_s: number[] = []; + const s_recall_collection: number[] = [] + const s_fail_collection: number[] = [] + const next_s_collection: number[] = [] + const expected_s_recall: number[] = [] + const expected_s_fail: number[] = [] + const expected_next_s: number[] = [] - const s = [5, 5, 5, 5]; - const d = [1, 2, 3, 4]; - const r = [0.9, 0.8, 0.7, 0.6]; + const s = [5, 5, 5, 5] + const d = [1, 2, 3, 4] + const r = [0.9, 0.8, 0.7, 0.6] Grades.forEach((grade, index) => { const s_recall = algorithm.next_recall_stability( d[index], s[index], r[index], - grade, - ); + grade + ) const s_fail = algorithm.next_forget_stability( d[index], s[index], - r[index], - ); - s_recall_collection.push(s_recall); - s_fail_collection.push(s_fail); - expected_s_fail.push(next_forget_stability(d[index], s[index], r[index])); + r[index] + ) + s_recall_collection.push(s_recall) + s_fail_collection.push(s_fail) + expected_s_fail.push(next_forget_stability(d[index], s[index], r[index])) expected_s_recall.push( - next_recall_stability(d[index], s[index], r[index], grade), - ); + next_recall_stability(d[index], s[index], r[index], grade) + ) if (grade === Rating.Again) { - next_s_collection.push(s_fail); + next_s_collection.push(s_fail) } else { - next_s_collection.push(s_recall); + next_s_collection.push(s_recall) } - expected_next_s.push(next_s(d[index], s[index], r[index], grade)); - }); + expected_next_s.push(next_s(d[index], s[index], r[index], grade)) + }) expect(s_recall_collection).toEqual([ 26.98093855, 14.12848781, 63.60068241, 208.72742276, - ]); - expect(s_recall_collection).toEqual(expected_s_recall); + ]) + expect(s_recall_collection).toEqual(expected_s_recall) expect(s_fail_collection).toEqual([ 1.9016012, 2.0777825, 2.3257503, 2.62916465, - ]); - expect(s_fail_collection).toEqual(expected_s_fail); + ]) + expect(s_fail_collection).toEqual(expected_s_fail) expect(next_s_collection).toEqual([ 1.9016012, 14.12848781, 63.60068241, 208.72742276, - ]); - expect(next_s_collection).toEqual(expected_next_s); - }); -}); + ]) + expect(next_s_collection).toEqual(expected_next_s) + }) +}) -describe("next_interval", () => { - it("next_ivl", () => { +describe('next_interval', () => { + it('next_ivl', () => { const desired_retentions: number[] = Array.from( { length: 10 }, - (_, i) => (i + 1) / 10, - ); + (_, i) => (i + 1) / 10 + ) const intervals: number[] = desired_retentions.map((r) => - fsrs({ request_retention: r }).next_interval(1.0, 0, false), - ); - expect(intervals).toEqual([422, 102, 43, 22, 13, 8, 4, 2, 1, 1]); - }); + fsrs({ request_retention: r }).next_interval(1.0, 0, false) + ) + expect(intervals).toEqual([422, 102, 43, 22, 13, 8, 4, 2, 1, 1]) + }) // https://github.com/open-spaced-repetition/ts-fsrs/pull/74 - it("next_ivl[max_limit]", () => { - const params = generatorParameters({ maximum_interval: 365 }); + it('next_ivl[max_limit]', () => { + const params = generatorParameters({ maximum_interval: 365 }) const intervalModifier = - (Math.pow(params.request_retention, 1 / DECAY) - 1) / FACTOR; - const f: FSRS = fsrs(params); + (Math.pow(params.request_retention, 1 / DECAY) - 1) / FACTOR + const f: FSRS = fsrs(params) - const s = 737.47; - const next_ivl = f.next_interval(s, 0, false); - expect(next_ivl).toEqual(params.maximum_interval); + const s = 737.47 + const next_ivl = f.next_interval(s, 0, false) + expect(next_ivl).toEqual(params.maximum_interval) - const t_fuzz = 98; - const next_ivl_fuzz = f.next_interval(s, t_fuzz, true); + const t_fuzz = 98 + const next_ivl_fuzz = f.next_interval(s, t_fuzz, true) const { min_ivl, max_ivl } = get_fuzz_range( Math.round(s * intervalModifier), t_fuzz, - params.maximum_interval, - ); - expect(next_ivl_fuzz).toBeGreaterThanOrEqual(min_ivl); - expect(max_ivl).toBe(params.maximum_interval); - expect(next_ivl_fuzz).toBeLessThanOrEqual(max_ivl); - }); -}); + params.maximum_interval + ) + expect(next_ivl_fuzz).toBeGreaterThanOrEqual(min_ivl) + expect(max_ivl).toBe(params.maximum_interval) + expect(next_ivl_fuzz).toBeLessThanOrEqual(max_ivl) + }) +}) -describe("FSRS apply_fuzz", () => { - test("return original interval when fuzzing is disabled", () => { - const ivl = 3.2; - const enable_fuzz = false; - const algorithm = new FSRS({ enable_fuzz: enable_fuzz }); - expect(algorithm.apply_fuzz(ivl, 0)).toBe(3); - }); +describe('FSRS apply_fuzz', () => { + test('return original interval when fuzzing is disabled', () => { + const ivl = 3.2 + const enable_fuzz = false + const algorithm = new FSRS({ enable_fuzz: enable_fuzz }) + expect(algorithm.apply_fuzz(ivl, 0)).toBe(3) + }) - test("return original interval when ivl is less than 2.5", () => { - const ivl = 2.3; - const enable_fuzz = true; - const algorithm = new FSRS({ enable_fuzz: enable_fuzz }); - expect(algorithm.apply_fuzz(ivl, 0)).toBe(2); - }); + test('return original interval when ivl is less than 2.5', () => { + const ivl = 2.3 + const enable_fuzz = true + const algorithm = new FSRS({ enable_fuzz: enable_fuzz }) + expect(algorithm.apply_fuzz(ivl, 0)).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 }); + 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, max_ivl } = get_fuzz_range( Math.round(2.5), 0, - default_maximum_interval, - ); - const fuzzedInterval = algorithm.apply_fuzz(ivl, 0); - expect(fuzzedInterval).toBeGreaterThanOrEqual(min_ivl); - expect(fuzzedInterval).toBeLessThanOrEqual(max_ivl); - }); -}); + default_maximum_interval + ) + const fuzzedInterval = algorithm.apply_fuzz(ivl, 0) + expect(fuzzedInterval).toBeGreaterThanOrEqual(min_ivl) + expect(fuzzedInterval).toBeLessThanOrEqual(max_ivl) + }) +}) -describe("change Params", () => { - test("change FSRSParameters", () => { - const f = fsrs(); +describe('change Params', () => { + test('change FSRSParameters', () => { + const f = fsrs() // I(r,s),r=0.9 then I(r,s)=s - expect(f.interval_modifier).toEqual(1); - expect(f.parameters).toEqual(generatorParameters()); + expect(f.interval_modifier).toEqual(1) + expect(f.parameters).toEqual(generatorParameters()) - const request_retention = 0.8; + const request_retention = 0.8 const update_w = [ 1.14, 1.01, 5.44, 14.67, 5.3024, 1.5662, 1.2503, 0.0028, 1.5489, 0.1763, 0.9953, 2.7473, 0.0179, 0.3105, 0.3976, 0.0, 2.0902, - ]; + ] f.parameters = generatorParameters({ request_retention: request_retention, w: update_w, enable_fuzz: true, - }); - expect(f.parameters.request_retention).toEqual(request_retention); - expect(f.parameters.w).toEqual(update_w); - expect(f.parameters.enable_fuzz).toEqual(true); + }) + expect(f.parameters.request_retention).toEqual(request_retention) + expect(f.parameters.w).toEqual(update_w) + expect(f.parameters.enable_fuzz).toEqual(true) expect(f.interval_modifier).toEqual( - f.calculate_interval_modifier(request_retention), - ); + f.calculate_interval_modifier(request_retention) + ) - f.parameters.request_retention = default_request_retention; + f.parameters.request_retention = default_request_retention expect(f.interval_modifier).toEqual( - f.calculate_interval_modifier(default_request_retention), - ); + f.calculate_interval_modifier(default_request_retention) + ) - f.parameters.w = default_w; - expect(f.parameters.w).toEqual(default_w); + f.parameters.w = default_w + expect(f.parameters.w).toEqual(default_w) - f.parameters.maximum_interval = 365; - expect(f.parameters.maximum_interval).toEqual(365); + f.parameters.maximum_interval = 365 + expect(f.parameters.maximum_interval).toEqual(365) - f.parameters.enable_fuzz = default_enable_fuzz; - expect(f.parameters.enable_fuzz).toEqual(default_enable_fuzz); + f.parameters.enable_fuzz = default_enable_fuzz + expect(f.parameters.enable_fuzz).toEqual(default_enable_fuzz) f.parameters = {} // check default values - expect(f.parameters).toEqual(generatorParameters()); - - }); + expect(f.parameters).toEqual(generatorParameters()) + }) - test("calculate_interval_modifier", () => { - const f = new FSRSAlgorithm(generatorParameters()); + test('calculate_interval_modifier', () => { + const f = new FSRSAlgorithm(generatorParameters()) expect(f.interval_modifier).toEqual( - f.calculate_interval_modifier(default_request_retention), - ); + f.calculate_interval_modifier(default_request_retention) + ) expect(() => { - f.parameters.request_retention = 1.2; - }).toThrow("Requested retention rate should be in the range (0,1]"); + f.parameters.request_retention = 1.2 + }).toThrow('Requested retention rate should be in the range (0,1]') expect(() => { - f.parameters.request_retention = -0.2; - }).toThrow("Requested retention rate should be in the range (0,1]"); - }); -}); + f.parameters.request_retention = -0.2 + }).toThrow('Requested retention rate should be in the range (0,1]') + }) +}) diff --git a/__tests__/default.test.ts b/__tests__/default.test.ts index e665996..6ef2c7e 100644 --- a/__tests__/default.test.ts +++ b/__tests__/default.test.ts @@ -5,48 +5,47 @@ import { default_request_retention, default_w, generatorParameters, -} from "../src/fsrs"; +} from '../src/fsrs' -describe("default params", () => { +describe('default params', () => { const expected_w = [ 0.5701, 1.4436, 4.1386, 10.9355, 5.1443, 1.2006, 0.8627, 0.0362, 1.629, 0.1342, 1.0166, 2.1174, 0.0839, 0.3204, 1.4676, 0.219, 2.8237, - ]; - expect(default_request_retention).toEqual(0.9); - expect(default_maximum_interval).toEqual(36500); - expect(default_enable_fuzz).toEqual(false); - expect(default_w.length).toBe(expected_w.length); - expect(default_w).toEqual(expected_w); + ] + expect(default_request_retention).toEqual(0.9) + expect(default_maximum_interval).toEqual(36500) + expect(default_enable_fuzz).toEqual(false) + expect(default_w.length).toBe(expected_w.length) + expect(default_w).toEqual(expected_w) - const params = generatorParameters(); - it("default_request_retention", () => { - expect(params.request_retention).toEqual(default_request_retention); - }); - it("default_maximum_interval", () => { - expect(params.maximum_interval).toEqual(default_maximum_interval); - }); - it("default_w ", () => { - expect(params.w).toEqual(expected_w); - }); - it("default_enable_fuzz ", () => { - expect(params.enable_fuzz).toEqual(default_enable_fuzz); - }); -}); + const params = generatorParameters() + it('default_request_retention', () => { + expect(params.request_retention).toEqual(default_request_retention) + }) + it('default_maximum_interval', () => { + expect(params.maximum_interval).toEqual(default_maximum_interval) + }) + it('default_w ', () => { + expect(params.w).toEqual(expected_w) + }) + it('default_enable_fuzz ', () => { + expect(params.enable_fuzz).toEqual(default_enable_fuzz) + }) +}) -describe("default Card", () => { - it("empty card", () => { - const time = [new Date(), new Date("2023-10-3 00:00:00")]; - for(const now of time){ - const card = createEmptyCard(now); - expect(card.due).toEqual(now); - expect(card.stability).toEqual(0); - expect(card.difficulty).toEqual(0); - expect(card.elapsed_days).toEqual(0); - expect(card.scheduled_days).toEqual(0); - expect(card.reps).toEqual(0); - expect(card.lapses).toEqual(0); - expect(card.state).toEqual(0); +describe('default Card', () => { + it('empty card', () => { + const time = [new Date(), new Date('2023-10-3 00:00:00')] + for (const now of time) { + const card = createEmptyCard(now) + expect(card.due).toEqual(now) + expect(card.stability).toEqual(0) + expect(card.difficulty).toEqual(0) + expect(card.elapsed_days).toEqual(0) + expect(card.scheduled_days).toEqual(0) + expect(card.reps).toEqual(0) + expect(card.lapses).toEqual(0) + expect(card.state).toEqual(0) } - }); - -}); + }) +}) diff --git a/__tests__/elapsed_days.test.ts b/__tests__/elapsed_days.test.ts index 1368c28..e68d0cd 100644 --- a/__tests__/elapsed_days.test.ts +++ b/__tests__/elapsed_days.test.ts @@ -1,74 +1,82 @@ // Ref:https://github.com/ishiko732/ts-fsrs/issues/44 -import { fsrs, FSRS, createEmptyCard, Rating, Grade } from "../src/fsrs"; +import { + fsrs, + FSRS, + createEmptyCard, + Rating, + Grade, + ReviewLog, +} from '../src/fsrs' -describe("elapsed_days", () => { - const f: FSRS = fsrs(); +describe('elapsed_days', () => { + const f: FSRS = fsrs() - const createDue = new Date(Date.UTC(2023, 9, 18, 14, 32, 3, 370)); - const grades: Grade[] = [ - Rating.Good, - Rating.Again, - Rating.Again, - Rating.Good, - ]; - let currentLog = null; - let index = 0; - let card = createEmptyCard(createDue); - test("first repeat[Rating.Good]", () => { - const firstDue = new Date(Date.UTC(2023, 10, 5, 8, 27, 2, 605)); - const sc = f.repeat(card, firstDue); - currentLog = sc[grades[index]].log; + const createDue = new Date(Date.UTC(2023, 9, 18, 14, 32, 3, 370)) + const grades: Grade[] = [Rating.Good, Rating.Again, Rating.Again, Rating.Good] + let currentLog: ReviewLog | null = null + let index = 0 + let card = createEmptyCard(createDue) + test('first repeat[Rating.Good]', () => { + const firstDue = new Date(Date.UTC(2023, 10, 5, 8, 27, 2, 605)) + const sc = f.repeat(card, firstDue) + currentLog = sc[grades[index]].log - expect(currentLog.elapsed_days).toEqual(0); + expect(currentLog.elapsed_days).toEqual(0) // console.log(sc[grades[index]].log) - card = sc[grades[index]].card; + card = sc[grades[index]].card // console.log(card) - index += 1; - }); + index += 1 + }) - test("second repeat[Rating.Again]", () => { + test('second repeat[Rating.Again]', () => { // 2023-11-08 15:02:09.791,4.93,2023-11-05 08:27:02.605 - const secondDue = new Date(Date.UTC(2023, 10, 8, 15, 2, 9, 791)); + const secondDue = new Date(Date.UTC(2023, 10, 8, 15, 2, 9, 791)) expect(card).not.toBeNull() - const sc = f.repeat(card, secondDue); + const sc = f.repeat(card, secondDue) - currentLog = sc[grades[index]].log; - expect(currentLog.elapsed_days).toEqual(secondDue.diff(card.last_review as Date,'days')); // 3 - expect(currentLog.elapsed_days).toEqual(3); // 0 - card = sc[grades[index]].card; + currentLog = sc[grades[index]].log + expect(currentLog.elapsed_days).toEqual( + secondDue.diff(card.last_review as Date, 'days') + ) // 3 + expect(currentLog.elapsed_days).toEqual(3) // 0 + card = sc[grades[index]].card // console.log(card) // console.log(currentLog) - index += 1; - }); + index += 1 + }) - test("third repeat[Rating.Again]", () => { + test('third repeat[Rating.Again]', () => { // 2023-11-08 15:02:30.799,4.93,2023-11-08 15:02:09.791 - const secondDue = new Date(Date.UTC(2023, 10, 8, 15, 2, 30, 799)); + const secondDue = new Date(Date.UTC(2023, 10, 8, 15, 2, 30, 799)) expect(card).not.toBeNull() - const sc = f.repeat(card, secondDue); + const sc = f.repeat(card, secondDue) - currentLog = sc[grades[index]].log; - expect(currentLog.elapsed_days).toEqual(secondDue.diff(card.last_review as Date,'days')); // 0 - expect(currentLog.elapsed_days).toEqual(0); // 0 + currentLog = sc[grades[index]].log + expect(currentLog.elapsed_days).toEqual( + secondDue.diff(card.last_review as Date, 'days') + ) // 0 + expect(currentLog.elapsed_days).toEqual(0) // 0 // console.log(currentLog); - card = sc[grades[index]].card; + card = sc[grades[index]].card // console.log(card); - index += 1; - }); + index += 1 + }) - test("fourth repeat[Rating.Good]", () => { + test('fourth repeat[Rating.Good]', () => { // 2023-11-08 15:04:08.739,4.93,2023-11-08 15:02:30.799 - const secondDue = new Date(Date.UTC(2023, 10, 8, 15, 4, 8, 739)); + const secondDue = new Date(Date.UTC(2023, 10, 8, 15, 4, 8, 739)) expect(card).not.toBeNull() - const sc = f.repeat(card, secondDue); + const sc = f.repeat(card, secondDue) - currentLog = sc[grades[index]].log; - expect(currentLog.elapsed_days).toEqual(secondDue.diff(card.last_review as Date,'days')); // 0 - expect(currentLog.elapsed_days).toEqual(0); // 0 + currentLog = sc[grades[index]].log + expect(currentLog.elapsed_days).toEqual( + secondDue.diff(card.last_review as Date, 'days') + ) // 0 + expect(currentLog.elapsed_days).toEqual(0) // 0 // console.log(currentLog); - card = sc[grades[index]].card; + card = sc[grades[index]].card // console.log(card); - index += 1; + index += 1 }) -}); +}) diff --git a/__tests__/forget.test.ts b/__tests__/forget.test.ts index a8ec6f0..574f198 100644 --- a/__tests__/forget.test.ts +++ b/__tests__/forget.test.ts @@ -1,77 +1,77 @@ -import { createEmptyCard, fsrs, FSRS, Rating } from "../src/fsrs"; -import { Grade } from "../src/fsrs"; +import { createEmptyCard, fsrs, FSRS, Rating } from '../src/fsrs' +import { Grade } from '../src/fsrs' -describe("FSRS forget", () => { +describe('FSRS forget', () => { const f: FSRS = fsrs({ w: [ 1.14, 1.01, 5.44, 14.67, 5.3024, 1.5662, 1.2503, 0.0028, 1.5489, 0.1763, 0.9953, 2.7473, 0.0179, 0.3105, 0.3976, 0.0, 2.0902, ], enable_fuzz: false, - }); - it("forget", () => { - const card = createEmptyCard(); - const now = new Date(2022, 11, 29, 12, 30, 0, 0); - const forget_now = new Date(2023, 11, 30, 12, 30, 0, 0); - const scheduling_cards = f.repeat(card, now); + }) + it('forget', () => { + const card = createEmptyCard() + const now = new Date(2022, 11, 29, 12, 30, 0, 0) + const forget_now = new Date(2023, 11, 30, 12, 30, 0, 0) + const scheduling_cards = f.repeat(card, now) const grades: Grade[] = [ Rating.Again, Rating.Hard, Rating.Good, Rating.Easy, - ]; + ] for (const grade of grades) { const forgetCard = f.forget( scheduling_cards[grade].card, forget_now, - true, - ); + true + ) expect(forgetCard.card).toEqual({ ...card, due: forget_now, lapses: 0, reps: 0, last_review: scheduling_cards[grade].card.last_review, - }); - expect(forgetCard.log.rating).toEqual(Rating.Manual); + }) + expect(forgetCard.log.rating).toEqual(Rating.Manual) expect(() => f.rollback(forgetCard.card, forgetCard.log)).toThrow( - "Cannot rollback a manual rating", - ); + 'Cannot rollback a manual rating' + ) } for (const grade of grades) { - const forgetCard = f.forget(scheduling_cards[grade].card, forget_now); + const forgetCard = f.forget(scheduling_cards[grade].card, forget_now) expect(forgetCard.card).toEqual({ ...card, due: forget_now, lapses: scheduling_cards[grade].card.lapses, reps: scheduling_cards[grade].card.reps, last_review: scheduling_cards[grade].card.last_review, - }); - expect(forgetCard.log.rating).toEqual(Rating.Manual); + }) + expect(forgetCard.log.rating).toEqual(Rating.Manual) expect(() => f.rollback(forgetCard.card, forgetCard.log)).toThrow( - "Cannot rollback a manual rating", - ); + 'Cannot rollback a manual rating' + ) } - }); + }) - 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); + 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); + }) + }) + 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__/handler.test.ts b/__tests__/handler.test.ts index 56c3f5d..39c6a5a 100644 --- a/__tests__/handler.test.ts +++ b/__tests__/handler.test.ts @@ -13,45 +13,45 @@ import { ReviewLog, State, StateType, -} from "../src/fsrs"; +} from '../src/fsrs' interface CardPrismaUnChecked - extends Omit { - cid: string; - due: Date | number; - last_review: Date | null | number; - state: StateType; + extends Omit { + cid: string + due: Date | number + last_review: Date | null | number + state: StateType } interface RevLogPrismaUnchecked - extends Omit { - cid: string; - due: Date | number; - state: StateType; - review: Date | number; - rating: RatingType; + extends Omit { + cid: string + due: Date | number + state: StateType + review: Date | number + rating: RatingType } interface RepeatRecordLog { - card: CardPrismaUnChecked; - log: RevLogPrismaUnchecked; + card: CardPrismaUnChecked + log: RevLogPrismaUnchecked } -describe("afterHandler", () => { - const f: FSRS = fsrs(); - const now = new Date(); +describe('afterHandler', () => { + const f: FSRS = fsrs() + const now = new Date() function cardAfterHandler(card: Card) { return { ...card, - cid: "test001", + cid: 'test001', state: State[card.state], last_review: card.last_review ?? null, - } as CardPrismaUnChecked; + } as CardPrismaUnChecked } function repeatAfterHandler(recordLog: RecordLog) { - const record: RepeatRecordLog[] = []; + const record: RepeatRecordLog[] = [] for (const grade of Grades) { record.push({ card: { @@ -70,9 +70,9 @@ describe("afterHandler", () => { state: State[recordLog[grade].log.state] as StateType, rating: Rating[recordLog[grade].log.rating] as RatingType, }, - }); + }) } - return record; + return record } // function repeatAfterHandler(recordLog: RecordLog) { @@ -120,70 +120,88 @@ describe("afterHandler", () => { state: State[recordLogItem.log.state] as StateType, rating: Rating[recordLogItem.log.rating] as RatingType, }, - }; + } } - it("createEmptyCard[afterHandler]", () => { - const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler); - expect(emptyCardFormAfterHandler.state).toEqual("New"); - expect(fixState(emptyCardFormAfterHandler.state)).toEqual(State.New); - expect(emptyCardFormAfterHandler.last_review).toEqual(null); - expect(emptyCardFormAfterHandler.cid).toEqual("test001"); - - const emptyCardFormAfterHandler2 = createEmptyCard(now, cardAfterHandler); - expect(emptyCardFormAfterHandler2.state).toEqual("New"); - expect(fixState(emptyCardFormAfterHandler2.state)).toEqual(State.New); - expect(emptyCardFormAfterHandler2.last_review).toEqual(null); - expect(emptyCardFormAfterHandler2.cid).toEqual("test001"); - }); - - it("repeat[afterHandler]", () => { - const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler); - const repeat = f.repeat(emptyCardFormAfterHandler, now, repeatAfterHandler); - expect(Array.isArray(repeat)).toEqual(true); + it('createEmptyCard[afterHandler]', () => { + const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler) + expect(emptyCardFormAfterHandler.state).toEqual('New') + expect(fixState(emptyCardFormAfterHandler.state)).toEqual(State.New) + expect(emptyCardFormAfterHandler.last_review).toEqual(null) + expect(emptyCardFormAfterHandler.cid).toEqual('test001') + + const emptyCardFormAfterHandler2 = createEmptyCard( + now, + cardAfterHandler + ) + expect(emptyCardFormAfterHandler2.state).toEqual('New') + expect(fixState(emptyCardFormAfterHandler2.state)).toEqual(State.New) + expect(emptyCardFormAfterHandler2.last_review).toEqual(null) + expect(emptyCardFormAfterHandler2.cid).toEqual('test001') + }) + + it('repeat[afterHandler]', () => { + const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler) + const repeat = f.repeat(emptyCardFormAfterHandler, now, repeatAfterHandler) + expect(Array.isArray(repeat)).toEqual(true) for (let i = 0; i < Grades.length; i++) { - expect(Number.isSafeInteger(repeat[i].card.due)).toEqual(true); - expect(typeof repeat[i].card.state === "string").toEqual(true); - expect(Number.isSafeInteger(repeat[i].card.last_review)).toEqual(true); - - expect(Number.isSafeInteger(repeat[i].log.due)).toEqual(true); - expect(Number.isSafeInteger(repeat[i].log.review)).toEqual(true); - expect(typeof repeat[i].log.state === "string").toEqual(true); - expect(typeof repeat[i].log.rating === "string").toEqual(true); - expect(repeat[i].card.cid).toEqual("test001"); - expect(repeat[i].log.cid).toEqual(repeat[i].card.cid); + expect(Number.isSafeInteger(repeat[i].card.due)).toEqual(true) + expect(typeof repeat[i].card.state === 'string').toEqual(true) + expect(Number.isSafeInteger(repeat[i].card.last_review)).toEqual(true) + + expect(Number.isSafeInteger(repeat[i].log.due)).toEqual(true) + expect(Number.isSafeInteger(repeat[i].log.review)).toEqual(true) + expect(typeof repeat[i].log.state === 'string').toEqual(true) + expect(typeof repeat[i].log.rating === 'string').toEqual(true) + expect(repeat[i].card.cid).toEqual('test001') + expect(repeat[i].log.cid).toEqual(repeat[i].card.cid) } - }); - - it("rollback[afterHandler]", () => { - const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler); - const repeatFormAfterHandler = f.repeat(emptyCardFormAfterHandler, now, repeatAfterHandler); - const { card, log } = repeatFormAfterHandler[Rating.Hard]; - const rollbackFromAfterHandler = f.rollback(card, log, cardAfterHandler); - expect(rollbackFromAfterHandler).toEqual(emptyCardFormAfterHandler); - expect(rollbackFromAfterHandler.cid).toEqual("test001"); - }); - - it("forget[afterHandler]", () => { - const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler); - const repeatFormAfterHandler = f.repeat(emptyCardFormAfterHandler, now, repeatAfterHandler); - const { card} = repeatFormAfterHandler[Rating.Hard]; - const forgetFromAfterHandler = f.forget(card, date_scheduler(now, 1, true), false, forgetAfterHandler); - - expect(Number.isSafeInteger(forgetFromAfterHandler.card.due)).toEqual(true); - expect(typeof forgetFromAfterHandler.card.state === "string").toEqual(true); + }) + + it('rollback[afterHandler]', () => { + const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler) + const repeatFormAfterHandler = f.repeat( + emptyCardFormAfterHandler, + now, + repeatAfterHandler + ) + const { card, log } = repeatFormAfterHandler[Rating.Hard] + const rollbackFromAfterHandler = f.rollback(card, log, cardAfterHandler) + expect(rollbackFromAfterHandler).toEqual(emptyCardFormAfterHandler) + expect(rollbackFromAfterHandler.cid).toEqual('test001') + }) + + it('forget[afterHandler]', () => { + const emptyCardFormAfterHandler = createEmptyCard(now, cardAfterHandler) + const repeatFormAfterHandler = f.repeat( + emptyCardFormAfterHandler, + now, + repeatAfterHandler + ) + const { card } = repeatFormAfterHandler[Rating.Hard] + const forgetFromAfterHandler = f.forget( + card, + date_scheduler(now, 1, true), + false, + forgetAfterHandler + ) + + expect(Number.isSafeInteger(forgetFromAfterHandler.card.due)).toEqual(true) + expect(typeof forgetFromAfterHandler.card.state === 'string').toEqual(true) expect( - Number.isSafeInteger(forgetFromAfterHandler.card.last_review), - ).toEqual(true); + Number.isSafeInteger(forgetFromAfterHandler.card.last_review) + ).toEqual(true) - expect(Number.isSafeInteger(forgetFromAfterHandler.log.due)).toEqual(true); + expect(Number.isSafeInteger(forgetFromAfterHandler.log.due)).toEqual(true) expect(Number.isSafeInteger(forgetFromAfterHandler.log.review)).toEqual( - true, - ); - expect(typeof forgetFromAfterHandler.log.state === "string").toEqual(true); - expect(typeof forgetFromAfterHandler.log.rating === "string").toEqual(true); - expect(forgetFromAfterHandler.card.cid).toEqual("test001"); - expect(forgetFromAfterHandler.log.cid).toEqual(forgetFromAfterHandler.card.cid); - }); -}); + true + ) + expect(typeof forgetFromAfterHandler.log.state === 'string').toEqual(true) + expect(typeof forgetFromAfterHandler.log.rating === 'string').toEqual(true) + expect(forgetFromAfterHandler.card.cid).toEqual('test001') + expect(forgetFromAfterHandler.log.cid).toEqual( + forgetFromAfterHandler.card.cid + ) + }) +}) diff --git a/__tests__/help.test.ts b/__tests__/help.test.ts index 5cec4e9..013e46e 100644 --- a/__tests__/help.test.ts +++ b/__tests__/help.test.ts @@ -1,5 +1,6 @@ import { - date_diff, date_scheduler, + date_diff, + date_scheduler, fixDate, fixRating, fixState, @@ -7,194 +8,192 @@ import { Grades, Rating, State, -} from "../src/fsrs"; +} from '../src/fsrs' -test("FSRS-Grades", () => { +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"); - expect(formatDate(now.getTime())).toEqual("2022-12-30 12:30:00"); - expect(formatDate(now.toUTCString())).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); - }); - - test("Date data real type is string/number", () => { - const now = "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", - ); + ]) +}) + +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') + expect(formatDate(now.getTime())).toEqual('2022-12-30 12:30:00') + expect(formatDate(now.toUTCString())).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) + }) + + test('Date data real type is string/number', () => { + const now = '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"); + 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"); + 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); - }); - - test("Date data real type is string/number", () => { - const now = "2023-11-25T12:30:00Z"; - const pre = new Date("2023-11-25T12:00:00Z").getTime(); - 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"; + 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) + }) + + test('Date data real type is string/number', () => { + const now = '2023-11-25T12:30:00Z' + const pre = new Date('2023-11-25T12:00:00Z').getTime() + 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]"); + 'Invalid rating:[invalid-rating]' + ) + expect(() => fixRating(null)).toThrowError('Invalid rating:[null]') expect(() => fixRating(undefined)).toThrowError( - "Invalid rating:[undefined]", - ); - }); -}); + 'Invalid rating:[undefined]' + ) + }) +}) diff --git a/__tests__/models.test.ts b/__tests__/models.test.ts index 2185338..8976208 100644 --- a/__tests__/models.test.ts +++ b/__tests__/models.test.ts @@ -1,74 +1,69 @@ -import { - Rating, - RatingType, - State, - StateType, -} from "../src/fsrs"; +import { Rating, RatingType, State, StateType } from '../src/fsrs' -describe("State", () => { - it("use State.New", () => { - expect(State.New).toEqual(0); - expect(0).toEqual(State.New); - expect(State[State.New]).toEqual("New"); - expect((0 as State).valueOf()).toEqual(0); - expect(State["New" as StateType]).toEqual(0); - }); +describe('State', () => { + it('use State.New', () => { + expect(State.New).toEqual(0) + expect(0).toEqual(State.New) + expect(State[State.New]).toEqual('New') + expect((0 as State).valueOf()).toEqual(0) + expect(State['New' as StateType]).toEqual(0) + }) - it("use State.Learning", () => { - expect(State.Learning).toEqual(1); - expect(1).toEqual(State.Learning); - expect(State[State.Learning]).toEqual("Learning"); - expect((1 as State).valueOf()).toEqual(1); - expect(State["Learning" as StateType]).toEqual(1); - }); + it('use State.Learning', () => { + expect(State.Learning).toEqual(1) + expect(1).toEqual(State.Learning) + expect(State[State.Learning]).toEqual('Learning') + expect((1 as State).valueOf()).toEqual(1) + expect(State['Learning' as StateType]).toEqual(1) + }) - it("use State.Review", () => { - expect(State.Review).toEqual(2); - expect(2).toEqual(State.Review); - expect(State[State.Review]).toEqual("Review"); - expect((2 as State).valueOf()).toEqual(2); - expect(State["Review" as StateType]).toEqual(2); - }); + it('use State.Review', () => { + expect(State.Review).toEqual(2) + expect(2).toEqual(State.Review) + expect(State[State.Review]).toEqual('Review') + expect((2 as State).valueOf()).toEqual(2) + expect(State['Review' as StateType]).toEqual(2) + }) - it("use State.Relearning", () => { - expect(State.Relearning).toEqual(3); - expect(3).toEqual(State.Relearning); - expect(State[State.Relearning]).toEqual("Relearning"); - expect((3 as State).valueOf()).toEqual(3); - expect(State["Relearning" as StateType]).toEqual(3); - }); -}); + it('use State.Relearning', () => { + expect(State.Relearning).toEqual(3) + expect(3).toEqual(State.Relearning) + expect(State[State.Relearning]).toEqual('Relearning') + expect((3 as State).valueOf()).toEqual(3) + expect(State['Relearning' as StateType]).toEqual(3) + }) +}) -describe("Rating", () => { - it("use Rating.Again", () => { - expect(Rating.Again).toEqual(1); - expect(1).toEqual(Rating.Again); - expect(Rating[Rating.Again]).toEqual("Again"); - expect((1 as Rating).valueOf()).toEqual(1); - expect(Rating["Again" as RatingType]).toEqual(1); - }); +describe('Rating', () => { + it('use Rating.Again', () => { + expect(Rating.Again).toEqual(1) + expect(1).toEqual(Rating.Again) + expect(Rating[Rating.Again]).toEqual('Again') + expect((1 as Rating).valueOf()).toEqual(1) + expect(Rating['Again' as RatingType]).toEqual(1) + }) - it("use Rating.Hard", () => { - expect(Rating.Hard).toEqual(2); - expect(2).toEqual(Rating.Hard); - expect(Rating[Rating.Hard]).toEqual("Hard"); - expect((2 as Rating).valueOf()).toEqual(2); - expect(Rating["Hard" as RatingType]).toEqual(2); - }); + it('use Rating.Hard', () => { + expect(Rating.Hard).toEqual(2) + expect(2).toEqual(Rating.Hard) + expect(Rating[Rating.Hard]).toEqual('Hard') + expect((2 as Rating).valueOf()).toEqual(2) + expect(Rating['Hard' as RatingType]).toEqual(2) + }) - it("use Rating.Good", () => { - expect(Rating.Good).toEqual(3); - expect(3).toEqual(Rating.Good); - expect(Rating[Rating.Good]).toEqual("Good"); - expect((3 as Rating).valueOf()).toEqual(3); - expect(Rating["Good" as RatingType]).toEqual(3); - }); + it('use Rating.Good', () => { + expect(Rating.Good).toEqual(3) + expect(3).toEqual(Rating.Good) + expect(Rating[Rating.Good]).toEqual('Good') + expect((3 as Rating).valueOf()).toEqual(3) + expect(Rating['Good' as RatingType]).toEqual(3) + }) - it("use Rating.Easy", () => { - expect(Rating.Easy).toEqual(4); - expect(4).toEqual(Rating.Easy); - expect(Rating[Rating.Easy]).toEqual("Easy"); - expect((4 as Rating).valueOf()).toEqual(4); - expect(Rating["Easy" as RatingType]).toEqual(4); - }); -}); \ No newline at end of file + it('use Rating.Easy', () => { + expect(Rating.Easy).toEqual(4) + expect(4).toEqual(Rating.Easy) + expect(Rating[Rating.Easy]).toEqual('Easy') + expect((4 as Rating).valueOf()).toEqual(4) + expect(Rating['Easy' as RatingType]).toEqual(4) + }) +}) diff --git a/__tests__/reschedule.test.ts b/__tests__/reschedule.test.ts index 1334e1c..7443924 100644 --- a/__tests__/reschedule.test.ts +++ b/__tests__/reschedule.test.ts @@ -8,23 +8,23 @@ import { FSRS, get_fuzz_range, State, -} from "../src/fsrs"; +} from '../src/fsrs' -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]; +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 CardType = Card & { - cid: number; - }; + cid: number + } function cardHandler(card: Card) { - (card as CardType)["cid"] = 1; - return card as CardType; + ;(card as CardType)['cid'] = 1 + return card as CardType } - const newCard = createEmptyCard(undefined, cardHandler); + const newCard = createEmptyCard(undefined, cardHandler) const learningCard: CardType = { cid: 1, due: new Date(), @@ -35,11 +35,11 @@ describe("FSRS reschedule", () => { reps: 1, lapses: 0, state: State.Learning, - last_review: new Date("2024-03-08 05:00:00"), - }; + last_review: new Date('2024-03-08 05:00:00'), + } const reviewCard: CardType = { cid: 1, - due: new Date("2024-03-17 04:43:02"), + due: new Date('2024-03-17 04:43:02'), stability: 48.26139059062234, difficulty: 5.67, elapsed_days: 18, @@ -47,11 +47,11 @@ describe("FSRS reschedule", () => { reps: 8, lapses: 1, state: State.Review, - last_review: new Date("2024-01-26 04:43:02"), - }; + last_review: new Date('2024-01-26 04:43:02'), + } const relearningCard: CardType = { cid: 1, - due: new Date("2024-02-15 08:43:05"), + due: new Date('2024-02-15 08:43:05'), stability: 0.27, difficulty: 10, elapsed_days: 2, @@ -59,85 +59,85 @@ describe("FSRS reschedule", () => { reps: 42, lapses: 8, state: State.Relearning, - last_review: new Date("2024-02-15 08:38:05"), - }; + last_review: new Date('2024-02-15 08:38:05'), + } function dateHandler(date: Date) { - return date.getTime(); + return date.getTime() } - const cards = [newCard, learningCard, reviewCard, relearningCard]; - it("reschedule", () => { + const cards = [newCard, learningCard, reviewCard, relearningCard] + it('reschedule', () => { for (const requestRetention of request_retentions) { - const f: FSRS = fsrs({ request_retention: requestRetention }); + const f: FSRS = fsrs({ request_retention: requestRetention }) const intervalModifier = - (Math.pow(requestRetention, 1 / DECAY) - 1) / FACTOR; - const reschedule_cards = f.reschedule(cards); + (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); + 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, - ); + default_maximum_interval + ) expect(reschedule_cards[0].scheduled_days).toBeGreaterThanOrEqual( - min_ivl, - ); - expect(reschedule_cards[0].scheduled_days).toBeLessThanOrEqual(max_ivl); + 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, - ), - ); + true + ) + ) } } - }); + }) - it("reschedule[dateHandler]", () => { + it('reschedule[dateHandler]', () => { for (const requestRetention of request_retentions) { - const f: FSRS = fsrs({ request_retention: requestRetention }); + const f: FSRS = fsrs({ request_retention: requestRetention }) const intervalModifier = - (Math.pow(requestRetention, 1 / DECAY) - 1) / FACTOR; + (Math.pow(requestRetention, 1 / DECAY) - 1) / FACTOR const [rescheduleCard] = f.reschedule([reviewCard], { dateHandler, - }); + }) if (rescheduleCard) { // next_ivl !== scheduled_days - expect(rescheduleCard.cid).toEqual(1); + expect(rescheduleCard.cid).toEqual(1) const { min_ivl, max_ivl } = get_fuzz_range( Math.round(reviewCard.stability * intervalModifier), reviewCard.elapsed_days, - default_maximum_interval, - ); + 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.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"); + true + ).getTime() + ) + expect(typeof rescheduleCard.due).toEqual('number') } } - }); + }) - it("reschedule[next_ivl === scheduled_days]", () => { - const f: FSRS = fsrs(); + 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"), + due: new Date('2024-03-13 04:43:02'), stability: 48.26139059062234, difficulty: 5.67, elapsed_days: 18, @@ -145,24 +145,24 @@ describe("FSRS reschedule", () => { reps: 8, lapses: 1, state: State.Review, - last_review: new Date("2024-01-26 04:43:02"), + last_review: new Date('2024-01-26 04:43:02'), }, ], - { enable_fuzz: false }, - ); - expect(reschedule_cards.length).toEqual(0); - }); + { enable_fuzz: false } + ) + expect(reschedule_cards.length).toEqual(0) + }) - it("reschedule by empty array", () => { - const f: FSRS = fsrs(); - const reschedule_cards = f.reschedule([]); - expect(reschedule_cards.length).toEqual(0); - }); + it('reschedule by empty array', () => { + const f: FSRS = fsrs() + const reschedule_cards = f.reschedule([]) + expect(reschedule_cards.length).toEqual(0) + }) - it("reschedule by not array", () => { - const f: FSRS = fsrs(); - expect(() =>{f.reschedule(createEmptyCard() as unknown as Card[])}).toThrow( - "cards must be an array", - ); - }); -}); + it('reschedule by not array', () => { + const f: FSRS = fsrs() + expect(() => { + f.reschedule(createEmptyCard() as unknown as Card[]) + }).toThrow('cards must be an array') + }) +}) diff --git a/__tests__/rollback.test.ts b/__tests__/rollback.test.ts index 7d523d3..cda60f7 100644 --- a/__tests__/rollback.test.ts +++ b/__tests__/rollback.test.ts @@ -1,41 +1,41 @@ -import {createEmptyCard, fsrs, FSRS, Grade, Rating} from "../src/fsrs"; +import { createEmptyCard, fsrs, FSRS, Grade, Rating } from '../src/fsrs' -describe("FSRS rollback", () => { +describe('FSRS rollback', () => { const f: FSRS = fsrs({ w: [ 1.14, 1.01, 5.44, 14.67, 5.3024, 1.5662, 1.2503, 0.0028, 1.5489, 0.1763, 0.9953, 2.7473, 0.0179, 0.3105, 0.3976, 0.0, 2.0902, ], enable_fuzz: false, - }); - it("first rollback", () => { - const card = createEmptyCard(); - const now = new Date(2022, 11, 29, 12, 30, 0, 0); - const scheduling_cards = f.repeat(card, now); - const grade:Grade[] = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy]; + }) + it('first rollback', () => { + const card = createEmptyCard() + const now = new Date(2022, 11, 29, 12, 30, 0, 0) + const scheduling_cards = f.repeat(card, now) + const grade: Grade[] = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy] for (const rating of grade) { const rollbackCard = f.rollback( scheduling_cards[rating].card, - scheduling_cards[rating].log, - ); - expect(rollbackCard).toEqual(card); + scheduling_cards[rating].log + ) + expect(rollbackCard).toEqual(card) } - }); + }) - it("rollback 2", () => { - let card = createEmptyCard(); - let now = new Date(2022, 11, 29, 12, 30, 0, 0); - let scheduling_cards = f.repeat(card, now); - card = scheduling_cards["4"].card; - now = card.due; - scheduling_cards = f.repeat(card, now); - const grade:Grade[] = [Rating.Again, Rating.Hard, Rating.Good,Rating.Easy]; + it('rollback 2', () => { + let card = createEmptyCard() + let now = new Date(2022, 11, 29, 12, 30, 0, 0) + let scheduling_cards = f.repeat(card, now) + card = scheduling_cards['4'].card + now = card.due + scheduling_cards = f.repeat(card, now) + const grade: Grade[] = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy] for (const rating of grade) { const rollbackCard = f.rollback( scheduling_cards[rating].card, - scheduling_cards[rating].log, - ); - expect(rollbackCard).toEqual(card); + scheduling_cards[rating].log + ) + expect(rollbackCard).toEqual(card) } - }); -}); + }) +}) diff --git a/__tests__/show_diff_message.test.ts b/__tests__/show_diff_message.test.ts new file mode 100644 index 0000000..5fa39e7 --- /dev/null +++ b/__tests__/show_diff_message.test.ts @@ -0,0 +1,190 @@ +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' + const t3 = '1970-01-01 00:00:00' + const t4 = '1970-01-02 00:00:00' + + const t5 = 0 + const t6 = 1000 * 60 * 60 * 24 + // @ts-ignore + 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) + const t3 = new Date(t1.getTime() + 1000 * 60 * 60 * 24 * 30) + 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) + const t3 = new Date(t1.getTime() + 1000 * 60 * 60 * 24 * 31 * 12) + 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) + const t3 = new Date( + t1.getTime() + 1000 * 60 * 60 * 24 * 31 * 13 + 1000 * 60 * 60 * 24 + ) + const t4 = new Date( + t1.getTime() + 1000 * 60 * 60 * 24 * 31 * 24 + 1000 * 60 * 60 * 24 + ) + 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' + ) +}) + +test('Date data real type is string/number', () => { + const TIMEUNITFORMAT_TEST = ['年'] + const t1 = new Date() + const t2 = new Date( + t1.getTime() + 1000 * 60 * 60 * 24 * 31 * 13 + ).toDateString() + expect(show_diff_message(t2, t1.getTime())).toBe('1') + expect(show_diff_message(t2, t1.toUTCString(), 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' + ) +}) diff --git a/__tests__/show_diff_message.ts b/__tests__/show_diff_message.ts deleted file mode 100644 index 112196b..0000000 --- a/__tests__/show_diff_message.ts +++ /dev/null @@ -1,147 +0,0 @@ -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"; - const t3 = "1970-01-01 00:00:00"; - const t4 = "1970-01-02 00:00:00"; - - const t5 = 0; - const t6 = 1000*60*60*24; - // @ts-ignore - 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); - const t3 = new Date(t1.getTime() + 1000 * 60 * 60 * 24 * 30); - 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); - const t3 = new Date(t1.getTime() + 1000 * 60 * 60 * 24 * 31 * 12); - 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); - const t3 = new Date( - t1.getTime() + 1000 * 60 * 60 * 24 * 31 * 13 + 1000 * 60 * 60 * 24, - ); - const t4 = new Date( - t1.getTime() + 1000 * 60 * 60 * 24 * 31 * 24 + 1000 * 60 * 60 * 24, - ); - 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"); -}); - -test("Date data real type is string/number", ()=>{ - const TIMEUNITFORMAT_TEST = ["年"]; - const t1 = new Date(); - const t2 = new Date(t1.getTime() + 1000 * 60 * 60 * 24 * 31 * 13).toDateString(); - expect(show_diff_message(t2, t1.getTime())).toBe("1"); - expect(show_diff_message(t2, t1.toUTCString(), 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/__tests__/version.test.ts b/__tests__/version.test.ts new file mode 100644 index 0000000..114af73 --- /dev/null +++ b/__tests__/version.test.ts @@ -0,0 +1,8 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { version } from '../package.json' +import { FSRSVersion } from '../src/fsrs' + +test('TS-FSRS-Version', () => { + expect(FSRSVersion).toBe(version) +}) diff --git a/__tests__/version.ts b/__tests__/version.ts deleted file mode 100644 index c0cc550..0000000 --- a/__tests__/version.ts +++ /dev/null @@ -1,9 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import {version} from '../package.json'; -import { FSRSVersion } from "../src/fsrs"; - - -test("TS-FSRS-Version", () => { - expect(FSRSVersion).toBe(version); -}); \ No newline at end of file diff --git a/package.json b/package.json index d0cb7cc..a7196c0 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "typescript": "^5.2.2" }, "scripts": { - "lint": "eslint --fix src/ && prettier --write src/", + "lint": "eslint src/", + "lint::fix": "eslint --fix src/ && prettier --write src/", "dev": "rollup -c rollup.config.ts --configPlugin esbuild -w", "test": "jest --config=jest.config.js --passWithNoTests", "test::coverage": "jest --config=jest.config.js --coverage", diff --git a/src/fsrs/algorithm.ts b/src/fsrs/algorithm.ts index 6a6e85a..0e3b1d0 100644 --- a/src/fsrs/algorithm.ts +++ b/src/fsrs/algorithm.ts @@ -1,42 +1,42 @@ -import pseudorandom from "seedrandom"; -import { generatorParameters } from "./default"; -import {SchedulingCard} from './scheduler' -import {FSRSParameters, Grade, Rating} from "./models"; -import type { int } from "./type"; -import { get_fuzz_range } from "./help"; +import pseudorandom from 'seedrandom' +import { generatorParameters } from './default' +import { SchedulingCard } from './scheduler' +import { FSRSParameters, Grade, Rating } from './models' +import type { int } from './type' +import { get_fuzz_range } from './help' /** * @default DECAY = -0.5 */ -export const DECAY: number = -0.5; +export const DECAY: number = -0.5 /** * FACTOR = Math.pow(0.9, 1 / DECAY) - 1= 19 / 81 * * $$\text{FACTOR} = \frac{19}{81}$$ * @default FACTOR = 19 / 81 */ -export const FACTOR: number = 19 / 81; +export const FACTOR: number = 19 / 81 /** * @see https://github.com/open-spaced-repetition/fsrs4anki/wiki/The-Algorithm#fsrs-45 */ export class FSRSAlgorithm { - protected param!: FSRSParameters; - protected intervalModifier!: number; - protected seed?: string; + protected param!: FSRSParameters + protected intervalModifier!: number + protected seed?: string constructor(params: Partial) { this.param = new Proxy( generatorParameters(params), - this.params_handler_proxy(), - ); + this.params_handler_proxy() + ) this.intervalModifier = this.calculate_interval_modifier( - this.param.request_retention, - ); + this.param.request_retention + ) } get interval_modifier(): number { - return this.intervalModifier; + return this.intervalModifier } /** @@ -48,16 +48,16 @@ export class FSRSAlgorithm { */ calculate_interval_modifier(request_retention: number): number { if (request_retention <= 0 || request_retention > 1) { - throw new Error("Requested retention rate should be in the range (0,1]"); + throw new Error('Requested retention rate should be in the range (0,1]') } - return +((Math.pow(request_retention, 1 / DECAY) - 1) / FACTOR).toFixed(8); + return +((Math.pow(request_retention, 1 / DECAY) - 1) / FACTOR).toFixed(8) } /** * Get the parameters of the algorithm. */ get parameters(): FSRSParameters { - return this.param; + return this.param } /** @@ -65,45 +65,45 @@ export class FSRSAlgorithm { * @param params Partial */ set parameters(params: Partial) { - this.update_parameters(params); + this.update_parameters(params) } private params_handler_proxy(): ProxyHandler { // eslint-disable-next-line @typescript-eslint/no-this-alias - const _this: FSRSAlgorithm = this; + const _this: FSRSAlgorithm = this return { set: function (target, prop, value) { - if (prop === "request_retention" && Number.isFinite(value)) { + if (prop === 'request_retention' && Number.isFinite(value)) { _this.intervalModifier = _this.calculate_interval_modifier( - Number(value), - ); + Number(value) + ) } // @ts-ignore - target[prop] = value; - return true; + target[prop] = value + return true }, - }; + } } private update_parameters(params: Partial): void { - const _params = generatorParameters(params); + const _params = generatorParameters(params) for (const key in _params) { if (key in this.param) { - const paramKey = key as keyof FSRSParameters; - this.param[paramKey] = _params[paramKey] as never; + const paramKey = key as keyof FSRSParameters + this.param[paramKey] = _params[paramKey] as never } } } init_ds(s: SchedulingCard): void { - s.again.difficulty = this.init_difficulty(Rating.Again); - s.again.stability = this.init_stability(Rating.Again); - s.hard.difficulty = this.init_difficulty(Rating.Hard); - s.hard.stability = this.init_stability(Rating.Hard); - s.good.difficulty = this.init_difficulty(Rating.Good); - s.good.stability = this.init_stability(Rating.Good); - s.easy.difficulty = this.init_difficulty(Rating.Easy); - s.easy.stability = this.init_stability(Rating.Easy); + s.again.difficulty = this.init_difficulty(Rating.Again) + s.again.stability = this.init_stability(Rating.Again) + s.hard.difficulty = this.init_difficulty(Rating.Hard) + s.hard.stability = this.init_stability(Rating.Hard) + s.good.difficulty = this.init_difficulty(Rating.Good) + s.good.stability = this.init_stability(Rating.Good) + s.easy.difficulty = this.init_difficulty(Rating.Easy) + s.easy.stability = this.init_stability(Rating.Easy) } /** @@ -118,35 +118,35 @@ export class FSRSAlgorithm { s: SchedulingCard, last_d: number, last_s: number, - retrievability: number, + retrievability: number ): void { - s.again.difficulty = this.next_difficulty(last_d, Rating.Again); + s.again.difficulty = this.next_difficulty(last_d, Rating.Again) s.again.stability = this.next_forget_stability( last_d, last_s, - retrievability, - ); - s.hard.difficulty = this.next_difficulty(last_d, Rating.Hard); + retrievability + ) + s.hard.difficulty = this.next_difficulty(last_d, Rating.Hard) s.hard.stability = this.next_recall_stability( last_d, last_s, retrievability, - Rating.Hard, - ); - s.good.difficulty = this.next_difficulty(last_d, Rating.Good); + Rating.Hard + ) + s.good.difficulty = this.next_difficulty(last_d, Rating.Good) s.good.stability = this.next_recall_stability( last_d, last_s, retrievability, - Rating.Good, - ); - s.easy.difficulty = this.next_difficulty(last_d, Rating.Easy); + Rating.Good + ) + s.easy.difficulty = this.next_difficulty(last_d, Rating.Easy) s.easy.stability = this.next_recall_stability( last_d, last_s, retrievability, - Rating.Easy, - ); + Rating.Easy + ) } /** @@ -158,7 +158,7 @@ export class FSRSAlgorithm { * @return Stability (interval when R=90%) */ init_stability(g: Grade): number { - return Math.max(this.param.w[g - 1], 0.1); + return Math.max(this.param.w[g - 1], 0.1) } /** @@ -173,8 +173,8 @@ export class FSRSAlgorithm { init_difficulty(g: Grade): number { return +Math.min( Math.max(this.param.w[4] - (g - 3) * this.param.w[5], 1), - 10, - ).toFixed(8); + 10 + ).toFixed(8) } /** @@ -185,15 +185,15 @@ export class FSRSAlgorithm { * @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; - const generator = pseudorandom(this.seed); - const fuzz_factor = generator(); + if (!enable_fuzz || ivl < 2.5) return Math.round(ivl) as int + const generator = pseudorandom(this.seed) + const fuzz_factor = generator() const { min_ivl, max_ivl } = get_fuzz_range( ivl, elapsed_days, - this.param.maximum_interval, - ); - return Math.floor(fuzz_factor * (max_ivl - min_ivl + 1) + min_ivl) as int; + this.param.maximum_interval + ) + return Math.floor(fuzz_factor * (max_ivl - min_ivl + 1) + min_ivl) as int } /** @@ -205,13 +205,13 @@ export class FSRSAlgorithm { next_interval( s: number, elapsed_days: number, - enable_fuzz: boolean = this.param.enable_fuzz, + enable_fuzz: boolean = this.param.enable_fuzz ): 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); + this.param.maximum_interval + ) as int + return this.apply_fuzz(newInterval, elapsed_days, enable_fuzz) } /** @@ -223,10 +223,10 @@ export class FSRSAlgorithm { * @return {number} $$\text{next}_D$$ */ next_difficulty(d: number, g: Grade): number { - const next_d = d - this.param.w[6] * (g - 3); + const next_d = d - this.param.w[6] * (g - 3) return this.constrain_difficulty( - this.mean_reversion(this.param.w[4], next_d), - ); + this.mean_reversion(this.param.w[4], next_d) + ) } /** @@ -235,7 +235,7 @@ export class FSRSAlgorithm { * @param {number} difficulty $$D \in [1,10]$$ */ constrain_difficulty(difficulty: number): number { - return Math.min(Math.max(+difficulty.toFixed(8), 1), 10); + return Math.min(Math.max(+difficulty.toFixed(8), 1), 10) } /** @@ -247,8 +247,8 @@ export class FSRSAlgorithm { */ mean_reversion(init: number, current: number): number { return +(this.param.w[7] * init + (1 - this.param.w[7]) * current).toFixed( - 8, - ); + 8 + ) } /** @@ -261,8 +261,8 @@ export class FSRSAlgorithm { * @return {number} S^\prime_r new stability after recall */ next_recall_stability(d: number, s: number, r: number, g: Grade): number { - const hard_penalty = Rating.Hard === g ? this.param.w[15] : 1; - const easy_bound = Rating.Easy === g ? this.param.w[16] : 1; + const hard_penalty = Rating.Hard === g ? this.param.w[15] : 1 + const easy_bound = Rating.Easy === g ? this.param.w[16] : 1 return +( s * (1 + @@ -272,7 +272,7 @@ export class FSRSAlgorithm { (Math.exp((1 - r) * this.param.w[10]) - 1) * hard_penalty * easy_bound) - ).toFixed(8); + ).toFixed(8) } /** @@ -289,7 +289,7 @@ export class FSRSAlgorithm { Math.pow(d, -this.param.w[12]) * (Math.pow(s + 1, this.param.w[13]) - 1) * Math.exp((1 - r) * this.param.w[14]) - ).toFixed(8); + ).toFixed(8) } /** @@ -300,6 +300,6 @@ export class FSRSAlgorithm { * @return {number} r Retrievability (probability of recall) */ forgetting_curve(elapsed_days: number, stability: number): number { - return +Math.pow(1 + (FACTOR * elapsed_days) / stability, DECAY).toFixed(8); + return +Math.pow(1 + (FACTOR * elapsed_days) / stability, DECAY).toFixed(8) } } diff --git a/src/fsrs/default.ts b/src/fsrs/default.ts index 1cbd3e9..ffd819c 100644 --- a/src/fsrs/default.ts +++ b/src/fsrs/default.ts @@ -1,26 +1,26 @@ -import { Card, DateInput, FSRSParameters, State } from "./models"; -import { fixDate } from "./help"; +import { Card, DateInput, FSRSParameters, State } from './models' +import { fixDate } from './help' -export const default_request_retention = 0.9; -export const default_maximum_interval = 36500; +export const default_request_retention = 0.9 +export const default_maximum_interval = 36500 export const default_w = [ 0.5701, 1.4436, 4.1386, 10.9355, 5.1443, 1.2006, 0.8627, 0.0362, 1.629, 0.1342, 1.0166, 2.1174, 0.0839, 0.3204, 1.4676, 0.219, 2.8237, -]; -export const default_enable_fuzz = false; +] +export const default_enable_fuzz = false -export const FSRSVersion: string = "3.5.7"; +export const FSRSVersion: string = '3.5.7' export const generatorParameters = ( - props?: Partial, + props?: Partial ): FSRSParameters => { return { request_retention: props?.request_retention || default_request_retention, maximum_interval: props?.maximum_interval || default_maximum_interval, w: props?.w || default_w, enable_fuzz: props?.enable_fuzz || default_enable_fuzz, - }; -}; + } +} /** * Create an empty card @@ -54,7 +54,7 @@ export const generatorParameters = ( */ export function createEmptyCard( now?: DateInput, - afterHandler?: (card: Card) => R, + afterHandler?: (card: Card) => R ): R { const emptyCard: Card = { due: now ? fixDate(now) : new Date(), @@ -66,10 +66,10 @@ export function createEmptyCard( lapses: 0, state: State.New, last_review: undefined, - }; - if (afterHandler && typeof afterHandler === "function") { - return afterHandler(emptyCard); + } + if (afterHandler && typeof afterHandler === 'function') { + return afterHandler(emptyCard) } else { - return emptyCard as R; + return emptyCard as R } } diff --git a/src/fsrs/fsrs.ts b/src/fsrs/fsrs.ts index 4bd5816..69d4fa1 100644 --- a/src/fsrs/fsrs.ts +++ b/src/fsrs/fsrs.ts @@ -1,5 +1,5 @@ -import { SchedulingCard } from "./scheduler"; -import { date_scheduler, fixDate, fixRating, fixState } from "./help"; +import { SchedulingCard } from './scheduler' +import { date_scheduler, fixDate, fixRating, fixState } from './help' import { Card, CardInput, @@ -12,13 +12,13 @@ import { ReviewLog, ReviewLogInput, State, -} from "./models"; -import type { int } from "./type"; -import { FSRSAlgorithm } from "./algorithm"; +} from './models' +import type { int } from './type' +import { FSRSAlgorithm } from './algorithm' export class FSRS extends FSRSAlgorithm { constructor(param: Partial) { - super(param); + super(param) } private preProcessCard(_card: CardInput | Card): Card { @@ -27,11 +27,11 @@ export class FSRS extends FSRSAlgorithm { state: fixState(_card.state), due: fixDate(_card.due), last_review: _card.last_review ? fixDate(_card.last_review) : undefined, - }; + } } private preProcessDate(_date: DateInput): Date { - return fixDate(_date); + return fixDate(_date) } private preProcessLog(_log: ReviewLogInput | ReviewLog): ReviewLog { @@ -41,7 +41,7 @@ export class FSRS extends FSRSAlgorithm { rating: fixRating(_log.rating), state: fixState(_log.state), review: fixDate(_log.review), - }; + } } /** @@ -104,58 +104,58 @@ export class FSRS extends FSRSAlgorithm { repeat( card: CardInput | Card, now: DateInput, - afterHandler?: (recordLog: RecordLog) => R, + afterHandler?: (recordLog: RecordLog) => R ): R { - const processedCard = this.preProcessCard(card); - now = this.preProcessDate(now); + const processedCard = this.preProcessCard(card) + now = this.preProcessDate(now) const s = new SchedulingCard(processedCard, now).update_state( - processedCard.state, - ); - this.seed = String(now.getTime()) + String(processedCard.reps); - let easy_interval, good_interval, hard_interval; - const interval = processedCard.elapsed_days; + processedCard.state + ) + this.seed = String(now.getTime()) + String(processedCard.reps) + let easy_interval, good_interval, hard_interval + const interval = processedCard.elapsed_days switch (processedCard.state) { case State.New: - this.init_ds(s); - s.again.due = now.scheduler(1 as int); - s.hard.due = now.scheduler(5 as int); - s.good.due = now.scheduler(10 as int); - easy_interval = this.next_interval(s.easy.stability, interval); - s.easy.scheduled_days = easy_interval; - s.easy.due = now.scheduler(easy_interval, true); - break; + this.init_ds(s) + s.again.due = now.scheduler(1 as int) + s.hard.due = now.scheduler(5 as int) + s.good.due = now.scheduler(10 as int) + easy_interval = this.next_interval(s.easy.stability, interval) + s.easy.scheduled_days = easy_interval + s.easy.due = now.scheduler(easy_interval, true) + break case State.Learning: case State.Relearning: - hard_interval = 0; - good_interval = this.next_interval(s.good.stability, interval); + hard_interval = 0 + good_interval = this.next_interval(s.good.stability, interval) easy_interval = Math.max( this.next_interval(s.easy.stability, interval), - good_interval + 1, - ); - s.schedule(now, hard_interval, good_interval, easy_interval); - break; + good_interval + 1 + ) + s.schedule(now, hard_interval, good_interval, easy_interval) + break case State.Review: { - const last_d = processedCard.difficulty; - const last_s = processedCard.stability; - const retrievability = this.forgetting_curve(interval, last_s); - this.next_ds(s, last_d, last_s, retrievability); - hard_interval = this.next_interval(s.hard.stability, interval); - good_interval = this.next_interval(s.good.stability, interval); - hard_interval = Math.min(hard_interval, good_interval); - good_interval = Math.max(good_interval, hard_interval + 1); + const last_d = processedCard.difficulty + const last_s = processedCard.stability + const retrievability = this.forgetting_curve(interval, last_s) + this.next_ds(s, last_d, last_s, retrievability) + hard_interval = this.next_interval(s.hard.stability, interval) + good_interval = this.next_interval(s.good.stability, interval) + hard_interval = Math.min(hard_interval, good_interval) + good_interval = Math.max(good_interval, hard_interval + 1) easy_interval = Math.max( this.next_interval(s.easy.stability, interval), - good_interval + 1, - ); - s.schedule(now, hard_interval, good_interval, easy_interval); - break; + good_interval + 1 + ) + s.schedule(now, hard_interval, good_interval, easy_interval) + break } } - const recordLog = s.record_log(processedCard, now); - if (afterHandler && typeof afterHandler === "function") { - return afterHandler(recordLog); + const recordLog = s.record_log(processedCard, now) + if (afterHandler && typeof afterHandler === 'function') { + return afterHandler(recordLog) } else { - return recordLog as R; + return recordLog as R } } @@ -169,18 +169,18 @@ export class FSRS extends FSRSAlgorithm { get_retrievability( card: CardInput | Card, now: DateInput, - format: T = true as T, + format: T = true as T ): undefined | (T extends true ? string : number) { - const processedCard = this.preProcessCard(card); - now = this.preProcessDate(now); + const processedCard = this.preProcessCard(card) + now = this.preProcessDate(now) if (processedCard.state !== State.Review) { - return undefined; + return undefined } - const t = Math.max(now.diff(processedCard.last_review as Date, "days"), 0); - const r = this.forgetting_curve(t, Math.round(processedCard.stability)); + const t = Math.max(now.diff(processedCard.last_review as Date, 'days'), 0) + const r = this.forgetting_curve(t, Math.round(processedCard.stability)) return (format ? `${(r * 100).toFixed(2)}%` : r) as T extends true ? string - : number; + : number } /** @@ -211,32 +211,32 @@ export class FSRS extends FSRSAlgorithm { rollback( card: CardInput | Card, log: ReviewLogInput, - afterHandler?: (prevCard: Card) => R, + afterHandler?: (prevCard: Card) => R ): R { - const processedCard = this.preProcessCard(card); - const processedLog = this.preProcessLog(log); + const processedCard = this.preProcessCard(card) + const processedLog = this.preProcessLog(log) if (processedLog.rating === Rating.Manual) { - throw new Error("Cannot rollback a manual rating"); + throw new Error('Cannot rollback a manual rating') } - let last_due, last_review, last_lapses; + let last_due, last_review, last_lapses switch (processedLog.state) { case State.New: - last_due = processedLog.due; - last_review = undefined; - last_lapses = 0; - break; + last_due = processedLog.due + last_review = undefined + last_lapses = 0 + break case State.Learning: case State.Relearning: case State.Review: - last_due = processedLog.review; - last_review = processedLog.due; + last_due = processedLog.review + last_review = processedLog.due last_lapses = processedCard.lapses - (processedLog.rating === Rating.Again && processedLog.state === State.Review ? 1 - : 0); - break; + : 0) + break } const prevCard: Card = { @@ -250,11 +250,11 @@ export class FSRS extends FSRSAlgorithm { lapses: Math.max(0, last_lapses), state: processedLog.state, last_review: last_review, - }; - if (afterHandler && typeof afterHandler === "function") { - return afterHandler(prevCard); + } + if (afterHandler && typeof afterHandler === 'function') { + return afterHandler(prevCard) } else { - return prevCard as R; + return prevCard as R } } @@ -313,14 +313,14 @@ export class FSRS extends FSRSAlgorithm { card: CardInput | Card, now: DateInput, reset_count: boolean = false, - afterHandler?: (recordLogItem: RecordLogItem) => R, + afterHandler?: (recordLogItem: RecordLogItem) => R ): R { - const processedCard = this.preProcessCard(card); - now = this.preProcessDate(now); + const processedCard = this.preProcessCard(card) + now = this.preProcessDate(now) const scheduled_days = processedCard.state === State.New ? 0 - : now.diff(processedCard.last_review as Date, "days"); + : now.diff(processedCard.last_review as Date, 'days') const forget_log: ReviewLog = { rating: Rating.Manual, state: processedCard.state, @@ -331,7 +331,7 @@ export class FSRS extends FSRSAlgorithm { last_elapsed_days: processedCard.elapsed_days, scheduled_days: scheduled_days, review: now, - }; + } const forget_card: Card = { ...processedCard, due: now, @@ -343,12 +343,12 @@ export class FSRS extends FSRSAlgorithm { lapses: reset_count ? 0 : processedCard.lapses, state: State.New, last_review: processedCard.last_review, - }; - const recordLogItem: RecordLogItem = { card: forget_card, log: forget_log }; - if (afterHandler && typeof afterHandler === "function") { - return afterHandler(recordLogItem); + } + const recordLogItem: RecordLogItem = { card: forget_card, log: forget_log } + if (afterHandler && typeof afterHandler === 'function') { + return afterHandler(recordLogItem) } else { - return recordLogItem as R; + return recordLogItem as R } } @@ -380,33 +380,33 @@ export class FSRS extends FSRSAlgorithm { */ reschedule( cards: Array, - options: RescheduleOptions = {}, + options: RescheduleOptions = {} ): Array { if (!Array.isArray(cards)) { - throw new Error("cards must be an array"); + throw new Error('cards must be an array') } - const processedCard: T[] = []; + const processedCard: T[] = [] for (const card of cards) { - if (fixState(card.state) !== State.Review || !card.last_review) continue; - const scheduled_days = Math.floor(card.scheduled_days) as int; + if (fixState(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), - options.enable_fuzz ?? true, - ); - if (next_ivl === scheduled_days || next_ivl === 0) continue; + options.enable_fuzz ?? true + ) + 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); + 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; + processCard.due = new_due } - processedCard.push(processCard); + processedCard.push(processCard) } - return processedCard; + return processedCard } } @@ -428,5 +428,5 @@ export class FSRS extends FSRSAlgorithm { * ``` */ export const fsrs = (params?: Partial) => { - return new FSRS(params || {}); -}; + return new FSRS(params || {}) +} diff --git a/src/fsrs/help.ts b/src/fsrs/help.ts index aae0830..9dd097a 100644 --- a/src/fsrs/help.ts +++ b/src/fsrs/help.ts @@ -1,22 +1,22 @@ -import type { int, unit } from "./type"; -import type { DateInput, Grade } from './models'; -import { Rating, State } from './models'; +import type { int, unit } from './type' +import type { DateInput, Grade } from './models' +import { Rating, State } from './models' declare global { export interface Date { - scheduler(t: int, isDay?: boolean): Date; + scheduler(t: int, isDay?: boolean): Date - diff(pre: Date, unit: unit): int; + diff(pre: Date, unit: unit): int - format(): string; + format(): string - dueFormat(last_review: Date, unit?: boolean,timeUnit?: string[]): string; + dueFormat(last_review: Date, unit?: boolean, timeUnit?: string[]): string } } Date.prototype.scheduler = function (t: int, isDay?: boolean): Date { - return date_scheduler(this, t, isDay); -}; + return date_scheduler(this, t, isDay) +} /** * 当前时间与之前的时间差值 @@ -24,16 +24,20 @@ Date.prototype.scheduler = function (t: int, isDay?: boolean): Date { * @param unit 单位: days | minutes */ Date.prototype.diff = function (pre: Date, unit: unit): int { - return date_diff(this, pre, unit) as int; -}; + return date_diff(this, pre, unit) as int +} Date.prototype.format = function (): string { - return formatDate(this); -}; + return formatDate(this) +} -Date.prototype.dueFormat = function (last_review: Date, unit?: boolean,timeUnit?: string[]) { - return show_diff_message(this, last_review, unit, timeUnit); -}; +Date.prototype.dueFormat = function ( + last_review: Date, + unit?: boolean, + timeUnit?: string[] +) { + return show_diff_message(this, last_review, unit, timeUnit) +} /** * 计算日期和时间的偏移,并返回一个新的日期对象。 @@ -42,124 +46,132 @@ Date.prototype.dueFormat = function (last_review: Date, unit?: boolean,timeUnit? * @param isDay (可选)是否按天数单位进行偏移,默认为 false,表示按分钟单位计算偏移 * @returns 偏移后的日期和时间对象 */ -export function date_scheduler(now: DateInput, t: number, isDay?: boolean): Date { +export function date_scheduler( + now: DateInput, + t: number, + isDay?: boolean +): Date { return new Date( isDay ? fixDate(now).getTime() + t * 24 * 60 * 60 * 1000 - : fixDate(now).getTime() + t * 60 * 1000, - ); + : fixDate(now).getTime() + t * 60 * 1000 + ) } export function date_diff(now: DateInput, pre: DateInput, unit: unit): number { if (!now || !pre) { - throw new Error("Invalid date"); + throw new Error('Invalid date') } - const diff = fixDate(now).getTime() - fixDate(pre).getTime(); - let r = 0; + const diff = fixDate(now).getTime() - fixDate(pre).getTime() + let r = 0 switch (unit) { - case "days": - r = Math.floor(diff / (24 * 60 * 60 * 1000)); - break; - case "minutes": - r = Math.floor(diff / (60 * 1000)); - break; + case 'days': + r = Math.floor(diff / (24 * 60 * 60 * 1000)) + break + case 'minutes': + r = Math.floor(diff / (60 * 1000)) + break } - return r; + return r } export function formatDate(dateInput: DateInput): string { - const date = fixDate(dateInput); - const year: number = date.getFullYear(); - const month: number = date.getMonth() + 1; - const day: number = date.getDate(); - const hours: number = date.getHours(); - const minutes: number = date.getMinutes(); - const seconds: number = date.getSeconds(); + const date = fixDate(dateInput) + const year: number = date.getFullYear() + const month: number = date.getMonth() + 1 + const day: number = date.getDate() + const hours: number = date.getHours() + const minutes: number = date.getMinutes() + const seconds: number = date.getSeconds() return `${year}-${padZero(month)}-${padZero(day)} ${padZero(hours)}:${padZero( - minutes, - )}:${padZero(seconds)}`; + minutes + )}:${padZero(seconds)}` } function padZero(num: number): string { - return num < 10 ? `0${num}` : `${num}`; + return num < 10 ? `0${num}` : `${num}` } -const TIMEUNIT = [60, 60, 24, 31, 12]; -const TIMEUNITFORMAT = ["second", "min", "hour", "day", "month", "year"]; +const TIMEUNIT = [60, 60, 24, 31, 12] +const TIMEUNITFORMAT = ['second', 'min', 'hour', 'day', 'month', 'year'] export function show_diff_message( due: DateInput, last_review: DateInput, unit?: boolean, - timeUnit: string[] = TIMEUNITFORMAT, + timeUnit: string[] = TIMEUNITFORMAT ): string { - due = fixDate(due); - last_review = fixDate(last_review); + due = fixDate(due) + last_review = fixDate(last_review) if (timeUnit.length !== TIMEUNITFORMAT.length) { - timeUnit = TIMEUNITFORMAT; + timeUnit = TIMEUNITFORMAT } - let diff = due.getTime() - last_review.getTime(); - let i; - diff /= 1000; + let diff = due.getTime() - last_review.getTime() + let i + diff /= 1000 for (i = 0; i < TIMEUNIT.length; i++) { if (diff < TIMEUNIT[i]) { - break; + break } else { - diff /= TIMEUNIT[i]; + diff /= TIMEUNIT[i] } } - return `${Math.floor(diff)}${unit ? timeUnit[i] : ""}`; + return `${Math.floor(diff)}${unit ? timeUnit[i] : ''}` } export function fixDate(value: unknown) { - if (typeof value === "object" && value instanceof Date) { - return value; - } else if (typeof value === "string") { - const timestamp = Date.parse(value); + if (typeof value === 'object' && value instanceof Date) { + return value + } else if (typeof value === 'string') { + const timestamp = Date.parse(value) if (!isNaN(timestamp)) { - return new Date(timestamp); + return new Date(timestamp) } else { - throw new Error(`Invalid date:[${value}]`); + throw new Error(`Invalid date:[${value}]`) } - } else if (typeof value === "number") { - return new Date(value); + } else if (typeof value === 'number') { + return new Date(value) } - throw new Error(`Invalid date:[${value}]`); + throw new Error(`Invalid date:[${value}]`) } export function fixState(value: unknown): State { - if (typeof value === "string") { - 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}]`); + if (typeof value === 'string') { + 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; + return ret + } else if (typeof value === 'number') { + return value as State } - throw new Error(`Invalid state:[${value}]`); + throw new Error(`Invalid state:[${value}]`) } export function fixRating(value: unknown): Rating { - if (typeof value === "string") { - const firstLetter = value.charAt(0).toUpperCase(); - const restOfString = value.slice(1).toLowerCase(); + if (typeof value === 'string') { + 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}]`); + if (ret === undefined) { + throw new Error(`Invalid rating:[${value}]`) } - return ret; - } else if (typeof value === "number") { - return value as Rating; + return ret + } else if (typeof value === 'number') { + return value as Rating } - throw new Error(`Invalid rating:[${value}]`); + throw new Error(`Invalid rating:[${value}]`) } - -export const Grades: Readonly = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy] as const; +export const Grades: Readonly = [ + Rating.Again, + Rating.Hard, + Rating.Good, + Rating.Easy, +] as const const FUZZ_RANGES = [ { @@ -177,24 +189,24 @@ const FUZZ_RANGES = [ end: Infinity, factor: 0.05, }, -] as const; +] as const export function get_fuzz_range( interval: number, elapsed_days: number, - maximum_interval: number, + maximum_interval: number ) { - let delta = 1.0; + let delta = 1.0 for (const range of FUZZ_RANGES) { delta += - range.factor * Math.max(Math.min(interval, range.end) - range.start, 0.0); + range.factor * Math.max(Math.min(interval, range.end) - range.start, 0.0) } - interval = Math.min(interval, maximum_interval); - let min_ivl = Math.max(2, Math.round(interval - delta)); - const max_ivl = Math.min(Math.round(interval + delta), maximum_interval); + interval = Math.min(interval, maximum_interval) + let min_ivl = Math.max(2, Math.round(interval - delta)) + const max_ivl = Math.min(Math.round(interval + delta), maximum_interval) if (interval > elapsed_days) { - min_ivl = Math.max(min_ivl, elapsed_days + 1); + min_ivl = Math.max(min_ivl, elapsed_days + 1) } - min_ivl = Math.min(min_ivl, max_ivl); - return { min_ivl, max_ivl }; + min_ivl = Math.min(min_ivl, max_ivl) + return { min_ivl, max_ivl } } diff --git a/src/fsrs/index.ts b/src/fsrs/index.ts index ae58623..1bfde74 100644 --- a/src/fsrs/index.ts +++ b/src/fsrs/index.ts @@ -1,10 +1,10 @@ -export * from "./scheduler"; -export * from "./default"; -export * from "./help"; -export * from "./algorithm" -export * from "./fsrs"; +export * from './scheduler' +export * from './default' +export * from './help' +export * from './algorithm' +export * from './fsrs' -export type * from "./type"; +export type * from './type' export type { FSRSParameters, Card, @@ -16,6 +16,6 @@ export type { Grade, CardInput, ReviewLogInput, - DateInput -} from "./models"; -export { State, Rating } from "./models"; \ No newline at end of file + DateInput, +} from './models' +export { State, Rating } from './models' diff --git a/src/fsrs/models.ts b/src/fsrs/models.ts index e9dc5f3..fd54350 100644 --- a/src/fsrs/models.ts +++ b/src/fsrs/models.ts @@ -1,4 +1,4 @@ -export type StateType = "New" | "Learning" | "Review" | "Relearning"; +export type StateType = 'New' | 'Learning' | 'Review' | 'Relearning' export enum State { New = 0, @@ -7,7 +7,7 @@ export enum State { Relearning = 3, } -export type RatingType = "Manual" | "Again" | "Hard" | "Good" | "Easy"; +export type RatingType = 'Manual' | 'Again' | 'Hard' | 'Good' | 'Easy' export enum Rating { Manual = 0, @@ -17,66 +17,66 @@ export enum Rating { Easy = 4, } -type ExcludeManual = Exclude; +type ExcludeManual = Exclude -export type Grade = ExcludeManual; +export type Grade = ExcludeManual export interface ReviewLog { - rating: Rating; // Rating of the review (Again, Hard, Good, Easy) - state: State; // State of the review (New, Learning, Review, Relearning) - due: Date; // Date of the last scheduling - stability: number; // Memory stability during the review - difficulty: number; // Difficulty of the card during the review - elapsed_days: number; // Number of days elapsed since the last review - last_elapsed_days: number; // Number of days between the last two reviews - scheduled_days: number; // Number of days until the next review - review: Date; // Date of the review + rating: Rating // Rating of the review (Again, Hard, Good, Easy) + state: State // State of the review (New, Learning, Review, Relearning) + due: Date // Date of the last scheduling + stability: number // Memory stability during the review + difficulty: number // Difficulty of the card during the review + elapsed_days: number // Number of days elapsed since the last review + last_elapsed_days: number // Number of days between the last two reviews + scheduled_days: number // Number of days until the next review + review: Date // Date of the review } export type RecordLogItem = { - card: Card; - log: ReviewLog; -}; + card: Card + log: ReviewLog +} export type RecordLog = { - [key in Grade]: RecordLogItem; -}; + [key in Grade]: RecordLogItem +} export interface Card { - due: Date; // Due date - stability: number; // Stability - difficulty: number; // Difficulty level - elapsed_days: number; // Number of days elapsed - scheduled_days: number; // Number of days scheduled - reps: number; // Repetition count - lapses: number; // Number of lapses or mistakes - state: State; // Card's state (New, Learning, Review, Relearning) - last_review?: Date; // Date of the last review (optional) + due: Date // Due date + stability: number // Stability + difficulty: number // Difficulty level + elapsed_days: number // Number of days elapsed + scheduled_days: number // Number of days scheduled + reps: number // Repetition count + lapses: number // Number of lapses or mistakes + state: State // Card's state (New, Learning, Review, Relearning) + last_review?: Date // Date of the last review (optional) } -export interface CardInput extends Omit { - state: StateType | State; // Card's state (New, Learning, Review, Relearning) - due: DateInput; // Due date - last_review?: DateInput | null; // Date of the last review (optional) +export interface CardInput extends Omit { + state: StateType | State // Card's state (New, Learning, Review, Relearning) + due: DateInput // Due date + last_review?: DateInput | null // Date of the last review (optional) } -export type DateInput = Date | number | string; +export type DateInput = Date | number | string export interface ReviewLogInput - extends Omit { - rating: RatingType | Rating; // Rating of the review (Again, Hard, Good, Easy) - state: StateType | State; // Card's state (New, Learning, Review, Relearning) - due: DateInput; // Due date - review: DateInput; // Date of the last review + extends Omit { + rating: RatingType | Rating // Rating of the review (Again, Hard, Good, Easy) + state: StateType | State // Card's state (New, Learning, Review, Relearning) + due: DateInput // Due date + review: DateInput // Date of the last review } export interface FSRSParameters { - request_retention: number; - maximum_interval: number; - w: number[]; - enable_fuzz: boolean; + request_retention: number + maximum_interval: number + w: number[] + enable_fuzz: boolean } export type RescheduleOptions = { - enable_fuzz?: boolean; - dateHandler?: (date: Date) => DateInput; -}; \ No newline at end of file + enable_fuzz?: boolean + dateHandler?: (date: Date) => DateInput +} diff --git a/src/fsrs/scheduler.ts b/src/fsrs/scheduler.ts index d93ecab..c4a4a17 100644 --- a/src/fsrs/scheduler.ts +++ b/src/fsrs/scheduler.ts @@ -1,72 +1,72 @@ -import { Card, Rating, RecordLog, State } from "./models"; -import { date_scheduler } from "./help"; +import { Card, Rating, RecordLog, State } from './models' +import { date_scheduler } from './help' export class SchedulingCard { - again: Card; - hard: Card; - good: Card; - easy: Card; - last_review: Date; - last_elapsed_days: number; + again: Card + hard: Card + good: Card + easy: Card + last_review: Date + last_elapsed_days: number private copy(card: Card): Card { return { ...card, - }; + } } constructor(card: Card, now: Date) { - this.last_review = card.last_review || card.due; - this.last_elapsed_days = card.elapsed_days; + this.last_review = card.last_review || card.due + this.last_elapsed_days = card.elapsed_days card.elapsed_days = - card.state === State.New ? 0 : now.diff(card.last_review as Date, "days"); //相距时间 - card.last_review = now; // 上次复习时间 - card.reps += 1; - this.again = this.copy(card); - this.hard = this.copy(card); - this.good = this.copy(card); - this.easy = this.copy(card); + card.state === State.New ? 0 : now.diff(card.last_review as Date, 'days') //相距时间 + card.last_review = now // 上次复习时间 + card.reps += 1 + this.again = this.copy(card) + this.hard = this.copy(card) + this.good = this.copy(card) + this.easy = this.copy(card) } update_state(state: State) { if (state === State.New) { - this.again.state = State.Learning; - this.hard.state = State.Learning; - this.good.state = State.Learning; - this.easy.state = State.Review; + this.again.state = State.Learning + this.hard.state = State.Learning + this.good.state = State.Learning + this.easy.state = State.Review } else if (state === State.Learning || state === State.Relearning) { - this.again.state = state; - this.hard.state = state; - this.good.state = State.Review; - this.easy.state = State.Review; + this.again.state = state + this.hard.state = state + this.good.state = State.Review + this.easy.state = State.Review } else if (state === State.Review) { - this.again.state = State.Relearning; - this.hard.state = State.Review; - this.good.state = State.Review; - this.easy.state = State.Review; - this.again.lapses += 1; + this.again.state = State.Relearning + this.hard.state = State.Review + this.good.state = State.Review + this.easy.state = State.Review + this.again.lapses += 1 } - return this; + return this } schedule( now: Date, hard_interval: number, good_interval: number, - easy_interval: number, + easy_interval: number ): SchedulingCard { - this.again.scheduled_days = 0; - this.hard.scheduled_days = hard_interval; - this.good.scheduled_days = good_interval; - this.easy.scheduled_days = easy_interval; - this.again.due = date_scheduler(now, 5); + this.again.scheduled_days = 0 + this.hard.scheduled_days = hard_interval + this.good.scheduled_days = good_interval + this.easy.scheduled_days = easy_interval + this.again.due = date_scheduler(now, 5) this.hard.due = hard_interval > 0 ? date_scheduler(now, hard_interval, true) - : date_scheduler(now, 10); - this.good.due = date_scheduler(now, good_interval, true); - this.easy.due = date_scheduler(now, easy_interval, true); - return this; + : date_scheduler(now, 10) + this.good.due = date_scheduler(now, good_interval, true) + this.easy.due = date_scheduler(now, easy_interval, true) + return this } record_log(card: Card, now: Date): RecordLog { @@ -127,6 +127,6 @@ export class SchedulingCard { review: now, }, }, - }; + } } } diff --git a/src/fsrs/type.ts b/src/fsrs/type.ts index 4aced0a..f84e8c0 100644 --- a/src/fsrs/type.ts +++ b/src/fsrs/type.ts @@ -1,3 +1,3 @@ -export type unit = "days" | "minutes"; -export type int = number & { __int__: void }; -export type double = number & { __double__: void }; +export type unit = 'days' | 'minutes' +export type int = number & { __int__: void } +export type double = number & { __double__: void }