Skip to content

Commit

Permalink
Feat/Impl initSeed& Scheduler extension based on the strategy pat…
Browse files Browse the repository at this point in the history
…tern (#137)

* Refactor/implement seed strategy and scheduler enhancements

* export Scheduler typo

* rename strategy->strategies

* update card_id_field type to string|number

* rename GenCardIdSeedStrategy->GenSeedStrategyWithCardId

* add test

* update typo

* fix instace-> instance
  • Loading branch information
ishiko732 authored Nov 30, 2024
1 parent 8137bdf commit 2845647
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 19 deletions.
185 changes: 185 additions & 0 deletions __tests__/strategies/seed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import {
AbstractScheduler,
Card,
createEmptyCard,
DefaultInitSeedStrategy,
fsrs,
GenSeedStrategyWithCardId,
Rating,
StrategyMode,
} from '../../src/fsrs'

interface ICard extends Card {
card_id: number
}

describe('seed strategy', () => {
it('default seed strategy', () => {
const seedStrategy = DefaultInitSeedStrategy
const f = fsrs().useStrategy(StrategyMode.SEED, seedStrategy)
const now = Date.UTC(2022, 11, 29, 12, 30, 0, 0)

const card = createEmptyCard<ICard>(now, (card: Card) => {
Object.assign(card, { card_id: 555 })
return card as ICard
})

const record = f.repeat(card, now)
const scheduler = new f['Scheduler'](card, now, f, {
seed: seedStrategy,
}) as AbstractScheduler

const seed = seedStrategy.bind(scheduler)()
console.debug('seed', seed)

expect(f['_seed']).toBe(seed)
})
})

describe('seed strategy with card ID', () => {
it('use seedStrategy', () => {
const seedStrategy = GenSeedStrategyWithCardId('card_id')
const f = fsrs().useStrategy(StrategyMode.SEED, seedStrategy)

expect(f['strategyHandler'].get(StrategyMode.SEED)).toBe(seedStrategy)

f.clearStrategy()
expect(f['strategyHandler'].get(StrategyMode.SEED)).toBeUndefined()
})
it('clear seedStrategy', () => {
const seedStrategy = GenSeedStrategyWithCardId('card_id')
const f = fsrs().useStrategy(StrategyMode.SEED, seedStrategy)
const now = Date.UTC(2022, 11, 29, 12, 30, 0, 0)

const card = createEmptyCard<ICard>(now, (card: Card) => {
Object.assign(card, { card_id: 555 })
return card as ICard
})

f.repeat(card, now)
let scheduler = new f['Scheduler'](card, now, f, {
seed: seedStrategy,
}) as AbstractScheduler

const seed_with_card_id = seedStrategy.bind(scheduler)()
console.debug('seed with card_id=555', seed_with_card_id)

f.clearStrategy(StrategyMode.SEED)

f.repeat(card, now)
scheduler = new f['Scheduler'](card, now, f, {
seed: DefaultInitSeedStrategy,
}) as AbstractScheduler
const basic_seed = DefaultInitSeedStrategy.bind(scheduler)()
console.debug('basic_seed with card_id=555', basic_seed)

expect(f['_seed']).toBe(basic_seed)

expect(seed_with_card_id).not.toBe(basic_seed)
})

it('exist card_id', () => {
const seedStrategy = GenSeedStrategyWithCardId('card_id')
const f = fsrs().useStrategy(StrategyMode.SEED, seedStrategy)
const now = Date.UTC(2022, 11, 29, 12, 30, 0, 0)

const card = createEmptyCard<ICard>(now, (card: Card) => {
Object.assign(card, { card_id: 555 })
return card as ICard
})

const record = f.repeat(card, now)
const scheduler = new f['Scheduler'](card, now, f, {
seed: seedStrategy,
}) as AbstractScheduler

const seed = seedStrategy.bind(scheduler)()
console.debug('seed with card_id=555', seed)

expect(f['_seed']).toBe(seed)
})

it('not exist card_id', () => {
const seedStrategy = GenSeedStrategyWithCardId('card_id')
const f = fsrs().useStrategy(StrategyMode.SEED, seedStrategy)
const now = Date.UTC(2022, 11, 29, 12, 30, 0, 0)

const card = createEmptyCard<ICard>(now)

const record = f.repeat(card, now)
const scheduler = new f['Scheduler'](card, now, f, {
seed: seedStrategy,
}) as AbstractScheduler

const seed = seedStrategy.bind(scheduler)()
console.debug('seed with card_id=undefined(default)', seed)

expect(f['_seed']).toBe(seed)
})

it('card_id = -1', () => {
const seedStrategy = GenSeedStrategyWithCardId('card_id')
const f = fsrs().useStrategy(StrategyMode.SEED, seedStrategy)
const now = Date.UTC(2022, 11, 29, 12, 30, 0, 0)

const card = createEmptyCard<ICard>(now, (card: Card) => {
Object.assign(card, { card_id: -1 })
return card as ICard
})

const record = f.repeat(card, now)
const scheduler = new f['Scheduler'](card, now, f, {
seed: seedStrategy,
}) as AbstractScheduler

const seed = seedStrategy.bind(scheduler)()
console.debug('with card_id=-1', seed)

expect(f['_seed']).toBe(seed)
expect(f['_seed']).toBe('0')
})

it('card_id is undefined', () => {
const seedStrategy = GenSeedStrategyWithCardId('card_id')
const f = fsrs().useStrategy(StrategyMode.SEED, seedStrategy)
const now = Date.UTC(2022, 11, 29, 12, 30, 0, 0)

const card = createEmptyCard<ICard>(now, (card: Card) => {
Object.assign(card, { card_id: undefined })
return card as ICard
})

const item = f.next(card, now, Rating.Good)
const scheduler = new f['Scheduler'](card, now, f, {
seed: seedStrategy,
}) as AbstractScheduler

const seed = seedStrategy.bind(scheduler)()
console.debug('seed with card_id=undefined', seed)

expect(f['_seed']).toBe(seed)
expect(f['_seed']).toBe(`${item.card.reps}`)
})

it('card_id is null', () => {
const seedStrategy = GenSeedStrategyWithCardId('card_id')
const f = fsrs().useStrategy(StrategyMode.SEED, seedStrategy)
const now = Date.UTC(2022, 11, 29, 12, 30, 0, 0)

const card = createEmptyCard<ICard>(now, (card: Card) => {
Object.assign(card, { card_id: null })
return card as ICard
})

const item = f.next(card, now, Rating.Good)
const scheduler = new f['Scheduler'](card, now, f, {
seed: seedStrategy,
}) as AbstractScheduler

const seed = seedStrategy.bind(scheduler)()
console.debug('seed with card_id=null', seed)

expect(f['_seed']).toBe(seed)
expect(f['_seed']).toBe(`${item.card.reps}`)
})
})
20 changes: 11 additions & 9 deletions src/fsrs/abstract_scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
type CardInput,
type DateInput,
} from './models'
import { DefaultInitSeedStrategy } from './strategies'
import type { TSeedStrategy } from './strategies/types'
import type { IPreview, IScheduler } from './types'

export abstract class AbstractScheduler implements IScheduler {
Expand All @@ -19,13 +21,20 @@ export abstract class AbstractScheduler implements IScheduler {
protected review_time: Date
protected next: Map<Grade, RecordLogItem> = new Map()
protected algorithm: FSRSAlgorithm
private initSeedStrategy: TSeedStrategy

constructor(
card: CardInput | Card,
now: DateInput,
algorithm: FSRSAlgorithm
algorithm: FSRSAlgorithm,
strategies: {
seed: TSeedStrategy
} = {
seed: DefaultInitSeedStrategy,
}
) {
this.algorithm = algorithm
this.initSeedStrategy = strategies.seed.bind(this)

this.last = TypeConvert.card(card)
this.current = TypeConvert.card(card)
Expand All @@ -42,7 +51,7 @@ export abstract class AbstractScheduler implements IScheduler {
this.current.last_review = this.review_time
this.current.elapsed_days = interval
this.current.reps += 1
this.initSeed()
this.algorithm.seed = this.initSeedStrategy()
}

public preview(): IPreview {
Expand Down Expand Up @@ -88,13 +97,6 @@ export abstract class AbstractScheduler implements IScheduler {

protected abstract reviewState(grade: Grade): RecordLogItem

private initSeed() {
const time = this.review_time.getTime()
const reps = this.current.reps
const mul = this.current.difficulty * this.current.stability
this.algorithm.seed = `${time}_${reps}_${mul}`
}

protected buildLog(rating: Grade): ReviewLog {
const { last_review, due, elapsed_days } = this.last

Expand Down
63 changes: 55 additions & 8 deletions src/fsrs/fsrs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,29 @@ import {
ReviewLogInput,
State,
} from './models'
import type { IPreview, IReschedule, RescheduleOptions } from './types'
import {
type IPreview,
type IReschedule,
type RescheduleOptions,
type IScheduler,
} from './types'
import { FSRSAlgorithm } from './algorithm'
import { TypeConvert } from './convert'
import BasicScheduler from './impl/basic_scheduler'
import LongTermScheduler from './impl/long_term_scheduler'
import { createEmptyCard } from './default'
import { Reschedule } from './reschedule'
import { DefaultInitSeedStrategy } from './strategies/seed'
import {
StrategyMode,
type TSeedStrategy,
type TSchedulerStrategy,
type TStrategyHandler,
} from './strategies/types'

export class FSRS extends FSRSAlgorithm {
private Scheduler
private strategyHandler = new Map<StrategyMode, TStrategyHandler>()
private Scheduler: TSchedulerStrategy
constructor(param: Partial<FSRSParameters>) {
super(param)
const { enable_short_term } = this.parameters
Expand Down Expand Up @@ -48,6 +61,42 @@ export class FSRS extends FSRSAlgorithm {
}
}

useStrategy<T extends StrategyMode>(
mode: T,
handler: TStrategyHandler<T>
): this {
this.strategyHandler.set(mode, handler)
return this
}

clearStrategy(mode?: StrategyMode): this {
if (mode) {
this.strategyHandler.delete(mode)
} else {
this.strategyHandler.clear()
}
return this
}

private getScheduler(card: CardInput | Card, now: DateInput): IScheduler {
const seedStrategy = this.strategyHandler.get(StrategyMode.SEED) as
| TSeedStrategy
| undefined

// Strategy scheduler
const schedulerStrategy = this.strategyHandler.get(
StrategyMode.SCHEDULER
) as TSchedulerStrategy | undefined

const Scheduler = schedulerStrategy || this.Scheduler
const Seed = seedStrategy || DefaultInitSeedStrategy
const instance = new Scheduler(card, now, this, {
seed: Seed,
})

return instance
}

/**
* Display the collection of cards and logs for the four scenarios after scheduling the card at the current time.
* @param card Card to be processed
Expand Down Expand Up @@ -111,9 +160,8 @@ export class FSRS extends FSRSAlgorithm {
now: DateInput,
afterHandler?: (recordLog: IPreview) => R
): R {
const Scheduler = this.Scheduler
const instace = new Scheduler(card, now, this satisfies FSRSAlgorithm)
const recordLog = instace.preview()
const instance = this.getScheduler(card, now)
const recordLog = instance.preview()
if (afterHandler && typeof afterHandler === 'function') {
return afterHandler(recordLog)
} else {
Expand Down Expand Up @@ -181,13 +229,12 @@ export class FSRS extends FSRSAlgorithm {
grade: Grade,
afterHandler?: (recordLog: RecordLogItem) => R
): R {
const Scheduler = this.Scheduler
const instace = new Scheduler(card, now, this satisfies FSRSAlgorithm)
const instance = this.getScheduler(card, now)
const g = TypeConvert.rating(grade)
if (g === Rating.Manual) {
throw new Error('Cannot review a manual rating')
}
const recordLogItem = instace.review(g)
const recordLogItem = instance.review(g)
if (afterHandler && typeof afterHandler === 'function') {
return afterHandler(recordLogItem)
} else {
Expand Down
5 changes: 5 additions & 0 deletions src/fsrs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ export type {
export { State, Rating } from './models'

export * from './convert'

export * from './strategies'
export * from './abstract_scheduler'
export * from './impl/basic_scheduler'
export * from './impl/long_term_scheduler'
2 changes: 2 additions & 0 deletions src/fsrs/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './seed'
export * from './types'
Loading

0 comments on commit 2845647

Please sign in to comment.