Skip to content

Commit

Permalink
Feat/long-term schduler (#100)
Browse files Browse the repository at this point in the history
* add enable_short_term parameters

* impl long-term schduler

* fix generatorParameters

* fix interval

* add test

* fix interval

* update test

* fix next_again.due

* update learningState

* bump ts-fsrs to 4.0.0

* check invalid grade
  • Loading branch information
ishiko732 authored Jul 25, 2024
1 parent dac7f7e commit 9285e33
Show file tree
Hide file tree
Showing 10 changed files with 618 additions and 22 deletions.
50 changes: 49 additions & 1 deletion __tests__/algorithm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ describe('FSRS apply_fuzz', () => {
})

describe('change Params', () => {
test('change FSRSParameters', () => {
test('change FSRSParameters[FSRS]', () => {
const f = fsrs()
// I(r,s),r=0.9 then I(r,s)=s
expect(f.interval_modifier).toEqual(1)
Expand Down Expand Up @@ -366,6 +366,54 @@ describe('change Params', () => {

f.parameters = {} // check default values
expect(f.parameters).toEqual(generatorParameters())

f.parameters.enable_short_term = false
expect(f.parameters.enable_short_term).toEqual(false)
})

test('change FSRSParameters[FSRSAlgorithm]', () => {
const params = generatorParameters()
const f = new FSRSAlgorithm(params)
// I(r,s),r=0.9 then I(r,s)=s
expect(f.interval_modifier).toEqual(1)
expect(f.parameters).toEqual(generatorParameters())

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, 0.48, 0.64,
]
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.interval_modifier).toEqual(
f.calculate_interval_modifier(request_retention)
)

f.parameters.request_retention = default_request_retention
expect(f.interval_modifier).toEqual(
f.calculate_interval_modifier(default_request_retention)
)

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.enable_fuzz = default_enable_fuzz
expect(f.parameters.enable_fuzz).toEqual(default_enable_fuzz)

f.parameters = {} // check default values
expect(f.parameters).toEqual(generatorParameters())

f.parameters.enable_short_term = false
expect(f.parameters.enable_short_term).toEqual(false)
})

test('calculate_interval_modifier', () => {
Expand Down
248 changes: 248 additions & 0 deletions __tests__/impl/long-term_schduler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import {
createEmptyCard,
fsrs,
FSRSAlgorithm,
generatorParameters,
Grade,
Rating,
State,
} from '../../src/fsrs'
import LongTermScheduler from '../../src/fsrs/impl/long_term_schduler'

describe('Long-term schduler', () => {
const w = [
0.4197, 1.1869, 3.0412, 15.2441, 7.1434, 0.6477, 1.0007, 0.0674, 1.6597,
0.1712, 1.1178, 2.0225, 0.0904, 0.3025, 2.1214, 0.2498, 2.9466, 0.4891,
0.6468,
]
const params = generatorParameters({ w, enable_short_term: false })
const f = fsrs(params)
// Grades => const grade: Grade[] = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy]

test('test1', () => {
let card = createEmptyCard()
let now = new Date(2022, 11, 29, 12, 30, 0, 0)
const ratings: Grade[] = [
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Again,
Rating.Again,
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Good,
Rating.Good,
]
const ivl_history: number[] = []
const s_history: number[] = []
const d_history: number[] = []
for (const rating of ratings) {
const record = f.repeat(card, now)[rating]
card = record.card
ivl_history.push(card.scheduled_days)
s_history.push(card.stability)
d_history.push(card.difficulty)
now = card.due
}

expect(ivl_history).toEqual([3, 6, 17, 42, 95, 200, 8, 2, 3, 5, 8, 14, 23])
expect(s_history).toEqual([
0.57587467, 6.28341418, 16.83356103, 41.95128557, 95.07063986,
199.53765138, 8.31519008, 1.96276113, 3.06877302, 4.90880017, 8.15177579,
13.50873393, 22.92901865,
])
expect(d_history).toEqual([
7.1434, 7.1434, 7.1434, 7.1434, 7.1434, 7.1434, 9.00990564, 10,
9.80746516, 9.62790717, 9.46045139, 9.30428213, 9.15863867,
])
})

test('test2', () => {
let card = createEmptyCard()
let now = new Date(2022, 11, 29, 12, 30, 0, 0)
const ratings: Grade[] = [
Rating.Again,
Rating.Hard,
Rating.Good,
Rating.Easy,
Rating.Again,
Rating.Hard,
Rating.Good,
Rating.Easy,
]
const ivl_history: number[] = []
const s_history: number[] = []
const d_history: number[] = []
for (const rating of ratings) {
const record = f.repeat(card, now)[rating]
card = record.card
ivl_history.push(card.scheduled_days)
s_history.push(card.stability)
d_history.push(card.difficulty)
now = card.due
}
expect(ivl_history).toEqual([1, 2, 3, 8, 2, 2, 4, 10])

expect(s_history).toEqual([
0.21652154, 0.51780862, 1.8183783, 8.18593986, 1.96087115, 2.23717242,
3.33185406, 10.31008873,
])
expect(d_history).toEqual([
9.00990564, 9.81735598, 9.63713135, 8.53580104, 10, 10, 9.80746516,
8.69465435,
])
})

test('test3', () => {
let card = createEmptyCard()
let now = new Date(2022, 11, 29, 12, 30, 0, 0)
const ratings: Grade[] = [
Rating.Hard,
Rating.Good,
Rating.Easy,
Rating.Again,
Rating.Hard,
Rating.Good,
Rating.Easy,
Rating.Again,
]
const ivl_history: number[] = []
const s_history: number[] = []
const d_history: number[] = []
for (const rating of ratings) {
const record = f.repeat(card, now)[rating]
card = record.card
ivl_history.push(card.scheduled_days)
s_history.push(card.stability)
d_history.push(card.difficulty)
now = card.due
}
expect(ivl_history).toEqual([2, 3, 17, 3, 4, 6, 18, 3])

expect(s_history).toEqual([
0.35311368, 3.40179546, 16.86596974, 2.9223039, 3.73601023, 6.27595217,
18.057595, 2.96541083,
])
expect(d_history).toEqual([
8.07665282, 8.01375158, 7.02183706, 8.89653604, 9.71162749, 9.53852896,
8.44384445, 10,
])
})

test('test4', () => {
let card = createEmptyCard()
let now = new Date(2022, 11, 29, 12, 30, 0, 0)
const ratings: Grade[] = [
Rating.Good,
Rating.Easy,
Rating.Again,
Rating.Hard,
Rating.Good,
Rating.Easy,
Rating.Again,
Rating.Hard,
]
const ivl_history: number[] = []
const s_history: number[] = []
const d_history: number[] = []
for (const rating of ratings) {
const record = f.repeat(card, now)[rating]
card = record.card
ivl_history.push(card.scheduled_days)
s_history.push(card.stability)
d_history.push(card.difficulty)
now = card.due
}

expect(ivl_history).toEqual([3, 17, 3, 4, 8, 30, 4, 4])

expect(s_history).toEqual([
0.57587467, 17.3937106, 2.98322161, 4.08808234, 7.99405969, 29.76078201,
3.78204811, 4.44627015,
])
expect(d_history).toEqual([
7.1434, 6.21014718, 8.13955406, 9.0056661, 8.88014936, 7.82983963,
9.65007924, 10,
])
})
test('test5', () => {
let card = createEmptyCard()
let now = new Date(2022, 11, 29, 12, 30, 0, 0)
const ratings: Grade[] = [
Rating.Easy,
Rating.Again,
Rating.Hard,
Rating.Good,
Rating.Easy,
Rating.Again,
Rating.Hard,
Rating.Good,
]
const ivl_history: number[] = []
const s_history: number[] = []
const d_history: number[] = []
for (const rating of ratings) {
const record = f.repeat(card, now)[rating]
card = record.card
ivl_history.push(card.scheduled_days)
s_history.push(card.stability)
d_history.push(card.difficulty)
now = card.due
}

expect(ivl_history).toEqual([4, 1, 2, 3, 13, 3, 3, 5])

expect(s_history).toEqual([
0.93916394, 0.70772253, 1.16119435, 3.42301186, 12.97577367, 2.53486926,
3.06202367, 4.60523893,
])
expect(d_history).toEqual([
6.21014718, 8.13955406, 9.0056661, 8.88014936, 7.82983963, 9.65007924, 10,
9.80746516,
])
})

test('[State.(Re)Learning]switch long-term schduler', () => {
// Good(short)->Good(long)->Again(long)->Good(long)->Good(short)->Again(short)
const ivl_history: number[] = []
const s_history: number[] = []
const d_history: number[] = []
const state_history: string[] = []

const grades: Grade[] = [
Rating.Good,
Rating.Good,
Rating.Again,
Rating.Good,
Rating.Good,
Rating.Again,
]
const short_term = [true, false, false, false, true, true]

let now = new Date(2022, 11, 29, 12, 30, 0, 0)
let card = createEmptyCard(now)
const f = fsrs({ w })
for (let i = 0; i < grades.length; i++) {
const grade = grades[i]
const enable = short_term[i]
f.parameters.enable_short_term = enable
const record = f.repeat(card, now)[grade]
card = record.card
now = card.due
ivl_history.push(card.scheduled_days)
s_history.push(card.stability)
d_history.push(card.difficulty)
state_history.push(State[card.state])
}
expect(ivl_history).toEqual([0, 4, 1, 4, 12, 0])
expect(s_history).toEqual([
3.0412, 3.0412, 1.20788692, 3.83856852, 12.23542321, 2.48288917,
])
expect(d_history).toEqual([4.49094334, 4.66971892, 6.70295066, 6.73263695,6.76032238,8.65264745])
expect(state_history).toEqual(['Learning', 'Review', 'Review', 'Review','Review','Relearning'])
})
})
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-fsrs",
"version": "3.5.7",
"version": "4.0.0",
"description": "ts-fsrs is a versatile package based on TypeScript that supports ES modules, CommonJS, and UMD. It implements the Free Spaced Repetition Scheduler (FSRS) algorithm, enabling developers to integrate FSRS into their flashcard applications to enhance the user learning experience.",
"main": "dist/index.cjs",
"umd": "dist/index.umd.js",
Expand Down
21 changes: 12 additions & 9 deletions src/fsrs/abstract_schduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,23 @@ export abstract class AbstractScheduler implements IScheduler {
}
public review(grade: Grade): RecordLogItem {
const { state } = this.last
let item: RecordLogItem | undefined
switch (state) {
case State.New:
return this.newState(grade)
item = this.newState(grade)
break
case State.Learning:
case State.Relearning:
return this.learningState(grade)
case State.Review: {
const item = this.reviewState(grade)
if (!item) {
throw new Error('Invalid grade')
}
return item
}
item = this.learningState(grade)
break
case State.Review:
item = this.reviewState(grade)
break
}
if (item) {
return item
}
throw new Error('Invalid grade')
}

protected abstract newState(grade: Grade): RecordLogItem
Expand Down
2 changes: 1 addition & 1 deletion src/fsrs/algorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class FSRSAlgorithm {
this.update_parameters(params)
}

private params_handler_proxy(): ProxyHandler<FSRSParameters> {
protected params_handler_proxy(): ProxyHandler<FSRSParameters> {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this: FSRSAlgorithm = this
return {
Expand Down
9 changes: 6 additions & 3 deletions src/fsrs/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ export const default_w = [
0.6468,
]
export const default_enable_fuzz = false
export const defualt_enable_short_term = true

export const FSRSVersion: string = 'v3.5.7 using FSRS V5.0'
export const FSRSVersion: string = 'v4.0.0 using FSRS V5.0'

export const generatorParameters = (
props?: Partial<FSRSParameters>
Expand All @@ -28,8 +29,10 @@ export const generatorParameters = (
request_retention: props?.request_retention || default_request_retention,
maximum_interval: props?.maximum_interval || default_maximum_interval,
w: w,
enable_fuzz: props?.enable_fuzz || default_enable_fuzz,
}
enable_fuzz: props?.enable_fuzz ?? default_enable_fuzz,
enable_short_term:
props?.enable_short_term ?? defualt_enable_short_term,
} satisfies FSRSParameters
}

/**
Expand Down
Loading

0 comments on commit 9285e33

Please sign in to comment.