package info.opencards.learnstrats.leitner; import info.opencards.Utils; import info.opencards.core.FlashCard; import info.opencards.core.Item; import info.opencards.core.ItemCollection; import info.opencards.ui.preferences.LeitnerSettings; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * An implementation of the leitner flash-card system. * * @author Holger Brandl */ public class LeitnerSystem extends ItemCollection { private final List<List<LeitnerItem>> boxes; transient private List<LeitnerListener> leitnerListeners; private int initBox; /** * Properties of theis leitner system instance (eg. its leraning configuration) */ public static final int LEARNT = 5; public static final int FAILED = -1; public static final int NEW = -100; public LeitnerSystem(Collection<LeitnerSystem> leitnerSystems) { this(); for (LeitnerSystem ls : leitnerSystems) { addAll(ls); for (int i = 0; i < ls.getBoxes().size(); i++) { if (i < boxes.size()) boxes.get(i).addAll(ls.getBox(i)); else boxes.get(boxes.size() - 1).addAll(ls.getBox(i)); } } } public LeitnerSystem() { this(Utils.getPrefs().getInt(LeitnerSettings.NUM_LEITNER_BOXES, LeitnerSettings.NUM_LEITNER_BOXES_DEFAULT), Utils.getPrefs().getInt(LeitnerSettings.INIT_LEITNER_BOXES, LeitnerSettings.INIT_LEITNER_BOXES_DEFAULT) - 1); } public LeitnerSystem(int numDecks, int initBox) { this.initBox = initBox; boxes = new ArrayList<List<LeitnerItem>>(); for (int i = 0; i < numDecks; i++) { boxes.add(new ArrayList<LeitnerItem>()); } } public void addItem(FlashCard flashCard) { LeitnerItem item = new LeitnerItem(flashCard); boxes.get(initBox).add(item); add(item); for (LeitnerListener leitnerListener : getLeitnerListeners()) { leitnerListener.newCard(item); } } public void removeItem(FlashCard flashCard) { LeitnerItem removeItem = (LeitnerItem) findItem(flashCard); List<LeitnerItem> box = getBoxOf(removeItem); assert box != null; box.remove(removeItem); for (LeitnerListener listener : getLeitnerListeners()) { listener.removedCard(removeItem); } super.removeItem(flashCard); } public void moveCard(LeitnerItem card, int newBox) { assert card != null; assert newBox >= 0 && newBox <= boxes.size(); List<LeitnerItem> currentBox = getBoxOf(card); if (currentBox == null) throw new RuntimeException("can not move unregistered card"); currentBox.remove(card); boxes.get(newBox).add(card); for (LeitnerListener listener : getLeitnerListeners()) { listener.boxingChanged(card); } } /** * Moves a given Item to next higher box (if possible). */ public void moveUp(LeitnerItem card) { moveCard(card, Math.min(getBoxIndex(card) + 1, boxes.size() - 1)); } /** * Moves a given Item to next lower box (if possible). */ public void moveDown(LeitnerItem card) { moveCard(card, Math.max(getBoxIndex(card) - 1, 0)); } public int getBoxIndex(LeitnerItem item) { return boxes.indexOf(getBoxOf(item)); } List<LeitnerItem> getBoxOf(LeitnerItem item) { for (List<LeitnerItem> box : boxes) { if (box.contains(item)) return box; } return null; } /** * Labels all cards as unlearnt and puts 'em back into the first box. */ public void reset() { List<LeitnerItem> startBox = boxes.get(initBox); for (int i = 0; i < boxes.size(); i++) { if (i == initBox) continue; startBox.addAll(boxes.get(i)); boxes.get(i).clear(); } // tag all cards as unknown for (LeitnerItem item : startBox) { item.setState(NEW); } for (LeitnerListener listener : getLeitnerListeners()) { listener.boxingChanged(boxes.get(initBox).toArray(new Item[]{})); } } /** * Returns all currently registered cards wrapped lists according to their current box-status. */ public List<List<LeitnerItem>> getBoxes() { return boxes; } /** * Returns a random card contained in a box between <code>minBox</code> and <code>maxBox</code> (both inclusive). */ public Item getRandomCard(int minBox, int maxBox, List<Item> skipList) { List<Item> allCards = getAllCards(minBox, maxBox); if (skipList != null) { allCards.removeAll(skipList); } boolean doReweightBoxProbs = Utils.getPrefs().getBoolean(LeitnerSettings.DO_PREFER_UNLEARNT, LeitnerSettings.DO_PREFER_UNLEARNT_DEFAULT); if (!doReweightBoxProbs) { int nextCardIndex = Utils.getRandGen().nextInt(allCards.size()); return allCards.get(nextCardIndex); } else { double weightFactor = Utils.getPrefs().getInt(LeitnerSettings.PREFER_UNLEARNT_AMOUNT, LeitnerSettings.PREFER_UNLEARNT_DEFAULT); weightFactor = 0.5 + 0.25 * weightFactor; // (1) compute box distributions based on box-fill and remap the distrubtion using rank and user-weight-factor double[] boxProbs = new double[numBoxes()]; double sum = 0; for (int i = 0; i < boxProbs.length; i++) { boxProbs[i] = (getBox(i).size() / (double) allCards.size()) * (weightFactor * (numBoxes() - i)); sum += boxProbs[i]; } // normalize distribution for (int i = 0; i < boxProbs.length; i++) { boxProbs[i] /= sum; } // (2) created randomized box index based on the mapped distribution double r = Utils.getRandGen().nextDouble(), pdfValue = boxProbs[0]; int randBoxIndex = 0; while (pdfValue < r && pdfValue < 0.9999) { randBoxIndex++; pdfValue += boxProbs[randBoxIndex]; } // (2) now select card in selected box List<LeitnerItem> box = getBox(randBoxIndex); int nextCardIndex = Utils.getRandGen().nextInt(box.size()); return box.get(nextCardIndex); } } /** * Returns all cards which are part of this <code>LeitnerSystem</code> */ public List<Item> getAllCards() { return getAllCards(0, boxes.size() - 1); } /** * Returns all cards which are not in the last box. */ public List<Item> getNotLastBoxCards() { return getAllCards(0, boxes.size() - 2); } /** * Selects all cards in a defined range of boxes. * * @param minBox collect cards starting with box (inclusive) * @param maxBox stop collecting cards at this box (inclusive) */ public List<Item> getAllCards(int minBox, int maxBox) { assert minBox > 0 && maxBox <= boxes.size() : "invalid collecting range"; List<Item> allCards = new ArrayList<Item>(); for (int i = minBox; i <= maxBox; i++) { allCards.addAll(boxes.get(i)); } return allCards; } /** * Adds a new listener. */ public void addLearnSessionChangeListener(LeitnerListener l) { if (l == null) return; getLeitnerListeners().add(l); } /** * Removes a listener. */ public void removeLearnSessionChangeListener(LeitnerListener l) { if (l == null) return; getLeitnerListeners().remove(l); } synchronized List<LeitnerListener> getLeitnerListeners() { if (leitnerListeners == null) leitnerListeners = new ArrayList<LeitnerListener>(); return leitnerListeners; } /** * Returns the number of boxes of this leitner system instance. */ public int numBoxes() { return boxes.size(); } public List<LeitnerItem> getBox(int boxIndex) { assert boxIndex >= 0 && boxIndex < boxes.size(); return boxes.get(boxIndex); } /** * If the new NumBoxes is smaller as the current one move all cards to the last box. */ public void reconfigure(int newNumBoxes, int newInitBox) { while (numBoxes() > newNumBoxes) { while (!getBox(newNumBoxes).isEmpty()) { moveCard(getBox(newNumBoxes).get(0), newNumBoxes - 1); } assert getBox(newNumBoxes).isEmpty(); boxes.remove(newNumBoxes); } while (numBoxes() < newNumBoxes) { boxes.add(new ArrayList<LeitnerItem>()); } initBox = newInitBox - 1; } public String toString() { return "LeitnerSystem: numBoxes=" + numBoxes() + " initalBox=" + initBox + " curSize=" + getAllCards().size(); } }