/* Copyright (C) 2010 Haowen Ning This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.liberty.android.fantastischmemo.scheduler; import android.util.Log; import org.liberty.android.fantastischmemo.entity.LearningData; import org.liberty.android.fantastischmemo.entity.SchedulingAlgorithmParameters; import org.liberty.android.fantastischmemo.modules.ForApplication; import org.liberty.android.fantastischmemo.utils.AMDateUtil; import java.util.Date; import java.util.Random; import javax.inject.Inject; /* * Default scheduler read the algorithm parameters * from the preferences and schedul card based on * modified mnemosyne algorithm. */ @ForApplication public class DefaultScheduler implements Scheduler { private final static String TAG = "DefaultScheduler"; private SchedulingAlgorithmParameters parameters; @Inject public DefaultScheduler(SchedulingAlgorithmParameters parameters) { this.parameters = parameters; } /* * Return the interval of the after schedule the new card */ @Override public LearningData schedule(LearningData oldData, int newGrade, boolean includeNoise) { Date currentDate = new Date(); double actualInterval = AMDateUtil.diffDate(oldData.getLastLearnDate(), currentDate); double scheduleInterval = oldData.getInterval(); double newInterval = 0.0; int oldGrade = oldData.getGrade(); float oldEasiness = oldData.getEasiness(); int newLapses = oldData.getLapses(); int newAcqReps = oldData.getAcqReps(); int newRetReps = oldData.getRetReps(); int newAcqRepsSinceLapse = oldData.getAcqRepsSinceLapse(); int newRetRepsSinceLapse = oldData.getRetRepsSinceLapse(); float newEasiness = oldData.getEasiness(); Date newFirstLearnDate = oldData.getFirstLearnDate(); if(actualInterval <= parameters.getMinimalInterval()){ actualInterval = parameters.getMinimalInterval(); } // new item (unseen = 1 in mnemosyne) if(newAcqReps == 0) { newAcqReps = 1; newEasiness = parameters.getInitialEasiness(); newAcqRepsSinceLapse = 1; newInterval = calculateInitialInterval(newGrade); } else if(!isGradeSuccessful(oldGrade, false) && !isGradeSuccessful(newGrade, false)) { newAcqReps += 1; newAcqRepsSinceLapse += 1; newInterval = 0; } else if(!isGradeSuccessful(oldGrade, false) && isGradeSuccessful(newGrade, false)){ newAcqReps += 1; newAcqRepsSinceLapse += 1; newInterval = parameters.getFailedGradingInterval(newGrade); } else if(isGradeSuccessful(oldGrade, false) && !isGradeSuccessful(newGrade, false)){ newRetReps += 1; newLapses += 1; newAcqRepsSinceLapse = 0; newRetRepsSinceLapse = 0; newInterval = 0; } else if (isGradeSuccessful(oldGrade, false) && isGradeSuccessful(newGrade, false)) { newRetReps += 1; newRetRepsSinceLapse += 1; newInterval = 0; if (actualInterval >= scheduleInterval) { newEasiness = oldEasiness + parameters.getEasinessIncremental(newGrade); if (newEasiness < parameters.getMinimalEasiness()) { newEasiness = parameters.getMinimalEasiness(); } if(actualInterval <= scheduleInterval){ newInterval = actualInterval * newEasiness; // Fix the cram review scheduling problem by using the larger of scheduled interval newInterval = actualInterval * newEasiness > scheduleInterval ? actualInterval * newEasiness : scheduleInterval; } else { newInterval = scheduleInterval * newEasiness; } } else { // If learning a card before it is cheduled // The old interval is used. newInterval = scheduleInterval; } if (newInterval <= parameters.getMinimalInterval()) { Log.w(TAG, "Interval " + newInterval + " is less than " + parameters.getMinimalInterval() + " for old data: " + oldData); newInterval = parameters.getMinimalInterval(); } } /* * By default the noise is included. However, * the estimation of days should not include noise * If the noise is disabled in the algorithm customization. */ if(includeNoise && parameters.getEnableNoise()){ newInterval = newInterval + calculateIntervalNoise(newInterval); } if (this.isCardNew(oldData)) { newFirstLearnDate = new Date(); } LearningData newData = new LearningData(); newData.setId(oldData.getId()); newData.setAcqReps(newAcqReps); newData.setAcqRepsSinceLapse(newAcqRepsSinceLapse); newData.setEasiness(newEasiness); newData.setGrade(newGrade); newData.setLapses(newLapses); newData.setLastLearnDate(currentDate); newData.setNextLearnDate(afterDays(currentDate, newInterval)); newData.setRetReps(newRetReps); newData.setRetRepsSinceLapse(newRetRepsSinceLapse); newData.setFirstLearnDate(newFirstLearnDate); return newData; } /* * This method returns true if the card should not * be repeated immediately. False if it need to be * repeaeted immediately. */ @Override public boolean isCardLearned(LearningData data) { if (isCardNew(data)) { return false; } else if (data.getGrade() < 2) { return false; } else { return true; } } @Override public boolean isCardNew(LearningData data) { return data.getAcqReps() == 0; } @Override public boolean isCardForReview(LearningData data) { if (isCardNew(data)) { return false; } return data.getNextLearnDate().compareTo(new Date()) <= 0; } /* * Whether a grade is considered successful not not. * Unsuccessful grade should be put in the queue. */ private boolean isGradeSuccessful(int grade, boolean isNew) { if (isNew) { return parameters.getInitialInterval(grade) >= parameters.getMinimalInterval(); } else { return parameters.getFailedGradingInterval(grade) >= parameters.getMinimalInterval(); } } /* * interval is in Day. */ private double calculateIntervalNoise(double interval){ // Noise value based on Mnymosyne double noise = 0.0; if(interval <= parameters.getMinimalInterval()){ noise = 0.0; } else if(interval <= 1.99999){ noise = randomNumber(0.0, 1.0); } else if(interval <= 10.0){ noise = randomNumber(-1.0, 1.0); } else if(interval <= 60.0){ noise = randomNumber(-3.0, 3.0); } else { noise = randomNumber(-0.05 * interval, 0.05 * interval); } return noise; } private float calculateInitialInterval(int grade){ return parameters.getInitialInterval(grade); } private double randomNumber(double min, double max){ return min + (new Random()).nextDouble() * (max - min); } private Date afterDays(Date date, double days) { long time = date.getTime() + Math.round(days * 1 * 60 * 60 * 24 * 1000 /* 1 day */); return new Date(time); } }