Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/long-term schduler #100

Merged
merged 13 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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