From d24f813f3f2a05c112e803e4256b53535895fc98 Mon Sep 17 00:00:00 2001 From: Jedidiah Barber Date: Wed, 14 Jul 2021 11:49:10 +1200 Subject: Initial mirror commit --- src/study/SpacedRepetitionModel.cpp | 390 ++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 src/study/SpacedRepetitionModel.cpp (limited to 'src/study/SpacedRepetitionModel.cpp') 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 +#include +#include + +#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 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(); + } -- cgit