Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support richer set of flashcard ordering during review #820

Merged
merged 25 commits into from
Jan 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
55cb01e
Implemented WeightedRandomNumber
ronzulu Oct 5, 2023
73327e6
Refactoring in preparation for additional functionality
ronzulu Oct 6, 2023
c6249ac
Implemented DeckOrder.EveryCardRandomDeck; not tested
ronzulu Oct 9, 2023
dcea940
Enhanced card ordering implemented not tested
ronzulu Oct 10, 2023
5e307f5
Added test cases; plus a fix
ronzulu Oct 11, 2023
3ff8f16
Implemented settings for new card/deck ordering features
ronzulu Oct 13, 2023
04e58d1
Merge branch 'st3v3nmw:master' into enhanced-card-ordering
ronzulu Oct 18, 2023
509c49a
Updated to use text resources rather than hard coding strings
ronzulu Oct 18, 2023
c885f34
Fixed bug that occurs if no existing settings file present
ronzulu Oct 18, 2023
a366691
Minor description update
ronzulu Oct 24, 2023
ab7a4d2
Merge branch 'st3v3nmw:master' into enhanced-card-review-ordering
ronzulu Oct 24, 2023
5a42ef3
Merge branch 'st3v3nmw:master' into enhanced-card-ordering
ronzulu Oct 25, 2023
8bd0750
Merge remote-tracking branch 'upstream/master' into enhanced-card-ord…
ronzulu Oct 25, 2023
19f6917
Merge branch 'enhanced-card-ordering' of https://github.com/ronzulu/o…
ronzulu Oct 25, 2023
30bdc75
Merge branch 'enhanced-card-review-ordering' of https://github.com/ro…
ronzulu Oct 26, 2023
fa28daa
Comments; added user text for non-English languages
ronzulu Nov 4, 2023
1ef4fd2
Merge remote-tracking branch 'upstream/master' into enhanced-card-rev…
ronzulu Nov 4, 2023
d79fb83
pnpm format
ronzulu Nov 4, 2023
6953a52
Merge remote-tracking branch 'upstream/master' into enhanced-card-rev…
ronzulu Nov 29, 2023
3c45661
Added debug
ronzulu Nov 29, 2023
d490ae2
Remove debug and fix
ronzulu Nov 29, 2023
91fd1ad
Fixed bug for PrevDeckComplete_Random setting
ronzulu Dec 25, 2023
11f346e
Fixed test case
ronzulu Dec 25, 2023
7d5fc39
Fixed issues that lint complained about
ronzulu Dec 25, 2023
bf53421
Merge branch 'master' into enhanced-card-review-ordering
st3v3nmw Dec 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).

#### [Unreleased]

- Feat: Support richer set of flashcard ordering during review; e.g. random card from random deck [`#814`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/814)
- Bug fix Problem with nested list item's indentation [`#800`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/800)
- Bug fix Problem with nested list item's indentation [`#812`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/812)
- Bug Cloze Breaks When }} Encountered [`#799`](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/799)
Expand Down
1 change: 0 additions & 1 deletion src/Deck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export class Deck {
private getQuestionCardCountForCardListType(question: Question, cards: Card[]): number {
let result: number = 0;
for (let i = 0; i < cards.length; i++) {
const card = cards[i];
if (Object.is(question, cards[i].question)) result++;
}
return result;
Expand Down
261 changes: 196 additions & 65 deletions src/DeckTreeIterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@ import { Card } from "./Card";
import { CardListType, Deck } from "./Deck";
import { Question } from "./Question";
import { TopicPath } from "./TopicPath";
import { globalRandomNumberProvider } from "./util/RandomNumberProvider";

export enum CardListOrder {
NewFirst,
DueFirst,
Random,
import { WeightedRandomNumber, globalRandomNumberProvider } from "./util/RandomNumberProvider";

export enum CardOrder {
NewFirstSequential,
NewFirstRandom,
DueFirstSequential,
DueFirstRandom,
EveryCardRandomDeckAndCard,
}
export enum OrderMethod {
Sequential,
Random,
export enum DeckOrder {
PrevDeckComplete_Sequential,
PrevDeckComplete_Random,
}
export enum IteratorDeckSource {
UpdatedByIterator,
CloneBeforeUse,
}

export interface IIteratorOrder {
// Choose decks in sequential order, or randomly
deckOrder: OrderMethod;
// Within a deck this specifies the order the cards should be reviewed
// e.g. new first, going sequentially
cardOrder: CardOrder;

// Within a deck, choose: new cards first, due cards first, or randomly
cardListOrder: CardListOrder;

// Within a card list (i.e. either new or due), choose cards sequentially or randomly
cardOrder: OrderMethod;
// Choose decks in sequential order, or randomly
deckOrder: DeckOrder;
}

export interface IDeckTreeIterator {
Expand All @@ -46,70 +46,106 @@ class SingleDeckIterator {
preferredCardListType: CardListType;
cardIdx?: number;
cardListType?: CardListType;
weightedRandomNumber: WeightedRandomNumber;

get hasCurrentCard(): boolean {
return this.cardIdx != null;
}

get currentCard(): Card {
if (this.cardIdx == null) return null;
return this.deck.getCard(this.cardIdx, this.cardListType);
let result: Card = null;
if (this.cardIdx != null) result = this.deck.getCard(this.cardIdx, this.cardListType);
return result;
}

constructor(iteratorOrder: IIteratorOrder) {
this.iteratorOrder = iteratorOrder;
this.preferredCardListType =
this.iteratorOrder.cardListOrder == CardListOrder.DueFirst
? CardListType.DueCard
: CardListType.NewCard;
this.preferredCardListType = SingleDeckIterator.getCardListTypeForIterator(
this.iteratorOrder,
);
this.weightedRandomNumber = WeightedRandomNumber.create();
}

setDeck(deck: Deck): void {
this.deck = deck;
this.setCardListType(null);
}

private setCardListType(cardListType?: CardListType): void {
//
// 0 <= cardIndex < newFlashcards.length + dueFlashcards.length
//
setNewOrDueCardIdx(cardIndex: number): void {
let cardListType: CardListType = CardListType.NewCard;
let index: number = cardIndex;
if (cardIndex >= this.deck.newFlashcards.length) {
cardListType = CardListType.DueCard;
index = cardIndex - this.deck.newFlashcards.length;
}
this.setCardListType(cardListType, index);
}

private setCardListType(cardListType?: CardListType, cardIdx: number = null): void {
this.cardListType = cardListType;
this.cardIdx = null;
this.cardIdx = cardIdx;
}

nextCard(): boolean {
// First return cards in the preferred list
if (this.cardListType == null) {
this.setCardListType(this.preferredCardListType);
}
if (this.iteratorOrder.cardOrder == CardOrder.EveryCardRandomDeckAndCard) {
this.nextRandomCard();
} else {
// First return cards in the preferred list
if (this.cardListType == null) {
this.setCardListType(this.preferredCardListType);
}

if (!this.nextCardWithinList()) {
if (this.cardListType == this.preferredCardListType) {
// Nothing left in the preferred list, so try the non-preferred list type
this.setCardListType(Deck.otherListType(this.cardListType));
if (!this.nextCardWithinList()) {
this.setCardListType(null);
if (!this.nextCardWithinCurrentList()) {
if (this.cardListType == this.preferredCardListType) {
// Nothing left in the preferred list, so try the non-preferred list type
this.setCardListType(Deck.otherListType(this.cardListType));
if (!this.nextCardWithinCurrentList()) {
this.setCardListType(null);
}
} else {
this.cardIdx = null;
}
} else {
this.cardIdx = null;
}
}

return this.cardIdx != null;
}

private nextCardWithinList(): boolean {
let result: boolean = false;
private nextRandomCard(): void {
const newCount: number = this.deck.newFlashcards.length;
const dueCount: number = this.deck.dueFlashcards.length;
if (newCount + dueCount > 0) {
// Generate a random number such that the probability of picking an individual card is the same
// regardless of whether the card is in the new/due list, or which list has more cards
// I.e. we don't pick the new/due list first at 50/50 and then a random card within it
const weights: Partial<Record<CardListType, number>> = {};
if (newCount > 0) weights[CardListType.NewCard] = newCount;
if (dueCount > 0) weights[CardListType.DueCard] = dueCount;
const [cardListType, index] = this.weightedRandomNumber.getRandomValues(weights);
this.setCardListType(cardListType, index);
} else {
this.setCardListType(null);
}
}

private nextCardWithinCurrentList(): boolean {
const cardList: Card[] = this.deck.getCardListForCardType(this.cardListType);

// Delete the current card so we don't return it again
if (this.hasCurrentCard) {
this.deleteCurrentCard();
}
result = cardList.length > 0;
const result: boolean = cardList.length > 0;
if (result) {
switch (this.iteratorOrder.cardOrder) {
case OrderMethod.Sequential:
case CardOrder.DueFirstSequential:
case CardOrder.NewFirstSequential:
// We always pick the card with index 0
// Sequential retrieval occurs by the caller deleting the card at this index after it is used
this.cardIdx = 0;
break;

case OrderMethod.Random:
case CardOrder.DueFirstRandom:
case CardOrder.NewFirstRandom:
this.cardIdx = globalRandomNumberProvider.getInteger(0, cardList.length - 1);
break;
}
Expand Down Expand Up @@ -158,17 +194,32 @@ class SingleDeckIterator {
private ensureCurrentCard() {
if (this.cardIdx == null || this.cardListType == null) throw "no current card";
}

private static getCardListTypeForIterator(iteratorOrder: IIteratorOrder): CardListType | null {
let result: CardListType = null;
switch (iteratorOrder.cardOrder) {
case CardOrder.DueFirstRandom:
case CardOrder.DueFirstSequential:
result = CardListType.DueCard;
break;

case CardOrder.NewFirstRandom:
case CardOrder.NewFirstSequential:
result = CardListType.NewCard;
break;
}
return result;
}
}

export class DeckTreeIterator implements IDeckTreeIterator {
deckTree: Deck;
preferredCardListType: CardListType;
iteratorOrder: IIteratorOrder;
deckSource: IteratorDeckSource;
private iteratorOrder: IIteratorOrder;
private deckSource: IteratorDeckSource;

singleDeckIterator: SingleDeckIterator;
deckArray: Deck[];
deckIdx?: number;
private singleDeckIterator: SingleDeckIterator;
private deckArray: Deck[];
private deckIdx?: number;
private weightedRandomNumber: WeightedRandomNumber;

get hasCurrentCard(): boolean {
return this.deckIdx != null && this.singleDeckIterator.hasCurrentCard;
Expand All @@ -180,49 +231,120 @@ export class DeckTreeIterator implements IDeckTreeIterator {
}

get currentCard(): Card {
if (this.deckIdx == null || !this.singleDeckIterator.hasCurrentCard) return null;
return this.singleDeckIterator.currentCard;
let result: Card = null;
if (this.deckIdx != null && this.singleDeckIterator.hasCurrentCard)
result = this.singleDeckIterator.currentCard;
return result;
}

constructor(iteratorOrder: IIteratorOrder, deckSource: IteratorDeckSource) {
this.singleDeckIterator = new SingleDeckIterator(iteratorOrder);
this.iteratorOrder = iteratorOrder;
this.deckSource = deckSource;
this.weightedRandomNumber = WeightedRandomNumber.create();
}

setDeck(deck: Deck): void {
// We don't want to change the supplied deck, so first clone
if (this.deckSource == IteratorDeckSource.CloneBeforeUse) deck = deck.clone();

this.deckTree = deck;
this.deckArray = deck.toDeckArray();
this.deckArray = DeckTreeIterator.filterForDecksWithCards(deck.toDeckArray());
this.setDeckIdx(null);
}

private static filterForDecksWithCards(sourceArray: Deck[]): Deck[] {
const result: Deck[] = [];
for (let idx = 0; idx < sourceArray.length; idx++) {
const deck: Deck = sourceArray[idx];
const hasAnyCards = deck.getCardCount(CardListType.All, false) > 0;
if (hasAnyCards) {
result.push(deck);
}
}
return result;
}

private setDeckIdx(deckIdx?: number): void {
this.deckIdx = deckIdx;
if (deckIdx != null) this.singleDeckIterator.setDeck(this.deckArray[deckIdx]);
}

nextCard(): boolean {
let result: boolean = false;
if (this.deckIdx == null) {
this.setDeckIdx(0);

// Delete the current card so we don't return it again
if (this.hasCurrentCard) {
this.singleDeckIterator.deleteCurrentCard();
}
while (this.deckIdx < this.deckArray.length) {
if (this.singleDeckIterator.nextCard()) {
result = true;
break;

if (this.iteratorOrder.cardOrder == CardOrder.EveryCardRandomDeckAndCard) {
result = this.nextCard_EveryCardRandomDeck();
} else {
// If we are just starting, then depending on settings we want to either start from the first deck,
// or a random deck
if (this.deckIdx == null) {
this.chooseNextDeck(true);
}
this.deckIdx++;
if (this.deckIdx < this.deckArray.length) {
this.singleDeckIterator.setDeck(this.deckArray[this.deckIdx]);
while (this.deckIdx < this.deckArray.length) {
if (this.singleDeckIterator.nextCard()) {
result = true;
break;
}
this.chooseNextDeck(false);
}
}
if (!result) this.deckIdx = null;
return result;
}

private chooseNextDeck(firstTime: boolean): void {
switch (this.iteratorOrder.deckOrder) {
case DeckOrder.PrevDeckComplete_Sequential:
this.deckIdx = firstTime ? 0 : this.deckIdx + 1;
break;

case DeckOrder.PrevDeckComplete_Random: {
// Equal probability of picking any deck that has cards within
const weights: Record<number, number> = {};
let hasDeck: boolean = false;
for (let i = 0; i < this.deckArray.length; i++) {
if (this.deckArray[i].getCardCount(CardListType.All, false)) {
weights[i] = 1;
hasDeck = true;
}
}
if (hasDeck) {
const [deckIdx, _] = this.weightedRandomNumber.getRandomValues(weights);
this.deckIdx = deckIdx;
} else {
// Our signal that no deck with cards present
this.deckIdx = this.deckArray.length;
}
break;
}
}
if (this.deckIdx < this.deckArray.length) {
this.singleDeckIterator.setDeck(this.deckArray[this.deckIdx]);
}
}

private nextCard_EveryCardRandomDeck(): boolean {
// Make the chance of picking a specific deck proportional to the number of cards within
const weights: Record<number, number> = {};
for (let i = 0; i < this.deckArray.length; i++) {
const cardCount: number = this.deckArray[i].getCardCount(CardListType.All, false);
if (cardCount) {
weights[i] = cardCount;
}
}
if (Object.keys(weights).length == 0) return false;

const [deckIdx, cardIdx] = this.weightedRandomNumber.getRandomValues(weights);
this.setDeckIdx(deckIdx);
this.singleDeckIterator.setNewOrDueCardIdx(cardIdx);
return true;
}

deleteCurrentQuestion(): boolean {
this.singleDeckIterator.deleteCurrentQuestion();
return this.nextCard();
Expand All @@ -236,4 +358,13 @@ export class DeckTreeIterator implements IDeckTreeIterator {
moveCurrentCardToEndOfList(): void {
this.singleDeckIterator.moveCurrentCardToEndOfList();
}

private removeCurrentDeckIfEmpty(): void {
if (this.currentDeck.getCardCount(CardListType.All, false) == 0) {
this.deckArray.splice(this.deckIdx, 1);

// There is no change to deckIdx, but this now is a different deck
if (this.deckIdx < this.deckArray.length) this.setDeckIdx(this.deckIdx);
}
}
}
Loading
Loading