summaryrefslogtreecommitdiff
path: root/src/study/SpacedRepetitionModel.cpp
diff options
context:
space:
mode:
authorJedidiah Barber <contact@jedbarber.id.au>2021-07-14 11:49:10 +1200
committerJedidiah Barber <contact@jedbarber.id.au>2021-07-14 11:49:10 +1200
commitd24f813f3f2a05c112e803e4256b53535895fc98 (patch)
tree601e6ae9a1cd44bcfdcf91739a5ca36aedd827c9 /src/study/SpacedRepetitionModel.cpp
Initial mirror commitHEADmaster
Diffstat (limited to 'src/study/SpacedRepetitionModel.cpp')
-rw-r--r--src/study/SpacedRepetitionModel.cpp390
1 files changed, 390 insertions, 0 deletions
diff --git a/src/study/SpacedRepetitionModel.cpp b/src/study/SpacedRepetitionModel.cpp
new file mode 100644
index 0000000..e10299a
--- /dev/null
+++ b/src/study/SpacedRepetitionModel.cpp
@@ -0,0 +1,390 @@
+#include "SpacedRepetitionModel.h"
+
+#include <time.h>
+#include <QtDebug>
+#include <QtCore>
+
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/Card.h"
+#include "../dictionary/CardPack.h"
+#include "../utils/IRandomGenerator.h"
+#include "../utils/TimeProvider.h"
+
+SpacedRepetitionModel::SpacedRepetitionModel(CardPack* aCardPack, IRandomGenerator* random):
+ IStudyModel(aCardPack),
+ curCard(NULL), settings(StudySettings::inst()), random(random)
+ {
+ srand(time(NULL));
+ curCardNum = 0;
+ connect(aCardPack, SIGNAL(cardsGenerated()), SLOT(updateStudyState()));
+ updateStudyState();
+ }
+
+SpacedRepetitionModel::~SpacedRepetitionModel()
+ {
+ saveStudy();
+ delete random;
+ }
+
+void SpacedRepetitionModel::saveStudy()
+ {
+ if(!cardPack)
+ return;
+ if(!cardPack->dictionary())
+ return;
+ cardPack->dictionary()->saveStudy();
+ }
+
+// Called after all cards are generated
+void SpacedRepetitionModel::updateStudyState()
+ {
+ curCard = cardPack->getCard( cardPack->findLastReviewedCard() );
+ curCardNum = 0;
+ if( !cardPack->curCardName().isEmpty() )
+ {
+ prevCard = curCard;
+ curCard = cardPack->getCard( cardPack->curCardName() );
+ if( curCard )
+ {
+ answerTime.start();
+ emit curCardUpdated();
+ }
+ else
+ {
+ curCard = prevCard;
+ pickNextCardAndNotify();
+ }
+ }
+ else
+ pickNextCardAndNotify();
+ }
+
+void SpacedRepetitionModel::scheduleCard(int newGrade)
+ {
+ if(!curCard)
+ return;
+ saveStudyRecord(createNewStudyRecord(newGrade));
+ curCardNum++;
+ pickNextCardAndNotify();
+ }
+
+StudyRecord SpacedRepetitionModel::createNewStudyRecord(int newGrade)
+{
+ StudyRecord prevStudy = curCard->getStudyRecord();
+ StudyRecord newStudy;
+
+ newStudy.grade = newGrade;
+ newStudy.level = getNewLevel(prevStudy, newGrade);
+ newStudy.easiness = getNewEasiness(prevStudy, newGrade);
+ newStudy.interval = getNextInterval(prevStudy, newStudy);
+ newStudy.date = TimeProvider::get();
+ newStudy.setRecallTime(curRecallTime / 1000.);
+ newStudy.setAnswerTime(answerTime.elapsed() / 1000.);
+ return newStudy;
+}
+
+int SpacedRepetitionModel::getNewLevel(const StudyRecord& prevStudy, int newGrade)
+{
+ int level = prevStudy.level;
+ switch(newGrade)
+ {
+ case StudyRecord::Unknown:
+ case StudyRecord::Incorrect:
+ level = StudyRecord::ShortLearning;
+ break;
+ case StudyRecord::Difficult:
+ case StudyRecord::Good:
+ if(prevStudy.isOneDayOld())
+ level = StudyRecord::Repeating;
+ else
+ level++;
+ break;
+ case StudyRecord::Easy:
+ level += 2;
+ break;
+ }
+ if(level > StudyRecord::LongLearning)
+ level = StudyRecord::Repeating;
+ return level;
+}
+
+double SpacedRepetitionModel::getNewEasiness(const StudyRecord& prevStudy,
+ int newGrade)
+{
+ switch(prevStudy.level)
+ {
+ case StudyRecord::ShortLearning:
+ case StudyRecord::LongLearning:
+ if(prevStudy.isOneDayOld())
+ return getChangeableEasiness(prevStudy, newGrade);
+ else
+ return prevStudy.easiness;
+ case StudyRecord::Repeating:
+ return getChangeableEasiness(prevStudy, newGrade);
+ default:
+ return prevStudy.easiness;
+ }
+}
+
+double SpacedRepetitionModel::getChangeableEasiness(const StudyRecord& prevStudy,
+ int newGrade) const
+{
+ double eas = prevStudy.easiness;
+ switch(newGrade)
+ {
+ case StudyRecord::Difficult:
+ eas += settings->difficultDelta;
+ break;
+ case StudyRecord::Easy:
+ eas += settings->easyDelta;
+ break;
+ default:
+ return prevStudy.easiness;
+ }
+ return limitEasiness(eas);
+}
+
+double SpacedRepetitionModel::limitEasiness(double eas) const
+{
+ if(eas < settings->minEasiness)
+ eas = settings->minEasiness;
+ else if(eas > settings->maxEasiness)
+ eas = settings->maxEasiness;
+ return eas;
+}
+
+double SpacedRepetitionModel::getNextInterval(const StudyRecord& prevStudy,
+ const StudyRecord& newStudy)
+{
+ switch(newStudy.level)
+ {
+ case StudyRecord::ShortLearning:
+ if(newStudy.grade == StudyRecord::Incorrect)
+ return settings->incorrectInterval;
+ else
+ return settings->unknownInterval;
+ case StudyRecord::LongLearning:
+ return settings->learningInterval;
+ case StudyRecord::Repeating:
+ return getNextRepeatingInterval(prevStudy, newStudy);
+ default:
+ return 0;
+ }
+}
+
+double SpacedRepetitionModel::getNextRepeatingInterval(const StudyRecord& prevStudy,
+ const StudyRecord& newStudy)
+{
+ switch(prevStudy.level)
+ {
+ case StudyRecord::ShortLearning:
+ return getNextRepeatingIntervalForShortLearning(prevStudy, newStudy);
+ case StudyRecord::LongLearning:
+ return getNextRepeatingIntervalForLongLearning(prevStudy, newStudy);
+ case StudyRecord::Repeating:
+ return getIncreasedInterval(prevStudy.interval, newStudy.easiness);
+ default:
+ return settings->nextDayInterval;
+ }
+}
+
+double SpacedRepetitionModel::getIncreasedInterval(double prevInterval,
+ double newEasiness)
+{
+ double interval = prevInterval * newEasiness;
+ interval += interval *
+ settings->schedRandomness * random->getInRange_11();
+ return interval;
+}
+
+double SpacedRepetitionModel::getNextRepeatingIntervalForShortLearning(
+ const StudyRecord& prevStudy,
+ const StudyRecord& newStudy)
+{
+ if(prevStudy.isOneDayOld())
+ return getIncreasedInterval(settings->nextDayInterval, newStudy.easiness);
+ else
+ return settings->nextDayInterval;
+}
+
+double SpacedRepetitionModel::getNextRepeatingIntervalForLongLearning(
+ const StudyRecord& prevStudy,
+ const StudyRecord& newStudy)
+{
+ if(prevStudy.isOneDayOld())
+ return getIncreasedInterval(settings->nextDayInterval, newStudy.easiness);
+ else if(newStudy.grade == StudyRecord::Easy)
+ return settings->twoDaysInterval;
+ else
+ return settings->nextDayInterval;
+}
+
+void SpacedRepetitionModel::saveStudyRecord(const StudyRecord& newStudy)
+{
+ cardPack->addStudyRecord(curCard->getQuestion(), newStudy);
+}
+
+QList<int> SpacedRepetitionModel::getAvailableGrades() const
+{
+ if(!curCard)
+ return {};
+ StudyRecord study = curCard->getStudyRecord();
+ switch(study.level)
+ {
+ case StudyRecord::New:
+ return {4, 5};
+ case StudyRecord::ShortLearning:
+ case StudyRecord::LongLearning:
+ if(study.isOneDayOld())
+ return {1, 2, 3, 4, 5};
+ else
+ return {1, 2, 4, 5};
+ case StudyRecord::Repeating:
+ return {1, 2, 3, 4, 5};
+ default:
+ return {};
+ }
+}
+
+bool SpacedRepetitionModel::isNew() const
+{
+ return curCard->getStudyRecord().level == StudyRecord::New;
+}
+
+void SpacedRepetitionModel::pickNextCardAndNotify()
+ {
+ answerTime.start();
+ pickNextCard();
+ if(curCard)
+ cardPack->setCurCard(curCard->getQuestion());
+ else
+ cardPack->setCurCard("");
+ // Notify the study window to show the selected card.
+ emit nextCardSelected();
+ }
+
+void SpacedRepetitionModel::pickNextCard()
+{
+ prevCard = curCard;
+ curCard = NULL;
+ pickActiveCard() ||
+ pickNewCard() ||
+ pickLearningCard();
+}
+
+bool SpacedRepetitionModel::mustRandomPickScheduledCard() const
+{
+ return random->getInRange_01() > settings->newCardsShare;
+}
+
+bool SpacedRepetitionModel::reachedNewCardsDayLimit() const
+{
+ return cardPack->getTodayNewCardsNum() >= settings->newCardsDayLimit;
+}
+
+bool SpacedRepetitionModel::pickActiveCard()
+{
+ QStringList activeCards = cardPack->getActiveCards();
+ if(activeCards.isEmpty())
+ return false;
+ if(pickPriorityActiveCard())
+ return true;
+ if(!mustPickScheduledCard())
+ return false;
+ curCard = cardPack->getCard(getRandomStr(activeCards));
+ return true;
+}
+
+bool SpacedRepetitionModel::pickPriorityActiveCard()
+{
+ QStringList priorityCards = cardPack->getPriorityActiveCards();
+ if(priorityCards.isEmpty())
+ return false;
+ QStringList smallestIntervals = cardPack->getSmallestIntervalCards(priorityCards);
+ curCard = cardPack->getCard(getRandomStr(smallestIntervals));
+ return true;
+}
+
+bool SpacedRepetitionModel::mustPickScheduledCard()
+{
+ bool noNewCards = cardPack->getNewCards().isEmpty();
+ if(noNewCards || reachedNewCardsDayLimit() ||
+ tooManyScheduledCards())
+ return true;
+ else
+ return mustRandomPickScheduledCard();
+}
+
+bool SpacedRepetitionModel::tooManyScheduledCards() const
+{
+ return cardPack->countScheduledForTodayCards() >= settings->limitForAddingNewCards;
+}
+
+bool SpacedRepetitionModel::pickNewCard()
+ {
+ if(reachedNewCardsDayLimit())
+ return false;
+ QStringList newCards = cardPack->getNewCards();
+ if(newCards.isEmpty())
+ return false;
+ QString cardName;
+ if(settings->showRandomly)
+ cardName = getRandomStr(newCards);
+ else
+ cardName = newCards.first();
+ curCard = cardPack->getCard(cardName);
+ return true;
+ }
+
+bool SpacedRepetitionModel::pickLearningCard()
+{
+ QStringList learningCards = cardPack->getLearningCards();
+ if(learningCards.isEmpty())
+ return false;
+ QStringList smallestIntervals = cardPack->getSmallestIntervalCards(learningCards);
+ curCard = cardPack->getCard(getRandomStr(smallestIntervals));
+ return true;
+}
+
+QString SpacedRepetitionModel::getRandomStr(const QStringList& list) const
+{
+ return list.at(random->getRand(list.size()));
+}
+
+/// New cards inside the reviewed ones still today
+int SpacedRepetitionModel::estimatedNewReviewedCardsToday() const
+ {
+ if(tooManyScheduledCards())
+ return 0;
+ int scheduledToday = cardPack->countScheduledForTodayCards();
+ int newRev = 0;
+ if( scheduledToday > 0 )
+ {
+ float newShare = settings->newCardsShare;
+ newRev = qRound( scheduledToday * newShare );
+ }
+ else
+ return 0;
+
+ // Check for remained new cards in pack
+ int newCardsNum = cardPack->getNewCards().size();
+ if( newRev > newCardsNum )
+ newRev = newCardsNum;
+
+ // Check for new cards day limit
+ int newCardsDayLimit = settings->newCardsDayLimit;
+ int todayReviewedNewCards = cardPack->getTodayNewCardsNum();
+ int remainedNewCardsLimit = newCardsDayLimit - todayReviewedNewCards;
+ if( newRev > remainedNewCardsLimit )
+ newRev = remainedNewCardsLimit;
+
+ return newRev;
+ }
+
+/** Calculates number of candidate cards to be shown in the current session.
+ */
+int SpacedRepetitionModel::countTodayRemainingCards() const
+ {
+ int timeTriggered = cardPack->getActiveCards().size();
+ return timeTriggered + estimatedNewReviewedCardsToday();
+ }