Skip to content

Commit

Permalink
add reschedule and postpone
Browse files Browse the repository at this point in the history
  • Loading branch information
Newdea committed Feb 12, 2024
1 parent f0f8f57 commit 4052a40
Show file tree
Hide file tree
Showing 13 changed files with 260 additions and 50 deletions.
9 changes: 0 additions & 9 deletions src/algorithms/algorithms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export enum algorithmNames {
export abstract class SrsAlgorithm {
settings: unknown;
// plugin: SRPlugin;
private dueDates: { [type: string]: Record<number, number> };
public static instance: SrsAlgorithm;

public static getInstance(): SrsAlgorithm {
Expand All @@ -27,14 +26,6 @@ export abstract class SrsAlgorithm {
// this.plugin = plugin;
SrsAlgorithm.instance = this;
}
setDueDates(notedueDates: Record<number, number>, carddueDates: Record<number, number>) {
this.dueDates = {};
this.dueDates[RPITEMTYPE.NOTE] = notedueDates;
this.dueDates[RPITEMTYPE.CARD] = carddueDates;
}
getDueDates(itemType: string) {
return this.dueDates && itemType in this.dueDates ? this.dueDates[itemType] : undefined;
}

abstract defaultSettings(): unknown;
abstract defaultData(): unknown;
Expand Down
2 changes: 1 addition & 1 deletion src/algorithms/algorithms_switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export async function algorithmSwitchData(
try {
const algo = algorithms[toAlgo];
algo.updateSettings(plugin.data.settings.algorithmSettings[toAlgo]);
algo.setDueDates(plugin.noteStats.delayedDays.dict, plugin.cardStats.delayedDays.dict);
// algo.setDueDates(plugin.noteStats.delayedDays.dict, plugin.cardStats.delayedDays.dict);
algo.importer(fromAlgo, items);
if (toAlgo === algorithmNames.Fsrs) {
store.data.items.find((item) => {
Expand Down
2 changes: 1 addition & 1 deletion src/algorithms/anki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export class AnkiAlgorithm extends SrsAlgorithm {

data.ease = MiscUtils.fixed(data.ease, 3);
data.iteration += 1;
nextInterval = balance(nextInterval, this.getDueDates(item.itemType));
// nextInterval = balance(nextInterval, item.itemType);
data.lastInterval = nextInterval;

return {
Expand Down
35 changes: 29 additions & 6 deletions src/algorithms/balance/balance.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
import { Notice } from "obsidian";
import { RPITEMTYPE } from "src/dataStore/repetitionItem";

let dueDatesDict: { [type: string]: Record<number, number> };

export function setDueDates(
notedueDates: Record<number, number>,
carddueDates: Record<number, number>,
) {
dueDatesDict = {};
dueDatesDict[RPITEMTYPE.NOTE] = notedueDates;
dueDatesDict[RPITEMTYPE.CARD] = carddueDates;
}

function getDueDates(itemType: string) {
return dueDatesDict && itemType in dueDatesDict ? dueDatesDict[itemType] : undefined;
}

/**
* balance review counts in a day, return new interval day.
Expand All @@ -9,14 +25,16 @@ import { Notice } from "obsidian";
*/
export function balance(
interval: number,
dueDates: Record<number, number>,
type: RPITEMTYPE,
// dueDates: Record<number, number>,
maximumInterval: number = 36525,
lowestCount: number = 10,
tolerance: number = 5,
): number {
// replaces random fuzz with load balancing over the fuzz interval
const beforeIntvl = interval;
let isChange = false;
let dueDates = dueDatesDict[type];
if (dueDates !== undefined) {
interval = Math.round(interval);
// const due = window.moment().add(interval,"days");
Expand All @@ -26,11 +44,8 @@ export function balance(
dueDates[interval] = 0;
} else if (dueDates[interval] >= lowestCount) {
// disable fuzzing for small intervals
if (interval > 4) {
let fuzz = 0;
if (interval < 7) fuzz = 1;
else if (interval < 30) fuzz = Math.max(2, Math.floor(interval * 0.15));
else fuzz = Math.max(4, Math.floor(interval * 0.05));
if (interval >= 1) {
let fuzz = getFuzz(interval);

const originalInterval = interval;
outer: for (let i = 1; i <= fuzz; i++) {
Expand Down Expand Up @@ -62,3 +77,11 @@ export function balance(
}
return interval;
}

function getFuzz(interval: number) {
let fuzz = 0;
if (interval < 7) fuzz = 1;
else if (interval < 30) fuzz = Math.max(2, Math.floor(interval * 0.15));
else fuzz = Math.max(4, Math.floor(interval * 0.05));
return fuzz;
}
60 changes: 60 additions & 0 deletions src/algorithms/balance/postpone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { RepetitionItem } from "src/dataStore/repetitionItem";
import { DateUtils, debug } from "src/util/utils_recall";

export function postponeItems(
items: RepetitionItem[],
cnt?: number,
days?: number,
): RepetitionItem[] {
const now = Date.now();
const newdue: number = days ? now + days * DateUtils.DAYS_TO_MILLIS : undefined;
const fltItems = items
.filter((item) => item.isTracked && item.nextReview < DateUtils.StartofToday)
.sort((a, b) => currentRetention(a) - currentRetention(b))
// https://github.com/open-spaced-repetition/fsrs4anki-helper/blob/58bcfcf8b5eeb60835c5cbde1d0d0ef769af62b0/schedule/postpone.py#L73
.filter((item) => {
// currentR>0.65
// const rate = (1 / currentRetention(item) - 1) / (1 / 0.9 - 1) - 1;
// const rate = currentRetention(item) / 0.9 - 1;
console.debug("current R:", currentRetention(item), 1 / currentRetention(item) - 1);
return currentRetention(item) > 0.65;
});
const safe_cnt = fltItems.length;
debug("postpone", 0, { safe_cnt });
postpone(fltItems, newdue);
return items;
}

function elapsed_days(item: RepetitionItem) {
const delay = (Date.now() - item.nextReview) / DateUtils.DAYS_TO_MILLIS;
return item.interval + delay;
}
function currentRetention(item: RepetitionItem) {
// from fsrs.js repeat retrievability
return Math.pow(1 + elapsed_days(item) / (9 * item.interval), -1);
}

function postpone(items: RepetitionItem[], newdue?: number): RepetitionItem[] {
let cnt = 0;
items.map((item) => {
let newitvl: number,
olastview = item.hasDue ? item.nextReview - item.interval*DateUtils.DAYS_TO_MILLIS : Date.now();

// reschedule, request Retention=0.9
// let interval = item.interval * 9 * (1 / 0.9 - 1);
// newitvl = Math.min(Math.max(Math.round(interval), 1), 3650);

const delay = (Date.now() - olastview) / DateUtils.DAYS_TO_MILLIS - item.interval;
newitvl = Math.min(
Math.max(1, Math.ceil(item.interval * (1.05 + 0.05 * Math.random())) + delay),
36500,
);
// newdue = newdue ? newdue : Date.now() + newitvl * DateUtils.DAYS_TO_MILLIS;
if (newitvl !== item.interval) {
cnt++;
item.updateDueInterval(newitvl, newdue);
}
});
debug("postpone", 0, { cnt });
return items;
}
54 changes: 54 additions & 0 deletions src/algorithms/balance/reschedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { RepetitionItem } from "src/dataStore/repetitionItem";
import { DateUtils, debug } from "src/util/utils_recall";
import { SrsAlgorithm } from "../algorithms";
import { FsrsAlgorithm, FsrsData } from "../fsrs";

export function reschedule(items: RepetitionItem[]): RepetitionItem[] {
let reCnt = 0;
console.group(`reschedule`);
if (items[0].isFsrs) {
const result = reschedule_fsrs(items);
reCnt = result.reCnt;
} else {
reCnt=reschedule_default(items).reCnt;
}
console.groupEnd();
debug("reschedule", 0, { items, reCnt });
return items;
}

function reschedule_default(items: RepetitionItem[]) {
let reCnt = 0;

items.map((item) => {
let newitvl: number;

let interval = item.interval * 9 * (1 / 0.9 - 1);
newitvl = Math.min(Math.max(Math.round(interval), 1), 3650);
if (newitvl !== item.interval) {
reCnt++;
item.updateDueInterval(newitvl);
}
});

// debug("reschedule", 0, { items, reCnt });
return { items, reCnt };
}

function reschedule_fsrs(items: RepetitionItem[]) {
let reCnt = 0;
let fsrs = (SrsAlgorithm.getInstance() as FsrsAlgorithm).fsrs;

items.map((item) => {
let newitvl: number;
if (!item.isTracked) return;
const data = item.data as FsrsData;
newitvl = fsrs.next_interval(data.stability);
if (newitvl !== data.scheduled_days) {
reCnt++;
item.updateDueInterval(newitvl);
}
});

return { items, reCnt };
}
16 changes: 0 additions & 16 deletions src/algorithms/fsrs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { t } from "src/lang/helpers";
import deepcopy from "deepcopy";
import { AnkiData } from "./anki";
import { Rating, ReviewLog } from "fsrs.js";
import { balance } from "./balance/balance";
import { RepetitionItem, ReviewResult } from "src/dataStore/repetitionItem";

// https://github.com/mgmeyers/obsidian-kanban/blob/main/src/Settings.ts
Expand Down Expand Up @@ -120,8 +119,6 @@ export class FsrsAlgorithm extends SrsAlgorithm {

calcAllOptsIntervals(item: RepetitionItem) {
const data = item.data as FsrsData;
data.due = new Date(data.due);
data.last_review = new Date(data.last_review);
const card = deepcopy(data);
const now = new Date();
const scheduling_cards = this.fsrs.repeat(card, now);
Expand All @@ -143,8 +140,6 @@ export class FsrsAlgorithm extends SrsAlgorithm {
log: boolean = true,
): ReviewResult {
let data = item.data as FsrsData;
data.due = new Date(data.due);
data.last_review = new Date(data.last_review);
const response = FsrsOptions.indexOf(optionStr) + 1;

let correct = true;
Expand All @@ -169,24 +164,13 @@ export class FsrsAlgorithm extends SrsAlgorithm {
data.difficulty = MiscUtils.fixed(data.difficulty, 5);
data.elapsed_days = MiscUtils.fixed(data.elapsed_days, 3);

//Get the due date for card:
// const due = card.due;

//Get the state for card:
// state = card.state;

// Get the review log after rating :
if (log) {
const review_log = scheduling_cards[response].review_log;
this.appendRevlog(item, review_log);
}

let nextInterval = data.due.valueOf() - data.last_review.valueOf();
// not sure should use balance or not.
let days = nextInterval / DateUtils.DAYS_TO_MILLIS;
days = balance(days, this.getDueDates(item.itemType), this.settings.maximum_interval);
nextInterval = days * DateUtils.DAYS_TO_MILLIS;
data.due = new Date(nextInterval + now.getTime());

return {
correct,
Expand Down
10 changes: 5 additions & 5 deletions src/algorithms/scheduling_default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function schedule(

// replaces random fuzz with load balancing over the fuzz interval

interval = balance(interval, dueDates, settingsObj.maximumInterval);
// interval = balance(interval, dueDates, settingsObj.maximumInterval);

return { interval: Math.round(interval * 10) / 10, ease };
}
Expand Down Expand Up @@ -102,20 +102,20 @@ export class DefaultAlgorithm extends SrsAlgorithm {
const now: number = Date.now();
const delayBeforeReview = due === 0 ? 0 : now - due; //just in case.
// console.log("item.data:", item.data);
const dueDatesNotesorCards = this.getDueDates(item.itemType);
// const dueDatesNotesorCards = this.getDueDates(item.itemType);

const intvls: number[] = [];
this.srsOptions().forEach((opt, ind) => {
const dataCopy = deepcopy(data);
const dueDates = deepcopy(dueDatesNotesorCards);
// const dueDates = deepcopy(dueDatesNotesorCards);

const schedObj: Record<string, number> = schedule(
ind,
dataCopy.lastInterval,
dataCopy.ease,
delayBeforeReview,
this.settings,
dueDates,
// dueDates,
);
const nextInterval = schedObj.interval;
intvls.push(nextInterval);
Expand Down Expand Up @@ -148,7 +148,7 @@ export class DefaultAlgorithm extends SrsAlgorithm {
data.ease,
delayBeforeReview,
this.settings,
this.getDueDates(item.itemType),
// this.getDueDates(item.itemType),
);

const nextReview = schedObj.interval;
Expand Down
1 change: 0 additions & 1 deletion src/algorithms/supermemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ export class Sm2Algorithm extends SrsAlgorithm {
}

data.ease = MiscUtils.fixed(data.ease, 3);
nextReview = balance(nextReview, this.getDueDates(item.itemType));
data.lastInterval = nextReview;
// console.log("item.data:", item.data);
// console.log("smdata:", data);
Expand Down
54 changes: 54 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { ReviewNote } from "src/reviewNote/review-note";
import { ItemInfoModal } from "src/gui/info";
import { Queue } from "./dataStore/queue";
import { debug } from "./util/utils_recall";
import { postponeItems } from "./algorithms/balance/postpone";
import { ReviewDeckSelectionModal } from "./gui/reviewDeckSelectionModal";
import { reschedule } from "./algorithms/balance/reschedule";

export default class Commands {
plugin: ObsidianSrsPlugin;
Expand Down Expand Up @@ -209,5 +212,56 @@ export default class Commands {
plugin.store.verifyItems();
},
});

plugin.addCommand({
id: "reschedule",
name: "Reschedule",
callback: () => {
reschedule(
plugin.store.items.filter((item) => item.hasDue && item.isTracked),
);
},
});

plugin.addCommand({
id: "postpone-cards",
name: "Postpone cards",
callback: () => {
postponeItems(plugin.store.items.filter((item) => item.isCard && item.isTracked));
},
});
plugin.addCommand({
id: "postpone-notes",
name: "Postpone notes",
callback: () => {
postponeItems(plugin.store.items.filter((item) => !item.isCard && item.isTracked));
},
});
plugin.addCommand({
id: "postpone-all",
name: "Postpone All",
callback: () => {
postponeItems(plugin.store.items.filter((item) => item.isTracked));
},
});
// plugin.addCommand({
// id: "postpone-manual",
// name: "Postpone after x days(wip)",
// callback: () => {
// return;
// const reviewDeckNames: string[] = Object.keys(plugin.reviewDecks);
// const cardItems = plugin.store.items.filter(
// (item) => item.isCard && item.isTracked,
// );
// const cardDeckNames: string[] = cardItems.map((item) => item.deckName).unique();
// const deckSelectionModal = new ReviewDeckSelectionModal(
// plugin.app,
// reviewDeckNames,
// );
// deckSelectionModal.submitCallback = (deck: string) => plugin.reviewNextNote(deck);
// deckSelectionModal.open();
// postponeItems(plugin.store.items.filter((item) => item.isTracked));
// },
// });
}
}
Loading

0 comments on commit 4052a40

Please sign in to comment.