package info.opencards.learnstrats.ltm; import info.opencards.core.FlashCard; import info.opencards.core.Item; import java.util.Date; /** * A spaced repetition model from the derived SM2-algorithm. For details cf. http://www.supermemo.com/english/ol/sm2.htm * * @author Holger Brandl */ public class LTMItem extends Item implements Cloneable { /** * Easiness factor reflecting the easiness of memorizing and retaining a given item in memory. */ private double easiness; private int numRepetition; private Date lastQuery; /** * A date before the item can not be scheduled. */ private Date skipUntil; public static final String DESIRED_RETENTION = "retPolicy"; public static final int DESIRED_RETENTION_DEFAULT = 80; public LTMItem(FlashCard flashCard) { super(flashCard); reset(); } public void reset() { easiness = 2.5; numRepetition = 0; lastQuery = new Date(System.currentTimeMillis()); skipUntil = null; } /** * Updates the E-factor of this item [0...5]. Small values indicate that the item is (almost) not known. Note: * Anki's approach http://ankisrs.net/docs/FrequentlyAskedQuestions.html#_what_spaced_repetition_algorithm_does_anki_use */ public void updateEFactor(double feedback) { if (feedback > 5 || feedback < 1) throw new RuntimeException("invalid feedback value: " + feedback); // This means that item which are learnt with OpenCards after midnight with // an OpenCards instantiated before midnight will be scheduled for the same day after the user has slept lastQuery = ScheduleUtils.getToday(); numRepetition++; if (feedback < 3) { numRepetition = 0; // return; // this was used until v0.16.1 but it seems to be better to adjust the easiness for every possible feedbackvalue } if (feedback == 3) numRepetition = Math.min(2, numRepetition); easiness += (0.1 - (5 - feedback) * (0.08 + (5 - feedback) * 0.02)); if (easiness < 1.3) { easiness = 1.3; } } /** * Returns the easiness score of this item. The higher the value the better is it memorized by the user. Low values * indicate bad retention in then past. */ public double getEFactor() { return easiness; } public boolean isNew() { return numRepetition == 0 && easiness == 2.5; // not perfect but should work in most cases } public int getNumRepetition() { return numRepetition; } void setNumRepetition(int numRepetition) { this.numRepetition = numRepetition; } public Date getLastQuery() { return lastQuery; } public boolean isScheduledForToday() { Date date = getNextScheduledDate(); return ScheduleUtils.isToday(date) || ScheduleUtils.isBeforeToday(date); } public Date getNextScheduledDate() { int dayInc = getInterRepetitionInterval(numRepetition); // apply the user-desired retention-factor here int desRetention = (Integer) getProperty(DESIRED_RETENTION, DESIRED_RETENTION_DEFAULT); if (dayInc > 2 && desRetention >= 50 && desRetention <= 100) { double incWeight = DESIRED_RETENTION_DEFAULT / (double) desRetention; dayInc = (int) Math.round(incWeight * dayInc); } // make all item to appear at least once a year if (dayInc > 365) dayInc = 365; Date nextDate = ScheduleUtils.getIncDate(lastQuery, dayInc); // change the nextDate to the skipDate if the latter lays in the future if (skipUntil != null) { nextDate = nextDate.compareTo(skipUntil) < 0 ? skipUntil : nextDate; } return nextDate; } private int getInterRepetitionInterval(int numRepetitions) { switch (numRepetitions) { case 0: return 0; case 1: return 1; case 2: return 4; default: int interRepetitionInterval = getInterRepetitionInterval(numRepetitions - 1); return (int) Math.round(interRepetitionInterval * getEFactor() * 0.8); } } public void skipUntil(Date date) { this.skipUntil = date; } public boolean isSkippedForToday() { return skipUntil != null && ScheduleUtils.getDayDiff(skipUntil, ScheduleUtils.getToday()) > 0; } public Object clone() { try { LTMItem clonedItem = (LTMItem) super.clone(); clonedItem.lastQuery = new Date(lastQuery.getTime()); if (skipUntil != null) clonedItem.skipUntil = new Date(skipUntil.getTime()); return clonedItem; } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public String toString() { StringBuilder s = new StringBuilder(); s.append(super.toString() + "; easiness=" + easiness + "; numRepetition=" + numRepetition + "; lastQuery=" + lastQuery); return s.toString(); } public static String exhaustiveStringDump(LTMItem item) { StringBuilder s = new StringBuilder(); s.append("\neasiness=" + item.easiness + "; numRepetition=" + item.numRepetition + "; lastQuery=" + item.lastQuery); s.append("\nnextOnScheduleFor=" + item.getNextScheduledDate() + "; interRepInterval=" + item.getInterRepetitionInterval(item.numRepetition)); s.append("\n"); for (int i = 1; i < 6; i++) { LTMItem cloneItem = (LTMItem) item.clone(); cloneItem.updateEFactor(i); s.append(i + "-->" + cloneItem.getNextScheduledDate() + "; "); } return s.toString(); } }