From 12137c4b7dca124247628f1142576fb9a2eaf745 Mon Sep 17 00:00:00 2001 From: psaunderualberta Date: Mon, 26 Dec 2022 16:47:40 -0700 Subject: [PATCH 01/15] Making program runnable on my computer --- .gitignore | 3 +++ esbuild.config.mjs | 3 ++- package.json | 1 + src/constants.ts | 4 ++-- src/flashcard-modal.tsx | 11 +++++++---- src/main.ts | 8 ++++---- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 3b4ba48b..7b352b72 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ build # coverage coverage + +# Temp for hot reloads +main.js \ No newline at end of file diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 57268723..11ce3efc 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -25,6 +25,7 @@ esbuild sourcemap: "inline", sourcesContent: !prod, treeShaking: true, - outfile: "build/main.js", + // outfile: "build/main.js", + outfile: "./main.js", }) .catch(() => process.exit(1)); diff --git a/package.json b/package.json index 2fcde9be..4b31d28e 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "dependencies": { "chart.js": "^4.0.1", + "i": "^0.3.7", "pagerank.js": "^1.0.2" }, "packageManager": "npm@8.18.0" diff --git a/src/constants.ts b/src/constants.ts index 64905fb3..2cb4263b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,8 +2,8 @@ export const SCHEDULING_INFO_REGEX = /^---\n((?:.*\n)*)sr-due: (.+)\nsr-interval: (\d+)\nsr-ease: (\d+)\n((?:.*\n)?)---/; export const YAML_FRONT_MATTER_REGEX = /^---\n((?:.*\n)*?)---/; -export const MULTI_SCHEDULING_EXTRACTOR = /!([\d-]+),(\d+),(\d+)/gm; -export const LEGACY_SCHEDULING_EXTRACTOR = //gm; +export const LEGACY_MULTI_SCHEDULING_EXTRACTOR = /!([\d-]+),(\d+),(\d+)/gm; +export const LEGACY_LEGACY_SCHEDULING_EXTRACTOR = //gm; export const IMAGE_FORMATS = ["jpg", "jpeg", "gif", "png", "svg"]; export const AUDIO_FORMATS = ["mp3", "webm", "m4a", "wav", "ogg"]; diff --git a/src/flashcard-modal.tsx b/src/flashcard-modal.tsx index 6f951ca6..2ac5d97b 100644 --- a/src/flashcard-modal.tsx +++ b/src/flashcard-modal.tsx @@ -15,8 +15,8 @@ import type SRPlugin from "src/main"; import { Card, CardType, schedule, textInterval, ReviewResponse } from "src/scheduling"; import { COLLAPSE_ICON, - MULTI_SCHEDULING_EXTRACTOR, - LEGACY_SCHEDULING_EXTRACTOR, + LEGACY_MULTI_SCHEDULING_EXTRACTOR, + LEGACY_LEGACY_SCHEDULING_EXTRACTOR, IMAGE_FORMATS, AUDIO_FORMATS, VIDEO_FORMATS, @@ -318,6 +318,9 @@ export class FlashcardModal extends Modal { this.plugin.dueDatesFlashcards ); } else { + + // First time this card was reviewed, so need to + // add data to it. let initial_ease: number = this.plugin.data.settings.baseEase; if ( Object.prototype.hasOwnProperty.call( @@ -375,10 +378,10 @@ export class FlashcardModal extends Modal { this.currentCard.cardText + sep + ``; } else { let scheduling: RegExpMatchArray[] = [ - ...this.currentCard.cardText.matchAll(MULTI_SCHEDULING_EXTRACTOR), + ...this.currentCard.cardText.matchAll(LEGACY_MULTI_SCHEDULING_EXTRACTOR), ]; if (scheduling.length === 0) { - scheduling = [...this.currentCard.cardText.matchAll(LEGACY_SCHEDULING_EXTRACTOR)]; + scheduling = [...this.currentCard.cardText.matchAll(LEGACY_LEGACY_SCHEDULING_EXTRACTOR)]; } const currCardSched: string[] = ["0", dueString, interval.toString(), ease.toString()]; diff --git a/src/main.ts b/src/main.ts index 1ca4295a..382107a0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,8 +17,8 @@ import { Card, CardType, ReviewResponse, schedule } from "src/scheduling"; import { YAML_FRONT_MATTER_REGEX, SCHEDULING_INFO_REGEX, - LEGACY_SCHEDULING_EXTRACTOR, - MULTI_SCHEDULING_EXTRACTOR, + LEGACY_LEGACY_SCHEDULING_EXTRACTOR, + LEGACY_MULTI_SCHEDULING_EXTRACTOR, } from "src/constants"; import { escapeRegexString, cyrb53 } from "src/utils"; import { ReviewDeck, ReviewDeckSelectionModal } from "src/review-deck"; @@ -754,9 +754,9 @@ export default class SRPlugin extends Plugin { } } - let scheduling: RegExpMatchArray[] = [...cardText.matchAll(MULTI_SCHEDULING_EXTRACTOR)]; + let scheduling: RegExpMatchArray[] = [...cardText.matchAll(LEGACY_MULTI_SCHEDULING_EXTRACTOR)]; if (scheduling.length === 0) - scheduling = [...cardText.matchAll(LEGACY_SCHEDULING_EXTRACTOR)]; + scheduling = [...cardText.matchAll(LEGACY_LEGACY_SCHEDULING_EXTRACTOR)]; // we have some extra scheduling dates to delete if (scheduling.length > siblingMatches.length) { From 8a04601abab6aa59926f07711e083807d887ab5c Mon Sep 17 00:00:00 2001 From: psaunderualberta Date: Tue, 27 Dec 2022 14:02:59 -0700 Subject: [PATCH 02/15] Added heap to plugin, not sure if working --- package.json | 2 ++ src/flashcard-modal.tsx | 21 ++++++++++++++++++--- src/main.ts | 1 + src/scheduling.ts | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 4b31d28e..b9bd7a19 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "author": "Stephen Mwangi", "license": "MIT", "devDependencies": { + "@types/heap": "^0.2.31", "@types/jest": "^29.2.4", "@types/node": "^18.11.13", "@types/vhtml": "^2.2.4", @@ -34,6 +35,7 @@ }, "dependencies": { "chart.js": "^4.0.1", + "heap": "^0.2.7", "i": "^0.3.7", "pagerank.js": "^1.0.2" }, diff --git a/src/flashcard-modal.tsx b/src/flashcard-modal.tsx index 2ac5d97b..432b1a1e 100644 --- a/src/flashcard-modal.tsx +++ b/src/flashcard-modal.tsx @@ -23,6 +23,7 @@ import { } from "src/constants"; import { escapeRegexString, cyrb53 } from "src/utils"; import { t } from "src/lang/helpers"; +import Heap from "heap"; export enum FlashcardModalMode { DecksList, @@ -350,7 +351,7 @@ export class FlashcardModal extends Modal { this.currentCard.interval = 1.0; this.currentCard.ease = this.plugin.data.settings.baseEase; if (this.currentCard.isDue) { - this.currentDeck.dueFlashcards.push(this.currentCard); + Heap.push(this.currentDeck.dueFlashcards, this.currentCard, Deck.comparator); } else { this.currentDeck.newFlashcards.push(this.currentCard); } @@ -624,9 +625,9 @@ export class Deck { if (deckPath.length === 0) { if (cardObj.isDue) { - this.dueFlashcards.push(cardObj); + Heap.push(this.dueFlashcards, cardObj, Deck.comparator); } else { - this.newFlashcards.push(cardObj); + Heap.push(this.newFlashcards, cardObj, Deck.comparator); } return; } @@ -654,13 +655,16 @@ export class Deck { } } + // TODO: May need to delete this? deleteFlashcardAtIndex(index: number, cardIsDue: boolean): void { if (cardIsDue) { this.dueFlashcards.splice(index, 1); this.dueFlashcardsCount--; + Heap.heapify(this.dueFlashcards, Deck.comparator); } else { this.newFlashcards.splice(index, 1); this.newFlashcardsCount--; + Heap.heapify(this.newFlashcards, Deck.comparator); } let deck: Deck = this.parent; @@ -794,6 +798,8 @@ export class Deck { let interval = 1.0, ease: number = modal.plugin.data.settings.baseEase, delayBeforeReview = 0; + + // TODO: Need to update below for Heap if (this.dueFlashcards.length > 0) { if (modal.plugin.data.settings.randomizeCardOrder) { modal.currentCardIdx = Math.floor(Math.random() * this.dueFlashcards.length); @@ -893,4 +899,13 @@ export class Deck { if (modal.plugin.data.settings.showFileNameInFileLink) modal.fileLinkView.setText(modal.currentCard.note.basename); } + + // TODO: Access whether to randomize or not + static comparator(a: Card, b: Card): number { + if (a.isDue && !b.isDue) + return 1; + if (!a.isDue && b.isDue) + return -1; + return 0; + } } diff --git a/src/main.ts b/src/main.ts index 382107a0..b11658d8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -754,6 +754,7 @@ export default class SRPlugin extends Plugin { } } + // TODO: Update scheduling information to minutes let scheduling: RegExpMatchArray[] = [...cardText.matchAll(LEGACY_MULTI_SCHEDULING_EXTRACTOR)]; if (scheduling.length === 0) scheduling = [...cardText.matchAll(LEGACY_LEGACY_SCHEDULING_EXTRACTOR)]; diff --git a/src/scheduling.ts b/src/scheduling.ts index 8aab84d9..ff84f5af 100644 --- a/src/scheduling.ts +++ b/src/scheduling.ts @@ -15,6 +15,7 @@ export enum ReviewResponse { export interface Card { // scheduling isDue: boolean; + isReDue: boolean; interval?: number; ease?: number; delayBeforeReview?: number; From 8036b55f2a15841b523b5617d5cc406d6618a127 Mon Sep 17 00:00:00 2001 From: psaunderualberta Date: Wed, 28 Dec 2022 16:33:35 -0700 Subject: [PATCH 03/15] Changed format of due dates --- data.json | 44 +++++++++++++++++++++++++++++++++++++++++ src/constants.ts | 4 ++-- src/flashcard-modal.tsx | 36 ++++++++++----------------------- src/lang/locale/en.ts | 1 + src/main.ts | 14 +++++++------ 5 files changed, 65 insertions(+), 34 deletions(-) create mode 100644 data.json diff --git a/data.json b/data.json new file mode 100644 index 00000000..71da4b7a --- /dev/null +++ b/data.json @@ -0,0 +1,44 @@ +{ + "settings": { + "flashcardEasyText": "Easy", + "flashcardGoodText": "Good", + "flashcardHardText": "Hard", + "flashcardTags": [ + "#flashcards" + ], + "convertFoldersToDecks": false, + "cardCommentOnSameLine": false, + "burySiblingCards": false, + "showContextInCards": true, + "flashcardHeightPercentage": 80, + "flashcardWidthPercentage": 40, + "showFileNameInFileLink": false, + "randomizeCardOrder": true, + "convertHighlightsToClozes": true, + "convertBoldTextToClozes": false, + "convertCurlyBracketsToClozes": false, + "singleLineCardSeparator": "::", + "singleLineReversedCardSeparator": ":::", + "multilineCardSeparator": "?", + "multilineReversedCardSeparator": "??", + "enableNoteReviewPaneOnStartup": true, + "tagsToReview": [ + "#review" + ], + "noteFoldersToIgnore": [], + "openRandomNote": false, + "autoNextNote": false, + "disableFileMenuReviewOptions": false, + "maxNDaysNotesReviewQueue": 365, + "initiallyExpandAllSubdecksInTree": false, + "baseEase": 250, + "lapsesIntervalChange": 0.5, + "easyBonus": 1.3, + "maximumInterval": 36525, + "maxLinkFactor": 1, + "showDebugMessages": true + }, + "buryDate": "2022-12-28", + "buryList": [], + "historyDeck": null +} \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 2cb4263b..ce363414 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,8 +2,8 @@ export const SCHEDULING_INFO_REGEX = /^---\n((?:.*\n)*)sr-due: (.+)\nsr-interval: (\d+)\nsr-ease: (\d+)\n((?:.*\n)?)---/; export const YAML_FRONT_MATTER_REGEX = /^---\n((?:.*\n)*?)---/; -export const LEGACY_MULTI_SCHEDULING_EXTRACTOR = /!([\d-]+),(\d+),(\d+)/gm; -export const LEGACY_LEGACY_SCHEDULING_EXTRACTOR = //gm; +export const MULTI_SCHEDULING_EXTRACTOR = /!([-\d :]+),(\d+),(\d+)/gm; +export const LEGACY_SCHEDULING_EXTRACTOR = //gm; export const IMAGE_FORMATS = ["jpg", "jpeg", "gif", "png", "svg"]; export const AUDIO_FORMATS = ["mp3", "webm", "m4a", "wav", "ogg"]; diff --git a/src/flashcard-modal.tsx b/src/flashcard-modal.tsx index 432b1a1e..11f0b365 100644 --- a/src/flashcard-modal.tsx +++ b/src/flashcard-modal.tsx @@ -15,8 +15,8 @@ import type SRPlugin from "src/main"; import { Card, CardType, schedule, textInterval, ReviewResponse } from "src/scheduling"; import { COLLAPSE_ICON, - LEGACY_MULTI_SCHEDULING_EXTRACTOR, - LEGACY_LEGACY_SCHEDULING_EXTRACTOR, + MULTI_SCHEDULING_EXTRACTOR, + LEGACY_SCHEDULING_EXTRACTOR, IMAGE_FORMATS, AUDIO_FORMATS, VIDEO_FORMATS, @@ -361,7 +361,8 @@ export class FlashcardModal extends Modal { return; } - const dueString: string = due.format("YYYY-MM-DD"); + // TODO: Change to only include time if re-reviewing on same day + const dueString: string = due.format(t("DATE_SCHED_FMT")); let fileText: string = await this.app.vault.read(this.currentCard.note); const replacementRegex = new RegExp(escapeRegexString(this.currentCard.cardText), "gm"); @@ -379,10 +380,10 @@ export class FlashcardModal extends Modal { this.currentCard.cardText + sep + ``; } else { let scheduling: RegExpMatchArray[] = [ - ...this.currentCard.cardText.matchAll(LEGACY_MULTI_SCHEDULING_EXTRACTOR), + ...this.currentCard.cardText.matchAll(MULTI_SCHEDULING_EXTRACTOR), ]; if (scheduling.length === 0) { - scheduling = [...this.currentCard.cardText.matchAll(LEGACY_LEGACY_SCHEDULING_EXTRACTOR)]; + scheduling = [...this.currentCard.cardText.matchAll(LEGACY_SCHEDULING_EXTRACTOR)]; } const currCardSched: string[] = ["0", dueString, interval.toString(), ease.toString()]; @@ -801,11 +802,8 @@ export class Deck { // TODO: Need to update below for Heap if (this.dueFlashcards.length > 0) { - if (modal.plugin.data.settings.randomizeCardOrder) { - modal.currentCardIdx = Math.floor(Math.random() * this.dueFlashcards.length); - } else { - modal.currentCardIdx = 0; - } + modal.currentCardIdx = 0; // Heap incorporates randomness based on settings + modal.currentCard = this.dueFlashcards[modal.currentCardIdx]; modal.renderMarkdownWrapper(modal.currentCard.front, modal.flashcardView); @@ -813,22 +811,8 @@ export class Deck { ease = modal.currentCard.ease; delayBeforeReview = modal.currentCard.delayBeforeReview; } else if (this.newFlashcards.length > 0) { - if (modal.plugin.data.settings.randomizeCardOrder) { - const pickedCardIdx = Math.floor(Math.random() * this.newFlashcards.length); - modal.currentCardIdx = pickedCardIdx; - - // look for first unscheduled sibling - const pickedCard: Card = this.newFlashcards[pickedCardIdx]; - let idx = pickedCardIdx; - while (idx >= 0 && pickedCard.siblings.includes(this.newFlashcards[idx])) { - if (!this.newFlashcards[idx].isDue) { - modal.currentCardIdx = idx; - } - idx--; - } - } else { - modal.currentCardIdx = 0; - } + // TODO: Explain why we needed to "look for first unscheduled sibling" + modal.currentCardIdx = 0; // Heap incorporates randomness based on settings modal.currentCard = this.newFlashcards[modal.currentCardIdx]; modal.renderMarkdownWrapper(modal.currentCard.front, modal.flashcardView); diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index edc91243..4fa9187f 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -43,6 +43,7 @@ export default { DAYS_STR_IVL_MOBILE: "${interval}d", MONTHS_STR_IVL_MOBILE: "${interval}m", YEARS_STR_IVL_MOBILE: "${interval}y", + DATE_SCHED_FMT: "YYYY-MM-DD hh:mm:ss", // settings.ts SETTINGS_HEADER: "Spaced Repetition Plugin - Settings", diff --git a/src/main.ts b/src/main.ts index b11658d8..e19072f8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,8 +17,8 @@ import { Card, CardType, ReviewResponse, schedule } from "src/scheduling"; import { YAML_FRONT_MATTER_REGEX, SCHEDULING_INFO_REGEX, - LEGACY_LEGACY_SCHEDULING_EXTRACTOR, - LEGACY_MULTI_SCHEDULING_EXTRACTOR, + LEGACY_SCHEDULING_EXTRACTOR, + MULTI_SCHEDULING_EXTRACTOR, } from "src/constants"; import { escapeRegexString, cyrb53 } from "src/utils"; import { ReviewDeck, ReviewDeckSelectionModal } from "src/review-deck"; @@ -645,6 +645,7 @@ export default class SRPlugin extends Plugin { settings.convertBoldTextToClozes, settings.convertCurlyBracketsToClozes ); + for (const parsedCard of parsedCards) { deckPath = noteDeckPath; const cardType: CardType = parsedCard[0], @@ -755,10 +756,10 @@ export default class SRPlugin extends Plugin { } // TODO: Update scheduling information to minutes - let scheduling: RegExpMatchArray[] = [...cardText.matchAll(LEGACY_MULTI_SCHEDULING_EXTRACTOR)]; + let scheduling: RegExpMatchArray[] = [...cardText.matchAll(MULTI_SCHEDULING_EXTRACTOR)]; if (scheduling.length === 0) - scheduling = [...cardText.matchAll(LEGACY_LEGACY_SCHEDULING_EXTRACTOR)]; - + scheduling = [...cardText.matchAll(LEGACY_SCHEDULING_EXTRACTOR)]; + // we have some extra scheduling dates to delete if (scheduling.length > siblingMatches.length) { const idxSched: number = cardText.lastIndexOf("/gm; +export const MINUTES_PER_DAY = 24 * 60; export const IMAGE_FORMATS = ["jpg", "jpeg", "gif", "png", "svg"]; export const AUDIO_FORMATS = ["mp3", "webm", "m4a", "wav", "ogg"]; diff --git a/src/flashcard-modal.tsx b/src/flashcard-modal.tsx index 11f0b365..0bc56d8e 100644 --- a/src/flashcard-modal.tsx +++ b/src/flashcard-modal.tsx @@ -229,6 +229,8 @@ export class FlashcardModal extends Modal { this.responseDiv = this.contentEl.createDiv("sr-response"); + // TODO: Add 'impossible' button. Requires adding for all languages. + this.hardBtn = document.createElement("button"); this.hardBtn.setAttribute("id", "sr-hard-btn"); this.hardBtn.setText(this.plugin.data.settings.flashcardHardText); @@ -346,7 +348,7 @@ export class FlashcardModal extends Modal { interval = schedObj.interval; ease = schedObj.ease; - due = window.moment(Date.now() + interval * 24 * 3600 * 1000); + due = window.moment(Date.now() + interval * 60 * 1000); } else { this.currentCard.interval = 1.0; this.currentCard.ease = this.plugin.data.settings.baseEase; @@ -362,7 +364,7 @@ export class FlashcardModal extends Modal { } // TODO: Change to only include time if re-reviewing on same day - const dueString: string = due.format(t("DATE_SCHED_FMT")); + const dueString: string = due.format("YYYY-MM-DD"); let fileText: string = await this.app.vault.read(this.currentCard.note); const replacementRegex = new RegExp(escapeRegexString(this.currentCard.cardText), "gm"); diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 4fa9187f..7de203fa 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -37,13 +37,15 @@ export default { ALL_CAUGHT_UP: "You're all caught up now :D.", // scheduling.ts + // TODO: Translate MINUTES_X to other langs + MINUTES_STR_IVL: "${interval} minutes(s)", DAYS_STR_IVL: "${interval} day(s)", MONTHS_STR_IVL: "${interval} month(s)", YEARS_STR_IVL: "${interval} year(s)", + MINUTES_STR_IVL_MOBILE: "${interval}m", DAYS_STR_IVL_MOBILE: "${interval}d", - MONTHS_STR_IVL_MOBILE: "${interval}m", + MONTHS_STR_IVL_MOBILE: "${interval}mo", YEARS_STR_IVL_MOBILE: "${interval}y", - DATE_SCHED_FMT: "YYYY-MM-DD hh:mm:ss", // settings.ts SETTINGS_HEADER: "Spaced Repetition Plugin - Settings", diff --git a/src/main.ts b/src/main.ts index e19072f8..5ea379c6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -802,7 +802,7 @@ export default class SRPlugin extends Plugin { this.deckTree.insertFlashcard([...deckPath], cardObj); } else if (i < scheduling.length) { const dueUnix: number = window - .moment(scheduling[i][1], ["YYYY-MM-DD", "DD-MM-YYYY", t("DATE_SCHED_FMT")]) + .moment(scheduling[i][1], ["YYYY-MM-DD", "DD-MM-YYYY"]) .valueOf(); const nDays: number = Math.ceil((dueUnix - now) / (24 * 3600 * 1000)); if (!Object.prototype.hasOwnProperty.call(this.dueDatesFlashcards, nDays)) { diff --git a/src/scheduling.ts b/src/scheduling.ts index ff84f5af..baaa68a6 100644 --- a/src/scheduling.ts +++ b/src/scheduling.ts @@ -2,6 +2,7 @@ import { TFile } from "obsidian"; import { SRSettings } from "src/settings"; import { t } from "src/lang/helpers"; +import { MINUTES_PER_DAY } from "./constants"; export enum ReviewResponse { Easy, @@ -42,6 +43,16 @@ export enum CardType { Cloze, } +/** + * + * @param response Whether or not the card was labelled 'easy', 'good', or 'hard' + * @param interval The interval in minutes between the previous review and when the card becomes available for review. + * @param ease The internal ease of the card + * @param delayBeforeReview Difference in ms between a card's scheduled review time & when its actually reviewed. + * @param settingsObj The global settings object + * @param dueDates The array of due dates (for statistics) + * @returns Object containing the new scheduling interval & ease of the card. + */ export function schedule( response: ReviewResponse, interval: number, @@ -50,20 +61,32 @@ export function schedule( settingsObj: SRSettings, dueDates?: Record ): Record { - delayBeforeReview = Math.max(0, Math.floor(delayBeforeReview / (24 * 3600 * 1000))); + const minutesBeforeReview: number = Math.max(0, Math.floor(delayBeforeReview / (60 * 1000))); - if (response === ReviewResponse.Easy) { - ease += 20; - interval = ((interval + delayBeforeReview) * ease) / 100; - interval *= settingsObj.easyBonus; - } else if (response === ReviewResponse.Good) { - interval = ((interval + delayBeforeReview / 2) * ease) / 100; - } else if (response === ReviewResponse.Hard) { - ease = Math.max(130, ease - 20); - interval = Math.max( - 1, - (interval + delayBeforeReview / 4) * settingsObj.lapsesIntervalChange - ); + if (interval < MINUTES_PER_DAY) { + if (response === ReviewResponse.Easy) { + ease += 20; + interval = MINUTES_PER_DAY; + } else if (response === ReviewResponse.Good) { + interval = 10; + } else { + interval = 1; + ease = Math.max(settingsObj.baseEase, ease - 20); + } + } else { + if (response === ReviewResponse.Easy) { + ease += 20; + interval = ((interval + minutesBeforeReview) * ease) / 100; + interval *= settingsObj.easyBonus; + } else if (response === ReviewResponse.Good) { + interval = ((interval + minutesBeforeReview / 2) * ease) / 100; + } else if (response === ReviewResponse.Hard) { + ease = Math.max(settingsObj.baseEase, ease - 20); + interval = Math.max( + 1, + (interval + minutesBeforeReview / 4) * settingsObj.lapsesIntervalChange + ); + } } // replaces random fuzz with load balancing over the fuzz interval @@ -73,17 +96,22 @@ export function schedule( dueDates[interval] = 0; } else { // disable fuzzing for small intervals - if (interval > 4) { + if (interval > 4 * MINUTES_PER_DAY) { 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 < 7 * MINUTES_PER_DAY) { + fuzz = MINUTES_PER_DAY; + } else if (interval < 30 * MINUTES_PER_DAY) { + fuzz = Math.max(2, Math.floor(interval * 0.15)); + } else { + fuzz = Math.max(4, Math.floor(interval * 0.05)); + } const originalInterval = interval; outer: for (let i = 1; i <= fuzz; i++) { for (const ivl of [originalInterval - i, originalInterval + i]) { if (!Object.prototype.hasOwnProperty.call(dueDates, ivl)) { - dueDates[ivl] = 0; + dueDates[Math.round(ivl / MINUTES_PER_DAY)] = 0; interval = ivl; break outer; } @@ -93,25 +121,33 @@ export function schedule( } } - dueDates[interval]++; + dueDates[Math.round(interval / MINUTES_PER_DAY)]++; } - interval = Math.min(interval, settingsObj.maximumInterval); + interval = Math.min(interval, settingsObj.maximumInterval * MINUTES_PER_DAY); + if (interval < 10) { + interval = 1; + } else if (interval < MINUTES_PER_DAY) { + interval = 10; + } return { interval: Math.round(interval * 10) / 10, ease }; } export function textInterval(interval: number, isMobile: boolean): string { - const m: number = Math.round(interval / 3.04375) / 10, - y: number = Math.round(interval / 36.525) / 10; + const days: number = Math.round(interval / (24 * 60)), + months: number = Math.round(days / 3.04375) / 10, + years: number = Math.round(days / 36.525) / 10; if (isMobile) { - if (m < 1.0) return t("DAYS_STR_IVL_MOBILE", { interval }); - else if (y < 1.0) return t("MONTHS_STR_IVL_MOBILE", { interval: m }); - else return t("YEARS_STR_IVL_MOBILE", { interval: y }); + if (days < 1.0) return t("MINUTES_STR_IVL_MOBILE", { interval }); + if (months < 1.0) return t("DAYS_STR_IVL_MOBILE", { interval: days }); + if (years < 1.0) return t("MONTHS_STR_IVL_MOBILE", { interval: months }); + return t("YEARS_STR_IVL_MOBILE", { interval: years }); } else { - if (m < 1.0) return t("DAYS_STR_IVL", { interval }); - else if (y < 1.0) return t("MONTHS_STR_IVL", { interval: m }); - else return t("YEARS_STR_IVL", { interval: y }); + if (days < 1.0) return t("MINUTES_STR_IVL", { interval }); + if (months < 1.0) return t("DAYS_STR_IVL", { interval: days }); + if (years < 1.0) return t("MONTHS_STR_IVL", { interval: months }); + return t("YEARS_STR_IVL", { interval: years }); } } From 820d23509172ed9e4e5081aeee5ca4eb78b69ad8 Mon Sep 17 00:00:00 2001 From: psaunderualberta Date: Thu, 29 Dec 2022 21:47:09 -0700 Subject: [PATCH 05/15] Added code so cards can be reviewed on same day --- src/flashcard-modal.tsx | 26 ++++++++++++++++++++++---- src/lang/locale/en.ts | 1 + src/main.ts | 3 ++- src/scheduling.ts | 2 ++ 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/flashcard-modal.tsx b/src/flashcard-modal.tsx index 0bc56d8e..fe82755b 100644 --- a/src/flashcard-modal.tsx +++ b/src/flashcard-modal.tsx @@ -20,6 +20,7 @@ import { IMAGE_FORMATS, AUDIO_FORMATS, VIDEO_FORMATS, + MINUTES_PER_DAY, } from "src/constants"; import { escapeRegexString, cyrb53 } from "src/utils"; import { t } from "src/lang/helpers"; @@ -348,7 +349,22 @@ export class FlashcardModal extends Modal { interval = schedObj.interval; ease = schedObj.ease; - due = window.moment(Date.now() + interval * 60 * 1000); + + // Re-add card to deck if we need to review the card + // on the same day. + if (interval < MINUTES_PER_DAY) { + this.currentCard.isDue = true; + this.currentCard.isReDue = true; + this.currentCard.interval = interval; + this.currentCard.ease = ease; + this.currentCard.delayBeforeReview = interval; + this.currentDeck.insertFlashcard([], this.currentCard); + due = window.moment(Date.now() + interval * 60 * 1000); + } else { + // Round down to start of day + due = window.moment(Date.now() + interval * 60 * 1000).startOf("day"); + } + } else { this.currentCard.interval = 1.0; this.currentCard.ease = this.plugin.data.settings.baseEase; @@ -363,8 +379,7 @@ export class FlashcardModal extends Modal { return; } - // TODO: Change to only include time if re-reviewing on same day - const dueString: string = due.format("YYYY-MM-DD"); + const dueString: string = due.format(t("DATE_SCHED_FMT")); let fileText: string = await this.app.vault.read(this.currentCard.note); const replacementRegex = new RegExp(escapeRegexString(this.currentCard.cardText), "gm"); @@ -579,6 +594,7 @@ export class FlashcardModal extends Modal { } } +// TODO: Update Deck to only contain one stack of flashcards export class Deck { public deckName: string; public newFlashcards: Card[]; @@ -624,7 +640,9 @@ export class Deck { } else { this.newFlashcardsCount++; } - this.totalFlashcards++; + + if (!cardObj.isReDue) + this.totalFlashcards++; if (deckPath.length === 0) { if (cardObj.isDue) { diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 7de203fa..41c6f189 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -46,6 +46,7 @@ export default { DAYS_STR_IVL_MOBILE: "${interval}d", MONTHS_STR_IVL_MOBILE: "${interval}mo", YEARS_STR_IVL_MOBILE: "${interval}y", + DATE_SCHED_FMT: "YYYY-MM-DD HH:mm:ss", // settings.ts SETTINGS_HEADER: "Spaced Repetition Plugin - Settings", diff --git a/src/main.ts b/src/main.ts index 5ea379c6..993bd758 100644 --- a/src/main.ts +++ b/src/main.ts @@ -417,6 +417,7 @@ export default class SRPlugin extends Plugin { } async saveReviewResponse(note: TFile, response: ReviewResponse): Promise { + console.log("HAHAHA"); const fileCachedData = this.app.metadataCache.getFileCache(note) || {}; const frontmatter: FrontMatterCache | Record = fileCachedData.frontmatter || {}; @@ -802,7 +803,7 @@ export default class SRPlugin extends Plugin { this.deckTree.insertFlashcard([...deckPath], cardObj); } else if (i < scheduling.length) { const dueUnix: number = window - .moment(scheduling[i][1], ["YYYY-MM-DD", "DD-MM-YYYY"]) + .moment(scheduling[i][1], ["YYYY-MM-DD", "DD-MM-YYYY", t("DATE_SCHED_FMT")]) .valueOf(); const nDays: number = Math.ceil((dueUnix - now) / (24 * 3600 * 1000)); if (!Object.prototype.hasOwnProperty.call(this.dueDatesFlashcards, nDays)) { diff --git a/src/scheduling.ts b/src/scheduling.ts index baaa68a6..654a0f8a 100644 --- a/src/scheduling.ts +++ b/src/scheduling.ts @@ -74,6 +74,7 @@ export function schedule( ease = Math.max(settingsObj.baseEase, ease - 20); } } else { + // TODO: Change old algorithm to deal with minutes? How? if (response === ReviewResponse.Easy) { ease += 20; interval = ((interval + minutesBeforeReview) * ease) / 100; @@ -124,6 +125,7 @@ export function schedule( dueDates[Math.round(interval / MINUTES_PER_DAY)]++; } + // Round down to 1m and 10m for consistency interval = Math.min(interval, settingsObj.maximumInterval * MINUTES_PER_DAY); if (interval < 10) { interval = 1; From f8fb5695d255f372af0f3c3b216d86586e11be52 Mon Sep 17 00:00:00 2001 From: psaunderualberta Date: Fri, 30 Dec 2022 10:51:27 -0700 Subject: [PATCH 06/15] Changed to use only one flashcard array --- src/flashcard-modal.tsx | 60 +++++++++++++---------------------------- 1 file changed, 19 insertions(+), 41 deletions(-) diff --git a/src/flashcard-modal.tsx b/src/flashcard-modal.tsx index fe82755b..c619c1a4 100644 --- a/src/flashcard-modal.tsx +++ b/src/flashcard-modal.tsx @@ -368,11 +368,7 @@ export class FlashcardModal extends Modal { } else { this.currentCard.interval = 1.0; this.currentCard.ease = this.plugin.data.settings.baseEase; - if (this.currentCard.isDue) { - Heap.push(this.currentDeck.dueFlashcards, this.currentCard, Deck.comparator); - } else { - this.currentDeck.newFlashcards.push(this.currentCard); - } + this.currentDeck.insertFlashcard([], this.currentCard); due = window.moment(Date.now()); new Notice(t("CARD_PROGRESS_RESET")); this.currentDeck.nextCard(this); @@ -437,18 +433,12 @@ export class FlashcardModal extends Modal { } for (const sibling of this.currentCard.siblings) { - const dueIdx = this.currentDeck.dueFlashcards.indexOf(sibling); - const newIdx = this.currentDeck.newFlashcards.indexOf(sibling); + const idx = this.currentDeck.flashcards.indexOf(sibling); - if (dueIdx !== -1) { + if (idx !== -1) { this.currentDeck.deleteFlashcardAtIndex( - dueIdx, - this.currentDeck.dueFlashcards[dueIdx].isDue - ); - } else if (newIdx !== -1) { - this.currentDeck.deleteFlashcardAtIndex( - newIdx, - this.currentDeck.newFlashcards[newIdx].isDue + idx, + this.currentDeck.flashcards[idx].isDue ); } } @@ -597,9 +587,8 @@ export class FlashcardModal extends Modal { // TODO: Update Deck to only contain one stack of flashcards export class Deck { public deckName: string; - public newFlashcards: Card[]; + public flashcards: Card[]; public newFlashcardsCount = 0; // counts those in subdecks too - public dueFlashcards: Card[]; public dueFlashcardsCount = 0; // counts those in subdecks too public totalFlashcards = 0; // counts those in subdecks too public subdecks: Deck[]; @@ -607,9 +596,8 @@ export class Deck { constructor(deckName: string, parent: Deck | null) { this.deckName = deckName; - this.newFlashcards = []; this.newFlashcardsCount = 0; - this.dueFlashcards = []; + this.flashcards = []; this.dueFlashcardsCount = 0; this.totalFlashcards = 0; this.subdecks = []; @@ -645,11 +633,7 @@ export class Deck { this.totalFlashcards++; if (deckPath.length === 0) { - if (cardObj.isDue) { - Heap.push(this.dueFlashcards, cardObj, Deck.comparator); - } else { - Heap.push(this.newFlashcards, cardObj, Deck.comparator); - } + Heap.push(this.flashcards, cardObj, Deck.comparator); return; } @@ -678,14 +662,12 @@ export class Deck { // TODO: May need to delete this? deleteFlashcardAtIndex(index: number, cardIsDue: boolean): void { + this.flashcards.splice(index, 1); + Heap.heapify(this.flashcards, Deck.comparator); if (cardIsDue) { - this.dueFlashcards.splice(index, 1); this.dueFlashcardsCount--; - Heap.heapify(this.dueFlashcards, Deck.comparator); } else { - this.newFlashcards.splice(index, 1); this.newFlashcardsCount--; - Heap.heapify(this.newFlashcards, Deck.comparator); } let deck: Deck = this.parent; @@ -786,7 +768,8 @@ export class Deck { } nextCard(modal: FlashcardModal): void { - if (this.newFlashcards.length + this.dueFlashcards.length === 0) { + Heap.heapify(this.flashcards, Deck.comparator); + if (this.flashcards.length === 0) { if (this.dueFlashcardsCount + this.newFlashcardsCount > 0) { for (const deck of this.subdecks) { if (deck.dueFlashcardsCount + deck.newFlashcardsCount > 0) { @@ -821,23 +804,18 @@ export class Deck { delayBeforeReview = 0; // TODO: Need to update below for Heap - if (this.dueFlashcards.length > 0) { - modal.currentCardIdx = 0; // Heap incorporates randomness based on settings - - modal.currentCard = this.dueFlashcards[modal.currentCardIdx]; - modal.renderMarkdownWrapper(modal.currentCard.front, modal.flashcardView); - - interval = modal.currentCard.interval; - ease = modal.currentCard.ease; - delayBeforeReview = modal.currentCard.delayBeforeReview; - } else if (this.newFlashcards.length > 0) { + if (this.flashcards.length > 0) { // TODO: Explain why we needed to "look for first unscheduled sibling" modal.currentCardIdx = 0; // Heap incorporates randomness based on settings - modal.currentCard = this.newFlashcards[modal.currentCardIdx]; + modal.currentCard = this.flashcards[modal.currentCardIdx]; modal.renderMarkdownWrapper(modal.currentCard.front, modal.flashcardView); - if ( + if (modal.currentCard.isDue) { + interval = modal.currentCard.interval; + ease = modal.currentCard.ease; + delayBeforeReview = modal.currentCard.delayBeforeReview; + } else if ( Object.prototype.hasOwnProperty.call( modal.plugin.easeByPath, modal.currentCard.note.path From a89925f69c3516792e5e76a801c8c56946dc6e64 Mon Sep 17 00:00:00 2001 From: psaunderualberta Date: Fri, 30 Dec 2022 12:41:55 -0700 Subject: [PATCH 07/15] Single-card reviews works, need to check with multiple cards & decks --- src/flashcard-modal.tsx | 170 ++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 76 deletions(-) diff --git a/src/flashcard-modal.tsx b/src/flashcard-modal.tsx index c619c1a4..cc640b43 100644 --- a/src/flashcard-modal.tsx +++ b/src/flashcard-modal.tsx @@ -306,74 +306,63 @@ export class FlashcardModal extends Modal { return; } - let interval: number, ease: number, due; - - this.currentDeck.deleteFlashcardAtIndex(this.currentCardIdx, this.currentCard.isDue); - if (response !== ReviewResponse.Reset) { - let schedObj: Record; - // scheduled card - if (this.currentCard.isDue) { - schedObj = schedule( - response, - this.currentCard.interval, - this.currentCard.ease, - this.currentCard.delayBeforeReview, - this.plugin.data.settings, - this.plugin.dueDatesFlashcards - ); - } else { - - // First time this card was reviewed, so need to - // add data to it. - let initial_ease: number = this.plugin.data.settings.baseEase; - if ( - Object.prototype.hasOwnProperty.call( - this.plugin.easeByPath, - this.currentCard.note.path - ) - ) { - initial_ease = Math.round(this.plugin.easeByPath[this.currentCard.note.path]); - } - - schedObj = schedule( - response, - 1.0, - initial_ease, - 0, - this.plugin.data.settings, - this.plugin.dueDatesFlashcards - ); - interval = schedObj.interval; - ease = schedObj.ease; - } - - interval = schedObj.interval; - ease = schedObj.ease; - - // Re-add card to deck if we need to review the card - // on the same day. - if (interval < MINUTES_PER_DAY) { - this.currentCard.isDue = true; - this.currentCard.isReDue = true; - this.currentCard.interval = interval; - this.currentCard.ease = ease; - this.currentCard.delayBeforeReview = interval; - this.currentDeck.insertFlashcard([], this.currentCard); - due = window.moment(Date.now() + interval * 60 * 1000); - } else { - // Round down to start of day - due = window.moment(Date.now() + interval * 60 * 1000).startOf("day"); - } - - } else { - this.currentCard.interval = 1.0; - this.currentCard.ease = this.plugin.data.settings.baseEase; - this.currentDeck.insertFlashcard([], this.currentCard); + let due: moment.Moment; + + // Exit early if doing a reset + if (response === ReviewResponse.Reset) { + const newCard: Card = {...this.currentCard}; + newCard.interval = 1.0; + newCard.ease = this.plugin.data.settings.baseEase; + this.currentDeck.notifyCardChanged(this.currentCardIdx, newCard); due = window.moment(Date.now()); new Notice(t("CARD_PROGRESS_RESET")); this.currentDeck.nextCard(this); return; } + + let schedObj: Record; + // scheduled card + if (this.currentCard.isDue) { + schedObj = schedule( + response, + this.currentCard.interval, + this.currentCard.ease, + this.currentCard.delayBeforeReview, + this.plugin.data.settings, + this.plugin.dueDatesFlashcards + ); + } else { + + // First time this card was reviewed, so need to + // add data to it. + let initial_ease: number = this.plugin.data.settings.baseEase; + if ( + Object.prototype.hasOwnProperty.call( + this.plugin.easeByPath, + this.currentCard.note.path + ) + ) { + initial_ease = Math.round(this.plugin.easeByPath[this.currentCard.note.path]); + } + + schedObj = schedule( + response, + 1.0, + initial_ease, + 0, + this.plugin.data.settings, + this.plugin.dueDatesFlashcards + ); + } + + const interval: number = schedObj.interval; + const ease: number = schedObj.ease; + + // Calculate the next due date for this card. + due = window.moment(Date.now() + interval * 60 * 1000); + if (interval >= MINUTES_PER_DAY) { + due = due.startOf("day"); + } const dueString: string = due.format(t("DATE_SCHED_FMT")); @@ -421,8 +410,24 @@ export class FlashcardModal extends Modal { if (this.plugin.data.settings.burySiblingCards) { this.burySiblingCards(true); } - + await this.app.vault.modify(this.currentCard.note, fileText); + + // Update the card within the deck if we need to re-review it. + // Otherwise, delete the card from the deck. + if (interval < MINUTES_PER_DAY) { + const newCard = {...this.currentCard}; // Need to copy so that old card's data persists + newCard.isDue = true; + newCard.isReDue = true; + newCard.interval = interval; + newCard.ease = ease; + newCard.delayBeforeReview = interval; + + this.currentDeck.notifyCardChanged(this.currentCardIdx, newCard); + } else { + this.currentDeck.deleteFlashcardAtIndex(this.currentCardIdx, this.currentCard.isDue); + } + this.currentDeck.nextCard(this); } @@ -432,10 +437,9 @@ export class FlashcardModal extends Modal { await this.plugin.savePluginData(); } + let idx; for (const sibling of this.currentCard.siblings) { - const idx = this.currentDeck.flashcards.indexOf(sibling); - - if (idx !== -1) { + while ((idx = this.currentDeck.flashcards.indexOf(sibling)) != -1) { this.currentDeck.deleteFlashcardAtIndex( idx, this.currentDeck.flashcards[idx].isDue @@ -629,6 +633,7 @@ export class Deck { this.newFlashcardsCount++; } + // Card was just deleted, don't increment total count; if (!cardObj.isReDue) this.totalFlashcards++; @@ -661,24 +666,37 @@ export class Deck { } // TODO: May need to delete this? - deleteFlashcardAtIndex(index: number, cardIsDue: boolean): void { - this.flashcards.splice(index, 1); - Heap.heapify(this.flashcards, Deck.comparator); + deleteFlashcardAtIndex(index: number, cardIsDue: boolean, base = true): void { + if (base) { + this.flashcards.splice(index, 1); + Heap.heapify(this.flashcards, Deck.comparator); + } + if (cardIsDue) { this.dueFlashcardsCount--; } else { this.newFlashcardsCount--; } - let deck: Deck = this.parent; + if (this.parent !== null) + this.parent.deleteFlashcardAtIndex(index, cardIsDue, false); + } + + notifyCardChanged(oldCardIdx: number, newCard: Card): void { + this.deleteFlashcardAtIndex(oldCardIdx, this.flashcards[oldCardIdx].isDue); + const deckName: string[] = [this.deckName]; + let deck = this.parent; + let root = this.parent; while (deck !== null) { - if (cardIsDue) { - deck.dueFlashcardsCount--; - } else { - deck.newFlashcardsCount--; - } + deckName.push(deck.deckName); + root = deck; deck = deck.parent; } + + console.log(deckName); + + root.insertFlashcard(deckName.reverse().slice(1, deckName.length), newCard); + console.log(root.dueFlashcardsCount); } sortSubdecksList(): void { From 2b2f6ef2d3734387a7761cc63a6bca774db1f2c3 Mon Sep 17 00:00:00 2001 From: psaunderualberta Date: Sat, 31 Dec 2022 12:15:35 -0700 Subject: [PATCH 08/15] Wrote comparator function for cards --- src/flashcard-modal.tsx | 52 +++++++++++++++++++++++++++++++++-------- src/scheduling.ts | 1 + 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/flashcard-modal.tsx b/src/flashcard-modal.tsx index cc640b43..7c01758f 100644 --- a/src/flashcard-modal.tsx +++ b/src/flashcard-modal.tsx @@ -422,6 +422,7 @@ export class FlashcardModal extends Modal { newCard.interval = interval; newCard.ease = ease; newCard.delayBeforeReview = interval; + newCard.previousReview = window.moment(Date.now()).valueOf(); this.currentDeck.notifyCardChanged(this.currentCardIdx, newCard); } else { @@ -693,10 +694,8 @@ export class Deck { deck = deck.parent; } - console.log(deckName); root.insertFlashcard(deckName.reverse().slice(1, deckName.length), newCard); - console.log(root.dueFlashcardsCount); } sortSubdecksList(): void { @@ -786,7 +785,6 @@ export class Deck { } nextCard(modal: FlashcardModal): void { - Heap.heapify(this.flashcards, Deck.comparator); if (this.flashcards.length === 0) { if (this.dueFlashcardsCount + this.newFlashcardsCount > 0) { for (const deck of this.subdecks) { @@ -806,6 +804,9 @@ export class Deck { } return; } + + // Actually get next card. + Heap.heapify(this.flashcards, Deck.comparator); modal.responseDiv.style.display = "none"; modal.resetLinkView.style.display = "none"; @@ -821,7 +822,6 @@ export class Deck { ease: number = modal.plugin.data.settings.baseEase, delayBeforeReview = 0; - // TODO: Need to update below for Heap if (this.flashcards.length > 0) { // TODO: Explain why we needed to "look for first unscheduled sibling" modal.currentCardIdx = 0; // Heap incorporates randomness based on settings @@ -900,12 +900,44 @@ export class Deck { modal.fileLinkView.setText(modal.currentCard.note.basename); } - // TODO: Access whether to randomize or not - static comparator(a: Card, b: Card): number { - if (a.isDue && !b.isDue) - return 1; - if (!a.isDue && b.isDue) - return -1; + /** + * + * @param a One card to compare with another + * @param b The other card to compare + * @returns +1 => b should be reviewed first, -1 => a should be reviewed first, 0 => no preference + */ + static comparator(a: Card, b: Card): number { + + // New cards are reviewed after due cards. + if (!a.isDue && !b.isDue) + return 0; + if (a.isDue && !b.isDue) + return -1; + if (!a.isDue && b.isDue) + return 1; + + const now = window.moment(Date.now()).valueOf(); + + // Both redue + if (a.isReDue && b.isReDue) { + const aReviewDelay = a.previousReview + a.interval * 60 * 1000 - now; + const bReviewDelay = b.previousReview + b.interval * 60 * 1000 - now; + + return (aReviewDelay < bReviewDelay) ? -1 : 1; + } + + // One redue, other is due + if (a.isReDue) { + const aDiff = now - a.previousReview + a.interval * 60 * 1000; + return (aDiff > 0) ? 1 : -1; + } else if (b.isReDue) { + const bDiff = now - b.previousReview + b.interval * 60 * 1000; + return (bDiff > 0) ? -1 : 1; + } + + // Both due, don't care which is reviewed first + // Currently assume randomness + // TODO: Access whether to randomize or not return 0; } } diff --git a/src/scheduling.ts b/src/scheduling.ts index 654a0f8a..d7dc5d03 100644 --- a/src/scheduling.ts +++ b/src/scheduling.ts @@ -20,6 +20,7 @@ export interface Card { interval?: number; ease?: number; delayBeforeReview?: number; + previousReview?: number; // note note: TFile; lineNo: number; From 8a9e1b1ddf5309af57f8708b5df0da9fa5058600 Mon Sep 17 00:00:00 2001 From: psaunderualberta Date: Sat, 31 Dec 2022 12:17:34 -0700 Subject: [PATCH 09/15] Ran prettier on main, flashcard-modal, scheduling --- src/flashcard-modal.tsx | 60 ++++++++++++++++++----------------------- src/main.ts | 2 +- src/scheduling.ts | 2 +- 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/flashcard-modal.tsx b/src/flashcard-modal.tsx index 7c01758f..5fc2d0c6 100644 --- a/src/flashcard-modal.tsx +++ b/src/flashcard-modal.tsx @@ -310,7 +310,7 @@ export class FlashcardModal extends Modal { // Exit early if doing a reset if (response === ReviewResponse.Reset) { - const newCard: Card = {...this.currentCard}; + const newCard: Card = { ...this.currentCard }; newCard.interval = 1.0; newCard.ease = this.plugin.data.settings.baseEase; this.currentDeck.notifyCardChanged(this.currentCardIdx, newCard); @@ -319,7 +319,7 @@ export class FlashcardModal extends Modal { this.currentDeck.nextCard(this); return; } - + let schedObj: Record; // scheduled card if (this.currentCard.isDue) { @@ -332,8 +332,7 @@ export class FlashcardModal extends Modal { this.plugin.dueDatesFlashcards ); } else { - - // First time this card was reviewed, so need to + // First time this card was reviewed, so need to // add data to it. let initial_ease: number = this.plugin.data.settings.baseEase; if ( @@ -410,13 +409,13 @@ export class FlashcardModal extends Modal { if (this.plugin.data.settings.burySiblingCards) { this.burySiblingCards(true); } - + await this.app.vault.modify(this.currentCard.note, fileText); // Update the card within the deck if we need to re-review it. // Otherwise, delete the card from the deck. if (interval < MINUTES_PER_DAY) { - const newCard = {...this.currentCard}; // Need to copy so that old card's data persists + const newCard = { ...this.currentCard }; // Need to copy so that old card's data persists newCard.isDue = true; newCard.isReDue = true; newCard.interval = interval; @@ -440,7 +439,7 @@ export class FlashcardModal extends Modal { let idx; for (const sibling of this.currentCard.siblings) { - while ((idx = this.currentDeck.flashcards.indexOf(sibling)) != -1) { + while ((idx = this.currentDeck.flashcards.indexOf(sibling)) != -1) { this.currentDeck.deleteFlashcardAtIndex( idx, this.currentDeck.flashcards[idx].isDue @@ -635,8 +634,7 @@ export class Deck { } // Card was just deleted, don't increment total count; - if (!cardObj.isReDue) - this.totalFlashcards++; + if (!cardObj.isReDue) this.totalFlashcards++; if (deckPath.length === 0) { Heap.push(this.flashcards, cardObj, Deck.comparator); @@ -670,7 +668,7 @@ export class Deck { deleteFlashcardAtIndex(index: number, cardIsDue: boolean, base = true): void { if (base) { this.flashcards.splice(index, 1); - Heap.heapify(this.flashcards, Deck.comparator); + Heap.heapify(this.flashcards, Deck.comparator); } if (cardIsDue) { @@ -679,8 +677,7 @@ export class Deck { this.newFlashcardsCount--; } - if (this.parent !== null) - this.parent.deleteFlashcardAtIndex(index, cardIsDue, false); + if (this.parent !== null) this.parent.deleteFlashcardAtIndex(index, cardIsDue, false); } notifyCardChanged(oldCardIdx: number, newCard: Card): void { @@ -694,7 +691,6 @@ export class Deck { deck = deck.parent; } - root.insertFlashcard(deckName.reverse().slice(1, deckName.length), newCard); } @@ -804,7 +800,7 @@ export class Deck { } return; } - + // Actually get next card. Heap.heapify(this.flashcards, Deck.comparator); @@ -824,7 +820,7 @@ export class Deck { if (this.flashcards.length > 0) { // TODO: Explain why we needed to "look for first unscheduled sibling" - modal.currentCardIdx = 0; // Heap incorporates randomness based on settings + modal.currentCardIdx = 0; // Heap incorporates randomness based on settings modal.currentCard = this.flashcards[modal.currentCardIdx]; modal.renderMarkdownWrapper(modal.currentCard.front, modal.flashcardView); @@ -901,40 +897,36 @@ export class Deck { } /** - * + * * @param a One card to compare with another * @param b The other card to compare * @returns +1 => b should be reviewed first, -1 => a should be reviewed first, 0 => no preference - */ - static comparator(a: Card, b: Card): number { - - // New cards are reviewed after due cards. - if (!a.isDue && !b.isDue) - return 0; - if (a.isDue && !b.isDue) - return -1; - if (!a.isDue && b.isDue) - return 1; - + */ + static comparator(a: Card, b: Card): number { + // New cards are reviewed after due cards. + if (!a.isDue && !b.isDue) return 0; + if (a.isDue && !b.isDue) return -1; + if (!a.isDue && b.isDue) return 1; + const now = window.moment(Date.now()).valueOf(); - + // Both redue if (a.isReDue && b.isReDue) { const aReviewDelay = a.previousReview + a.interval * 60 * 1000 - now; const bReviewDelay = b.previousReview + b.interval * 60 * 1000 - now; - - return (aReviewDelay < bReviewDelay) ? -1 : 1; + + return aReviewDelay < bReviewDelay ? -1 : 1; } - + // One redue, other is due if (a.isReDue) { const aDiff = now - a.previousReview + a.interval * 60 * 1000; - return (aDiff > 0) ? 1 : -1; + return aDiff > 0 ? 1 : -1; } else if (b.isReDue) { const bDiff = now - b.previousReview + b.interval * 60 * 1000; - return (bDiff > 0) ? -1 : 1; + return bDiff > 0 ? -1 : 1; } - + // Both due, don't care which is reviewed first // Currently assume randomness // TODO: Access whether to randomize or not diff --git a/src/main.ts b/src/main.ts index 993bd758..fa25a37f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -760,7 +760,7 @@ export default class SRPlugin extends Plugin { let scheduling: RegExpMatchArray[] = [...cardText.matchAll(MULTI_SCHEDULING_EXTRACTOR)]; if (scheduling.length === 0) scheduling = [...cardText.matchAll(LEGACY_SCHEDULING_EXTRACTOR)]; - + // we have some extra scheduling dates to delete if (scheduling.length > siblingMatches.length) { const idxSched: number = cardText.lastIndexOf("", ...defaultArgs)).toEqual([ [CardType.SingleLineBasic, "Question::Answer\n", 0], ]); expect(parse("Question::Answer ", ...defaultArgs)).toEqual([ [CardType.SingleLineBasic, "Question::Answer ", 0], ]); + + // New syntax + expect(parse("Question::Answer\n", ...defaultArgs)).toEqual([ + [CardType.SingleLineBasic, "Question::Answer\n", 0], + ]); + expect(parse("Question::Answer ", ...defaultArgs)).toEqual([ + [CardType.SingleLineBasic, "Question::Answer ", 0], + ]); + expect(parse("Some text before\nQuestion ::Answer", ...defaultArgs)).toEqual([ [CardType.SingleLineBasic, "Question ::Answer", 1], ]); @@ -47,12 +58,24 @@ test("Test parsing of multi line basic cards", () => { expect(parse("Question\n?\nAnswer", ...defaultArgs)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer", 1], ]); + + // Legacy syntax expect(parse("Question\n?\nAnswer ", ...defaultArgs)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer ", 1], ]); expect(parse("Question\n?\nAnswer\n", ...defaultArgs)).toEqual([ [CardType.MultiLineBasic, "Question\n?\nAnswer\n", 1], ]); + + // New syntax + expect(parse("Question\n?\nAnswer ", ...defaultArgs)).toEqual([ + [CardType.MultiLineBasic, "Question\n?\nAnswer ", 1], + ]); + expect(parse("Question\n?\nAnswer\n", ...defaultArgs)).toEqual([ + [CardType.MultiLineBasic, "Question\n?\nAnswer\n", 1], + ]); + + expect(parse("Some text before\nQuestion\n?\nAnswer", ...defaultArgs)).toEqual([ [CardType.MultiLineBasic, "Some text before\nQuestion\n?\nAnswer", 2], ]); @@ -88,12 +111,23 @@ test("Test parsing of cloze cards", () => { expect(parse("cloze ==deletion== test", ...defaultArgs)).toEqual([ [CardType.Cloze, "cloze ==deletion== test", 0], ]); + + // Legacy syntax expect(parse("cloze ==deletion== test\n", ...defaultArgs)).toEqual([ [CardType.Cloze, "cloze ==deletion== test\n", 0], ]); expect(parse("cloze ==deletion== test ", ...defaultArgs)).toEqual([ [CardType.Cloze, "cloze ==deletion== test ", 0], ]); + + // New syntax + expect(parse("cloze ==deletion== test\n", ...defaultArgs)).toEqual([ + [CardType.Cloze, "cloze ==deletion== test\n", 0], + ]); + expect(parse("cloze ==deletion== test ", ...defaultArgs)).toEqual([ + [CardType.Cloze, "cloze ==deletion== test ", 0], + ]); + expect(parse("==this== is a ==deletion==\n", ...defaultArgs)).toEqual([ [CardType.Cloze, "==this== is a ==deletion==", 0], ]); @@ -119,12 +153,24 @@ test("Test parsing of cloze cards", () => { expect(parse("cloze **deletion** test", ...defaultArgs)).toEqual([ [CardType.Cloze, "cloze **deletion** test", 0], ]); + + // Legacy syntax expect(parse("cloze **deletion** test\n", ...defaultArgs)).toEqual([ [CardType.Cloze, "cloze **deletion** test\n", 0], ]); expect(parse("cloze **deletion** test ", ...defaultArgs)).toEqual([ [CardType.Cloze, "cloze **deletion** test ", 0], ]); + + // New syntax + expect(parse("cloze **deletion** test\n", ...defaultArgs)).toEqual([ + [CardType.Cloze, "cloze **deletion** test\n", 0], + ]); + expect(parse("cloze **deletion** test ", ...defaultArgs)).toEqual([ + [CardType.Cloze, "cloze **deletion** test ", 0], + ]); + + expect(parse("**this** is a **deletion**\n", ...defaultArgs)).toEqual([ [CardType.Cloze, "**this** is a **deletion**", 0], ]); @@ -153,6 +199,7 @@ test("Test parsing of cloze cards", () => { }); test("Test parsing of a mix of card types", () => { + // Legacy expect( parse( "# Lorem Ipsum\n\nLorem ipsum dolor ==sit amet==, consectetur ==adipiscing== elit.\n" + @@ -176,6 +223,31 @@ test("Test parsing of a mix of card types", () => { 9, ], ]); + + // New syntax + expect( + parse( + "# Lorem Ipsum\n\nLorem ipsum dolor ==sit amet==, consectetur ==adipiscing== elit.\n" + + "Duis magna arcu, eleifend rhoncus ==euismod non,==\nlaoreet vitae enim.\n\n" + + "Fusce placerat::velit in pharetra gravida\n\n" + + "Donec dapibus ullamcorper aliquam.\n??\nDonec dapibus ullamcorper aliquam.\n", + ...defaultArgs + ) + ).toEqual([ + [ + CardType.Cloze, + "Lorem ipsum dolor ==sit amet==, consectetur ==adipiscing== elit.\n" + + "Duis magna arcu, eleifend rhoncus ==euismod non,==\n" + + "laoreet vitae enim.", + 2, + ], + [CardType.SingleLineBasic, "Fusce placerat::velit in pharetra gravida", 6], + [ + CardType.MultiLineReversed, + "Donec dapibus ullamcorper aliquam.\n??\nDonec dapibus ullamcorper aliquam.\n", + 9, + ], + ]); }); test("Test codeblocks", () => { @@ -245,6 +317,7 @@ test("Test codeblocks", () => { }); test("Test not parsing cards in HTML comments", () => { + // Legacy syntax expect( parse("\n-->", ...defaultArgs) ).toEqual([]); @@ -254,6 +327,17 @@ test("Test not parsing cards in HTML comments", () => { ...defaultArgs ) ).toEqual([]); + + // New syntax + expect( + parse("\n-->", ...defaultArgs) + ).toEqual([]); + expect( + parse( + "\n\n-->", + ...defaultArgs + ) + ).toEqual([]); expect(parse("", ...defaultArgs)).toEqual([]); expect(parse("", ...defaultArgs)).toEqual([]); }); diff --git a/tests/unit/scheduling.test.ts b/tests/unit/scheduling.test.ts index e72646f7..90004bf4 100644 --- a/tests/unit/scheduling.test.ts +++ b/tests/unit/scheduling.test.ts @@ -20,6 +20,13 @@ test("Test reviewing with default settings", () => { expect( schedule(ReviewResponse.Hard, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {}) + ).toEqual({ + ease: DEFAULT_SETTINGS.baseEase, + interval: 5, + }); + + expect( + schedule(ReviewResponse.Impossible, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {}) ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, interval: 1, @@ -46,96 +53,115 @@ test("Test reviewing with default settings & delay", () => { schedule(ReviewResponse.Hard, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}) ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, - interval: 1, + interval: 5, }); -}); -test("Test load balancing, small interval (load balancing disabled)", () => { - const dueDates = { - 0: 1, - 1: 1, - 2: 1, - 3: 4, - }; expect( - schedule(ReviewResponse.Good, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates) + schedule(ReviewResponse.Impossible, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}) ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, - interval: 10, - }); - expect(dueDates).toEqual({ - 0: 2, - 1: 1, - 2: 1, - 3: 4, + interval: 1, }); }); -test("Test load balancing", () => { - // interval < 7 - let dueDates: Record = { - 5: 2, - }; - expect( - schedule(ReviewResponse.Good, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates) - ).toEqual({ - ease: DEFAULT_SETTINGS.baseEase, - interval: 10, +test("Test load balancing at 1 day", () => { + const interval = MINUTES_PER_DAY; + + // Easy review + // 1 day review becomes 4 day review + expect(schedule(ReviewResponse.Easy, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ + ease: DEFAULT_SETTINGS.baseEase + 20, + interval: 5054, }); - expect(dueDates).toEqual({ - 0: 1, - 5: 2, + + // Good review + // 1 day review becomes 3 day review + expect(schedule(ReviewResponse.Good, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ + ease: DEFAULT_SETTINGS.baseEase, + interval: 3600 }); - // 7 <= interval < 30 - dueDates = { - 25: 2, - }; - expect( - schedule(ReviewResponse.Good, 10, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates) - ).toEqual({ + // Hard review + // 1 day review becomes 1 day review + expect(schedule(ReviewResponse.Hard, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ ease: DEFAULT_SETTINGS.baseEase, - interval: 10, - }); - expect(dueDates).toEqual({ - 0: 1, - 25: 2, + interval: 1440, }); - // interval >= 30 - dueDates = { - 2: 5, - 59: 8, - 60: 9, - 61: 3, - 62: 5, - 63: 4, - 64: 4, - 65: 8, - 66: 2, - 67: 10, - }; - expect( - schedule(ReviewResponse.Good, 25, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates) - ).toEqual({ + // Impossible review + // 1 day review becomes 5m review + expect(schedule(ReviewResponse.Impossible, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ ease: DEFAULT_SETTINGS.baseEase, - interval: 10, - }); - expect(dueDates).toEqual({ - 0: 1, - 2: 5, - 59: 8, - 60: 9, - 61: 3, - 62: 5, - 63: 4, - 64: 4, - 65: 8, - 66: 2, - 67: 10, + interval: 5 }); }); +// test("Test load balancing at 2 weeks", () => { +// const interval = 14 * MINUTES_PER_DAY; +// // Easy review +// let dueDates = { +// 2: 5, +// 59: 8, +// }; + +// // 1 day review becomes 4 day review +// let schedObj = schedule(ReviewResponse.Easy, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates); +// expect(schedObj["ease"]).toEqual(DEFAULT_SETTINGS.baseEase + 20); +// expect(schedObj["interval"]).toEqual(70762); +// expect(dueDates).toEqual({ +// 2: 5, +// 59: 8, +// }); + +// // Good review +// dueDates = { +// 2: 5, +// 59: 8, +// }; + +// // 1 day review becomes 3 day review +// schedObj = schedule(ReviewResponse.Good, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates); +// expect(schedObj["ease"]).toEqual(DEFAULT_SETTINGS.baseEase); +// expect(schedObj["interval"]).toEqual(3600); +// expect(dueDates).toEqual({ +// 2: 5, +// 3: 1, +// 59: 8, +// }); + +// // Hard review +// dueDates = { +// 2: 5, +// 59: 8, +// }; + +// // 1 day review becomes 10m review +// schedObj = schedule(ReviewResponse.Hard, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates); +// expect(schedObj["ease"]).toEqual(DEFAULT_SETTINGS.baseEase); +// expect(schedObj["interval"]).toEqual(10); +// expect(dueDates).toEqual({ +// 0: 1, +// 2: 5, +// 59: 8, +// }); + +// // Impossible review +// dueDates = { +// 2: 5, +// 59: 8, +// }; + +// // 1 day review becomes 10m review +// schedObj = schedule(ReviewResponse.Impossible, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates); +// expect(schedObj["ease"]).toEqual(DEFAULT_SETTINGS.baseEase); +// expect(schedObj["interval"]).toEqual(5); +// expect(dueDates).toEqual({ +// 0: 1, +// 2: 5, +// 59: 8, +// }); +// }); + test("Test textInterval - desktop", () => { expect(textInterval(1, false)).toEqual("1 minute(s)"); expect(textInterval(1 * MINUTES_PER_DAY, false)).toEqual("1 day(s)"); From 658be3f13b27b1e1970512cc59479ce8e8c5860b Mon Sep 17 00:00:00 2001 From: psaunderualberta Date: Thu, 15 Jun 2023 12:54:59 -0600 Subject: [PATCH 13/15] Achieving 100% branch & stmt coverage on scheduling.ts --- src/scheduling.ts | 14 +- tests/unit/scheduling.test.ts | 349 +++++++++++++++++++++++++--------- 2 files changed, 267 insertions(+), 96 deletions(-) diff --git a/src/scheduling.ts b/src/scheduling.ts index 57330e00..08e1fb5d 100644 --- a/src/scheduling.ts +++ b/src/scheduling.ts @@ -109,20 +109,18 @@ export function schedule( if (interval > 4 * MINUTES_PER_DAY) { let fuzz = 0; - if (interval < 7 * MINUTES_PER_DAY) { - fuzz = 1; - } else if (interval < 30 * MINUTES_PER_DAY) { - fuzz = Math.max(2, Math.floor(interval * 0.15)); + if (interval <= 7 * MINUTES_PER_DAY) { + fuzz = MINUTES_PER_DAY; + } else if (interval <= 30 * MINUTES_PER_DAY) { + fuzz = Math.max(2 * MINUTES_PER_DAY, Math.floor(interval * 0.15)); } else { - fuzz = Math.max(4, Math.floor(interval * 0.05)); + fuzz = Math.max(4 * MINUTES_PER_DAY, Math.floor(interval * 0.05)); } - fuzz *= MINUTES_PER_DAY; - const originalInterval = interval; outer: for (let i = MINUTES_PER_DAY; i <= fuzz; i += MINUTES_PER_DAY) { for (let ivl of [originalInterval - i, originalInterval + i]) { - ivl = roundInterval(ivl, settingsObj.maximumInterval);; + ivl = roundInterval(ivl, settingsObj.maximumInterval); const dayIvl = Math.round(ivl / MINUTES_PER_DAY); if (!Object.prototype.hasOwnProperty.call(dueDates, dayIvl)) { dueDates[dayIvl] = 0; diff --git a/tests/unit/scheduling.test.ts b/tests/unit/scheduling.test.ts index 90004bf4..270d717b 100644 --- a/tests/unit/scheduling.test.ts +++ b/tests/unit/scheduling.test.ts @@ -33,134 +33,307 @@ test("Test reviewing with default settings", () => { }); }); -test("Test reviewing with default settings & delay", () => { - const delay = 2 * 24 * 3600 * 1000; // two day delay +test("Test reviewing with default settings & 1 day interval", () => { + const interval = MINUTES_PER_DAY; + + // Easy review + // 1 day review becomes 4 day review + expect(schedule(ReviewResponse.Easy, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ + ease: DEFAULT_SETTINGS.baseEase + 20, + interval: 5054, + }); + + // Good review + // 1 day review becomes 3 day review + expect(schedule(ReviewResponse.Good, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ + ease: DEFAULT_SETTINGS.baseEase, + interval: 3600 + }); + + // Hard review + // 1 day review becomes 1 day review + expect(schedule(ReviewResponse.Hard, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ + ease: DEFAULT_SETTINGS.baseEase, + interval: 1440, + }); + + // Impossible review + // 1 day review becomes 5m review + expect(schedule(ReviewResponse.Impossible, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ + ease: DEFAULT_SETTINGS.baseEase, + interval: 5 + }); +}); + +test("Test reviewing with default settings & 1 day delay", () => { + const delay = MINUTES_PER_DAY; // two day delay + const interval = 10; + expect( - schedule(ReviewResponse.Easy, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}) + schedule(ReviewResponse.Easy, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}) ).toEqual({ ease: DEFAULT_SETTINGS.baseEase + 20, interval: 1440, }); expect( - schedule(ReviewResponse.Good, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}) + schedule(ReviewResponse.Good, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}) ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, interval: 10, }); expect( - schedule(ReviewResponse.Hard, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}) + schedule(ReviewResponse.Hard, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}) ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, interval: 5, }); expect( - schedule(ReviewResponse.Impossible, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}) + schedule(ReviewResponse.Impossible, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}) ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, interval: 1, }); }); -test("Test load balancing at 1 day", () => { - const interval = MINUTES_PER_DAY; - +test("Test 'easy' load balancing at 5 days", () => { + const interval = 5 * MINUTES_PER_DAY; + const delay = 0; + // Easy review - // 1 day review becomes 4 day review - expect(schedule(ReviewResponse.Easy, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ + const dueDates = { + 16: 1, + 18: 1, + 19: 1, + 20: 1 + }; + + // 5 day review becomes 15 day review + expect( + schedule(ReviewResponse.Easy, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, dueDates) + ).toEqual({ ease: DEFAULT_SETTINGS.baseEase + 20, - interval: 5054, + interval: 23832, }); - + + expect(dueDates).toEqual({ + 16: 1, + 17: 1, + 18: 1, + 19: 1, + 20: 1 + }); +}); + +test("Test 'good' load balancing at 5 days", () => { + const interval = 5 * MINUTES_PER_DAY; + const delay = 0; + // Good review - // 1 day review becomes 3 day review - expect(schedule(ReviewResponse.Good, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ + const dueDates = { + 9: 1, + 10: 2, + 11: 2, + 12: 2, + 14: 1, + 15: 2 + }; + + // 5 day review becomes 13 day review + expect( + schedule(ReviewResponse.Good, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, dueDates) + ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, - interval: 3600 + interval: 18000, }); + expect(dueDates).toEqual({ + 9: 1, + 10: 2, + 11: 2, + 12: 2, + 13: 1, + 14: 1, + 15: 2 + }); +}); + +test("Test 'hard' load balancing at 5 days", () => { + const interval = 5 * MINUTES_PER_DAY; + const delay = 0; + // Hard review - // 1 day review becomes 1 day review - expect(schedule(ReviewResponse.Hard, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ + const dueDates = { + 1: 1, + 2: 1, + 3: 1, + 4: 1, + }; + + // 5 day review becomes same-day review + expect( + schedule(ReviewResponse.Hard, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, dueDates) + ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, - interval: 1440, + interval: 3600, }); - // Impossible review - // 1 day review becomes 5m review - expect(schedule(ReviewResponse.Impossible, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {})).toEqual({ + expect(dueDates).toEqual({ + 1: 1, + 2: 1, + 3: 2, + 4: 1, + }); +}); + +test("Test 'impossible' load balancing at 5 days", () => { + const interval = 5 * MINUTES_PER_DAY; + const delay = 0; + + // Good review + const dueDates = { + 1: 1, + 2: 1, + 3: 2, + 4: 1, + }; + + // 5 day review becomes 10 day review + expect( + schedule(ReviewResponse.Impossible, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, dueDates) + ).toEqual({ ease: DEFAULT_SETTINGS.baseEase, - interval: 5 + interval: 5, + }); + + expect(dueDates).toEqual({ + 0: 1, + 1: 1, + 2: 1, + 3: 2, + 4: 1, }); }); -// test("Test load balancing at 2 weeks", () => { -// const interval = 14 * MINUTES_PER_DAY; -// // Easy review -// let dueDates = { -// 2: 5, -// 59: 8, -// }; - -// // 1 day review becomes 4 day review -// let schedObj = schedule(ReviewResponse.Easy, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates); -// expect(schedObj["ease"]).toEqual(DEFAULT_SETTINGS.baseEase + 20); -// expect(schedObj["interval"]).toEqual(70762); -// expect(dueDates).toEqual({ -// 2: 5, -// 59: 8, -// }); - -// // Good review -// dueDates = { -// 2: 5, -// 59: 8, -// }; - -// // 1 day review becomes 3 day review -// schedObj = schedule(ReviewResponse.Good, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates); -// expect(schedObj["ease"]).toEqual(DEFAULT_SETTINGS.baseEase); -// expect(schedObj["interval"]).toEqual(3600); -// expect(dueDates).toEqual({ -// 2: 5, -// 3: 1, -// 59: 8, -// }); - -// // Hard review -// dueDates = { -// 2: 5, -// 59: 8, -// }; - -// // 1 day review becomes 10m review -// schedObj = schedule(ReviewResponse.Hard, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates); -// expect(schedObj["ease"]).toEqual(DEFAULT_SETTINGS.baseEase); -// expect(schedObj["interval"]).toEqual(10); -// expect(dueDates).toEqual({ -// 0: 1, -// 2: 5, -// 59: 8, -// }); - -// // Impossible review -// dueDates = { -// 2: 5, -// 59: 8, -// }; - -// // 1 day review becomes 10m review -// schedObj = schedule(ReviewResponse.Impossible, interval, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates); -// expect(schedObj["ease"]).toEqual(DEFAULT_SETTINGS.baseEase); -// expect(schedObj["interval"]).toEqual(5); -// expect(dueDates).toEqual({ -// 0: 1, -// 2: 5, -// 59: 8, -// }); -// }); +test("Test 'easy' load balancing at 2 weeks", () => { + const interval = 14 * MINUTES_PER_DAY; + const delay = 0; + + const dueDates = { + 47: 1, + 48: 1, + 49: 2, + 50: 1, + 52: 1, + }; + + // 2 week review becomes same-day review + expect( + schedule(ReviewResponse.Easy, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, dueDates) + ).toEqual({ + ease: DEFAULT_SETTINGS.baseEase + 20, + interval: 73642, + }); + + expect(dueDates).toEqual({ + 47: 1, + 48: 1, + 49: 2, + 50: 1, + 51: 1, + 52: 1, + }); +}); + +test("Test 'good' load balancing at 2 weeks", () => { + const interval = 14 * MINUTES_PER_DAY; + const delay = 0; + + const dueDates = { + 33: 1, + 34: 1, + 35: 1, + 36: 1, + 38: 1, + }; + + // 2 week review becomes same-day review + expect( + schedule(ReviewResponse.Good, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, dueDates) + ).toEqual({ + ease: DEFAULT_SETTINGS.baseEase, + interval: 53280, + }); + + expect(dueDates).toEqual({ + 33: 1, + 34: 1, + 35: 1, + 36: 1, + 37: 1, + 38: 1, + }); +}); + +test("Test 'hard' load balancing at 2 weeks", () => { + const interval = 14 * MINUTES_PER_DAY; + const delay = 0; + + const dueDates = { + 4: 1, + 5: 1, + 7: 1, + 8: 1, + 9: 1, + }; + + // 2 week review becomes 5-day + expect( + schedule(ReviewResponse.Hard, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, dueDates) + ).toEqual({ + ease: DEFAULT_SETTINGS.baseEase, + interval: 8640, + }); + + expect(dueDates).toEqual({ + 4: 1, + 5: 1, + 6: 1, + 7: 1, + 8: 1, + 9: 1, + }); +}); + +test("Test 'impossible' load balancing at 2 weeks", () => { + const interval = 14 * MINUTES_PER_DAY; + const delay = 0; + + const dueDates = { + 1: 1, + 2: 1, + 3: 2, + 4: 1, + }; + + // 2 week review becomes same-day review + expect( + schedule(ReviewResponse.Impossible, interval, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, dueDates) + ).toEqual({ + ease: DEFAULT_SETTINGS.baseEase, + interval: 5, + }); + + expect(dueDates).toEqual({ + 0: 1, + 1: 1, + 2: 1, + 3: 2, + 4: 1, + }); +}); test("Test textInterval - desktop", () => { expect(textInterval(1, false)).toEqual("1 minute(s)"); From d46d7b0f337f3b6ce0e6b1b9be3bdf3789147718 Mon Sep 17 00:00:00 2001 From: psaunderualberta Date: Thu, 15 Jun 2023 12:56:04 -0600 Subject: [PATCH 14/15] Added deck.test.ts --- tests/unit/deck.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/unit/deck.test.ts diff --git a/tests/unit/deck.test.ts b/tests/unit/deck.test.ts new file mode 100644 index 00000000..e69de29b From 4c99b20174ee13bda777934b86d345948747208d Mon Sep 17 00:00:00 2001 From: psaunderualberta Date: Thu, 15 Jun 2023 12:57:13 -0600 Subject: [PATCH 15/15] Got rid of deck.test.ts, fits better as e2e tests --- tests/unit/deck.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/unit/deck.test.ts diff --git a/tests/unit/deck.test.ts b/tests/unit/deck.test.ts deleted file mode 100644 index e69de29b..00000000