#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(); }