/****************************************************************************************
* Copyright (c) 2009 Daniel Svärd <daniel.svard@gmail.com> *
* *
* 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 3 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, see <http://www.gnu.org/licenses/>. *
****************************************************************************************/
package com.ichi2.anki.model;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ichi2.anki.Utils;
/**
* A card is a presentation of a fact, and has two sides: a question and an answer. Any number of fields can appear on
* each side. When you add a fact to Anki, cards which show that fact are generated. Some models generate one card,
* others generate more than one.
*
* @see http://ichi2.net/anki/wiki/KeyTermsAndConcepts#Cards
*/
public class Card {
// TODO: Javadoc.
public static Logger log = LoggerFactory.getLogger(Image.class);
/** Card types. */
public static final int TYPE_FAILED = 0;
public static final int TYPE_REV = 1;
public static final int TYPE_NEW = 2;
/** Card states. */
public static final String STATE_NEW = "new";
public static final String STATE_YOUNG = "young";
public static final String STATE_MATURE = "mature";
/** Auto priorities. */
public static final int PRIORITY_NONE = 0;
public static final int PRIORITY_LOW = 1;
public static final int PRIORITY_NORMAL = 2;
public static final int PRIORITY_MEDIUM = 3;
public static final int PRIORITY_HIGH = 4;
/** Manual priorities. */
public static final int PRIORITY_REVIEW_EARLY = -1;
public static final int PRIORITY_BURIED = -2;
public static final int PRIORITY_SUSPENDED = -3;
/** Ease. */
public static final int EASE_NONE = 0;
public static final int EASE_FAILED = 1;
public static final int EASE_HARD = 2;
public static final int EASE_MID = 3;
public static final int EASE_EASY = 4;
/** Tags src constants. */
public static final int TAGS_FACT = 0;
public static final int TAGS_MODEL = 1;
public static final int TAGS_TEMPL = 2;
private static final int LEARNT_THRESHOLD = 7;
public static final int MATURE_THRESHOLD = 21;
private static final double MAX_TIMER = 60.0;
// BEGIN SQL table entries
private long mId; // Primary key
private long mFactId; // Foreign key facts.id
private long mCardModelId; // Foreign key cardModels.id
private double mCreated = Utils.now();
private double mModified = Utils.now();
private String mTags = "";
private int mOrdinal;
// Cached - changed on fact update
private String mQuestion = "";
private String mAnswer = "";
private int mPriority = PRIORITY_NORMAL;
private double mInterval = 0;
private double mLastInterval = 0;
private double mDue = Utils.now();
private double mLastDue = 0;
private double mFactor = Deck.INITIAL_FACTOR;
private double mLastFactor = Deck.INITIAL_FACTOR;
private double mFirstAnswered = 0;
// Stats
private int mReps = 0;
private int mSuccessive = 0;
private double mAverageTime = 0;
private double mReviewTime = 0;
private int mYoungEase0 = 0;
private int mYoungEase1 = 0;
private int mYoungEase2 = 0;
private int mYoungEase3 = 0;
private int mYoungEase4 = 0;
private int mMatureEase0 = 0;
private int mMatureEase1 = 0;
private int mMatureEase2 = 0;
private int mMatureEase3 = 0;
private int mMatureEase4 = 0;
// This duplicates the above data, because there's no way to map imported
// data to the above
private int mYesCount = 0;
private int mNoCount = 0;
private double mSpaceUntil = 0; // obsolete in libanki 1.1.4
// relativeDelay is reused as type without scheduling (ie, it remains 0-2 even if card is suspended, etc)
private double mRelativeDelay = 0;
private int mIsDue = 0; // obsolete in libanki 1.1
private int mType = TYPE_NEW;
private double mCombinedDue = 0;
// END SQL table entries
public Deck mDeck;
// BEGIN JOINed variables
private CardModel mCardModel;
private Fact mFact;
private String[] mTagsBySrc;
// END JOINed variables
private double mTimerStarted;
private double mTimerStopped;
private double mFuzz = 0;
// Leech flags, not read from database, only set to true during the actual suspension
private boolean isLeechMarked;
private boolean isLeechSuspended;
public Card(Deck deck, Fact fact, CardModel cardModel, double created) {
mTags = "";
mTagsBySrc = new String[TAGS_TEMPL + 1];
mTagsBySrc[TAGS_FACT] = "";
mTagsBySrc[TAGS_MODEL] = "";
mTagsBySrc[TAGS_TEMPL] = "";
mId = Utils.genID();
// New cards start as new & due
mType = TYPE_NEW;
mRelativeDelay = mType;
mTimerStarted = Double.NaN;
mTimerStopped = Double.NaN;
mModified = Utils.now();
if (Double.isNaN(created)) {
mCreated = created;
mDue = created;
} else {
mDue = mModified;
}
isLeechSuspended = false;
mCombinedDue = mDue;
mDeck = deck;
mFact = fact;
if (fact != null) {
mFactId = fact.getId();
}
mCardModel = cardModel;
if (cardModel != null) {
mCardModelId = cardModel.getId();
mOrdinal = cardModel.getOrdinal();
}
}
/**
* Format qa
*/
public void rebuildQA(Deck deck) {
rebuildQA(deck, true);
}
public void rebuildQA(Deck deck, boolean media) {
// Format qa
if (mFact != null && mCardModel != null) {
HashMap<String, String> qa = CardModel.formatQA(mFact, mCardModel, splitTags());
if (media) {
// Find old media references
HashMap<String, Integer> files = new HashMap<String, Integer>();
ArrayList<String> filesFromQA = Media.mediaFiles(mQuestion);
filesFromQA.addAll(Media.mediaFiles(mAnswer));
for (String f : filesFromQA) {
if (files.containsKey(f)) {
files.put(f, files.get(f) - 1);
} else {
files.put(f, -1);
}
}
// Update q/a
mQuestion = qa.get("question");
mAnswer = qa.get("answer");
// Determine media delta
filesFromQA = Media.mediaFiles(mQuestion);
filesFromQA.addAll(Media.mediaFiles(mAnswer));
for (String f : filesFromQA) {
if (files.containsKey(f)) {
files.put(f, files.get(f) + 1);
} else {
files.put(f, 1);
}
}
// Update media counts if we're attached to deck
for (Entry<String, Integer> entry : files.entrySet()) {
Media.updateMediaCount(deck, entry.getKey(), entry.getValue());
}
} else {
// Update q/a
mQuestion = qa.get("question");
mAnswer = qa.get("answer");
}
setModified();
}
}
public Card(Deck deck) {
this(deck, null, null, Double.NaN);
}
public Fact getFact() {
if (mFact == null) {
mFact = new Fact(mDeck, mFactId);
}
return mFact;
}
public void setModified() {
mModified = Utils.now();
}
public void setTimerStart(double time) {
mTimerStarted = time;
}
public void startTimer() {
mTimerStarted = Utils.now();
}
public void stopTimer() {
mTimerStopped = Utils.now();
}
public void resumeTimer() {
if (!Double.isNaN(mTimerStarted) && !Double.isNaN(mTimerStopped)) {
mTimerStarted += Utils.now() - mTimerStopped;
mTimerStopped = Double.NaN;
} else {
log.info("Card Timer: nothing to resume");
}
}
public double thinkingTime() {
if (Double.isNaN(mTimerStopped)) {
return (Utils.now() - mTimerStarted);
} else {
return (mTimerStopped - mTimerStarted);
}
}
public double totalTime() {
return (Utils.now() - mTimerStarted);
}
public double getFuzz() {
if (mFuzz == 0) {
genFuzz();
}
return mFuzz;
}
public void genFuzz() {
// Random rand = new Random();
// mFuzz = 0.95 + (0.1 * rand.nextDouble());
mFuzz = (double) Math.random();
}
// XXX Unused
// public String htmlQuestion(String type, boolean align) {
// return null;
// }
//
//
// public String htmlAnswer(boolean align) {
// return htmlQuestion("answer", align);
// }
public void updateStats(int ease, String state) {
char[] newState = state.toCharArray();
mReps += 1;
if (ease > EASE_FAILED) {
mSuccessive += 1;
} else {
mSuccessive = 0;
}
double delay = Math.min(totalTime(), MAX_TIMER);
// Ignore any times over 60 seconds
mReviewTime += delay;
if (mAverageTime != 0) {
mAverageTime = (mAverageTime + delay) / 2.0;
} else {
mAverageTime = delay;
}
// We don't track first answer for cards
if (STATE_NEW.equalsIgnoreCase(state)) {
newState = STATE_YOUNG.toCharArray();
}
// Update ease and yes/no count
// We want attr to be of the form mYoungEase3
newState[0] = Character.toUpperCase(newState[0]);
String attr = "m" + String.valueOf(newState) + String.format("Ease%d", ease);
try {
java.lang.reflect.Field f = this.getClass().getDeclaredField(attr);
f.setInt(this, f.getInt(this) + 1);
} catch (Exception e) {
log.error("Failed to update " + attr + " : " + e.getMessage());
}
if (ease < EASE_HARD) {
mNoCount += 1;
} else {
mYesCount += 1;
}
if (mFirstAnswered == 0) {
mFirstAnswered = Utils.now();
}
setModified();
}
public void updateFactor(int ease, double averageFactor) {
mLastFactor = mFactor;
if (isNew()) {
mFactor = averageFactor; // card is new, inherit beginning factor
}
if (isRev() && !isBeingLearnt()) {
if (ease == EASE_FAILED) {
mFactor -= 0.20;
} else if (ease == EASE_HARD) {
mFactor -= 0.15;
}
}
if (ease == EASE_EASY) {
mFactor += 0.10;
}
mFactor = Math.max(Deck.FACTOR_FOUR, mFactor);
}
public double adjustedDelay(int ease) {
double now = Utils.now();
if (isNew()) {
return 0;
}
if (mCombinedDue <= now) {
return (now -mDue) / 86400.0;
} else {
return (now - mCombinedDue) / 86400.0;
}
}
/**
* Suspend this card.
*/
public void suspend() {
long[] ids = new long[1];
ids[0] = mId;
mDeck.suspendCards(ids);
mDeck.reset();
}
/**
* Unsuspend this card.
*/
public void unsuspend() {
long[] ids = new long[1];
ids[0] = mId;
mDeck.unsuspendCards(ids);
}
public boolean getSuspendedState() {
return mDeck.getSuspendedState(mId);
}
/**
* Delete this card.
*/
public void delete() {
List<String> ids = new ArrayList<String>();
ids.add(Long.toString(mId));
mDeck.deleteCards(ids);
}
public String getState() {
if (isNew()) {
return STATE_NEW;
} else if (mInterval > MATURE_THRESHOLD) {
return STATE_MATURE;
}
return STATE_YOUNG;
}
/**
* Check if a card is a new card.
* @return True if a card has never been seen before.
*/
public boolean isNew() {
return mReps == 0;
}
/**
* Check if this is a revision of a successfully answered card.
* @return True if the card was successfully answered last time.
*/
public boolean isRev() {
return mSuccessive != 0;
}
/**
* Check if a card is being learnt.
* @return True if card should use present intervals.
*/
public boolean isBeingLearnt() {
return mLastInterval < LEARNT_THRESHOLD;
}
public String[] splitTags() {
String[] tags = new String[]{
getFact().getTags(),
Model.getModel(mDeck, getFact().getModelId(), true).getTags(),
getCardModel().getName()
};
return tags;
}
private String allTags() {
// Non-Canonified string of fact and model tags
if ((mTagsBySrc[TAGS_FACT].length() > 0) && (mTagsBySrc[TAGS_MODEL].length() > 0)) {
return mTagsBySrc[TAGS_FACT] + "," + mTagsBySrc[TAGS_MODEL];
} else if (mTagsBySrc[TAGS_FACT].length() > 0) {
return mTagsBySrc[TAGS_FACT];
} else {
return mTagsBySrc[TAGS_MODEL];
}
}
public boolean hasTag(String tag) {
return (allTags().indexOf(tag) != -1);
}
public boolean isMarked() throws SQLException {
int markedId = mDeck.getMarketTagId();
if (markedId == -1) {
return false;
} else {
return (mDeck.getDB().queryScalar("SELECT count(*) FROM cardTags WHERE cardId = " + mId + " AND tagId = " + markedId + " LIMIT 1") != 0);
}
}
// FIXME: Should be removed. Calling code should directly interact with Model
public CardModel getCardModel() {
Model myModel = Model.getModel(mDeck, mCardModelId, false);
return myModel.getCardModel(mCardModelId);
}
// Loading tags for this card. Needed when:
// - we modify the card fields and need to update question and answer.
// - we check is a card is marked
public void loadTags() {
ResultSet result = null;
int tagSrc = 0;
// Flush tags
for (int i = 0; i < mTagsBySrc.length; i++) {
mTagsBySrc[i] = "";
}
try {
result = mDeck.getDB().rawQuery(
"SELECT tags.tag, cardTags.src "
+ "FROM cardTags JOIN tags ON cardTags.tagId = tags.id " + "WHERE cardTags.cardId = " + mId
+ " AND cardTags.src in (" + TAGS_FACT + ", " + TAGS_MODEL + "," + TAGS_TEMPL + ") "
+ "ORDER BY cardTags.id");
while (result.next()) {
tagSrc = result.getInt(1);
if (mTagsBySrc[tagSrc].length() > 0) {
mTagsBySrc[tagSrc] += "," + result.getString(1);
} else {
mTagsBySrc[tagSrc] += result.getString(1);
}
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (result != null) {
try {
result.close();
} catch (SQLException e) {
}
}
}
}
public void resetCard() {
log.info("Reset Card: " + mId);
mModified = Utils.now();
mPriority = PRIORITY_NORMAL;
mInterval = 0;
mLastInterval = 0;
mDue = Utils.now();
mLastDue = 0;
mFactor = Deck.INITIAL_FACTOR;
mLastFactor = Deck.INITIAL_FACTOR;
mFirstAnswered = 0;
mReps = 0;
mSuccessive = 0;
mAverageTime = 0;
mReviewTime = 0;
mYoungEase0 = 0;
mYoungEase1 = 0;
mYoungEase2 = 0;
mYoungEase3 = 0;
mYoungEase4 = 0;
mMatureEase0 = 0;
mMatureEase1 = 0;
mMatureEase2 = 0;
mMatureEase3 = 0;
mMatureEase4 = 0;
mYesCount = 0;
mNoCount = 0;
mRelativeDelay = 0;
mType = TYPE_NEW;
mCombinedDue = 0;
toDB();
}
public boolean fromDB(long id) {
ResultSet result = null;
try {
result = mDeck.getDB().rawQuery(
"SELECT id, factId, cardModelId, created, modified, tags, "
+ "ordinal, question, answer, priority, interval, lastInterval, "
+ "due, lastDue, factor, lastFactor, firstAnswered, reps, "
+ "successive, averageTime, reviewTime, youngEase0, youngEase1, "
+ "youngEase2, youngEase3, youngEase4, matureEase0, matureEase1, "
+ "matureEase2, matureEase3, matureEase4, yesCount, noCount, "
+ "spaceUntil, isDue, type, combinedDue, relativeDelay " + "FROM cards " + "WHERE id = " + id);
if (!result.next()) {
log.warn("Card.java (fromDB(id)): No result from query.");
return false;
}
int i = 1;
mId = result.getLong(i++);
mFactId = result.getLong(i++);
mCardModelId = result.getLong(i++);
mCreated = result.getDouble(i++);
mModified = result.getDouble(i++);
mTags = result.getString(i++);
mOrdinal = result.getInt(i++);
mQuestion = result.getString(i++);
mAnswer = result.getString(i++);
mPriority = result.getInt(i++);
mInterval = result.getDouble(i++);
mLastInterval = result.getDouble(i++);
mDue = result.getDouble(i++);
mLastDue = result.getDouble(i++);
mFactor = result.getDouble(i++);
mLastFactor = result.getDouble(i++);
mFirstAnswered = result.getDouble(i++);
mReps = result.getInt(i++);
mSuccessive = result.getInt(i++);
mAverageTime = result.getDouble(i++);
mReviewTime = result.getDouble(i++);
mYoungEase0 = result.getInt(i++);
mYoungEase1 = result.getInt(i++);
mYoungEase2 = result.getInt(i++);
mYoungEase3 = result.getInt(i++);
mYoungEase4 = result.getInt(i++);
mMatureEase0 = result.getInt(i++);
mMatureEase1 = result.getInt(i++);
mMatureEase2 = result.getInt(i++);
mMatureEase3 = result.getInt(i++);
mMatureEase4 = result.getInt(i++);
mYesCount = result.getInt(i++);
mNoCount = result.getInt(i++);
mSpaceUntil = result.getDouble(i++);
mIsDue = result.getInt(i++);
mType = result.getInt(i++);
mCombinedDue = result.getDouble(i++);
mRelativeDelay = result.getDouble(i++);
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (result != null) {
try {
result.close();
} catch (SQLException e) {
}
}
}
// TODO: Should also read JOINed entries CardModel and Fact.
return true;
}
// TODO: Remove Redundancies
// I did a separated method because I don't want to interfere with other code while fact adding is not tested.
public void addToDb(){
if (isNew()) {
mType = TYPE_NEW;
} else if (isRev()) {
mType = TYPE_REV;
} else {
mType = TYPE_FAILED;
}
Map<String, Object> values = new HashMap<String, Object>();
values.put("id", mId);
values.put("factId", mFactId);
values.put("cardModelId", mCardModelId);
values.put("created", mCreated);
values.put("modified", mModified);
values.put("tags", mTags);
values.put("ordinal", mOrdinal);
values.put("question", mQuestion);
values.put("answer", mAnswer);
values.put("priority", mPriority);
values.put("interval", mInterval);
values.put("lastInterval", mLastInterval);
values.put("due", mDue);
values.put("lastDue", mLastDue);
values.put("factor", mFactor);
values.put("lastFactor", mLastFactor);
values.put("firstAnswered", mFirstAnswered);
values.put("reps", mReps);
values.put("successive", mSuccessive);
values.put("averageTime", mAverageTime);
values.put("reviewTime", mReviewTime);
values.put("youngEase0", mYoungEase0);
values.put("youngEase1", mYoungEase1);
values.put("youngEase2", mYoungEase2);
values.put("youngEase3", mYoungEase3);
values.put("youngEase4", mYoungEase4);
values.put("matureEase0", mMatureEase0);
values.put("matureEase1", mMatureEase1);
values.put("matureEase2", mMatureEase2);
values.put("matureEase3", mMatureEase3);
values.put("matureEase4", mMatureEase4);
values.put("yesCount", mYesCount);
values.put("noCount", mNoCount);
values.put("spaceUntil", mSpaceUntil);
values.put("isDue", mIsDue);
values.put("type", mType);
values.put("combinedDue", Math.max(mSpaceUntil, mDue));
values.put("relativeDelay", 0.0);
mDeck.getDB().insert(mDeck, "cards", null, values);
}
public void toDB() {
Map<String, Object> values = new HashMap<String, Object>();
values.put("factId", mFactId);
values.put("cardModelId", mCardModelId);
values.put("created", mCreated);
values.put("modified", mModified);
values.put("tags", mTags);
values.put("ordinal", mOrdinal);
values.put("question", mQuestion);
values.put("answer", mAnswer);
values.put("priority", mPriority);
values.put("interval", mInterval);
values.put("lastInterval", mLastInterval);
values.put("due", mDue);
values.put("lastDue", mLastDue);
values.put("factor", mFactor);
values.put("lastFactor", mLastFactor);
values.put("firstAnswered", mFirstAnswered);
values.put("reps", mReps);
values.put("successive", mSuccessive);
values.put("averageTime", mAverageTime);
values.put("reviewTime", mReviewTime);
values.put("youngEase0", mYoungEase0);
values.put("youngEase1", mYoungEase1);
values.put("youngEase2", mYoungEase2);
values.put("youngEase3", mYoungEase3);
values.put("youngEase4", mYoungEase4);
values.put("matureEase0", mMatureEase0);
values.put("matureEase1", mMatureEase1);
values.put("matureEase2", mMatureEase2);
values.put("matureEase3", mMatureEase3);
values.put("matureEase4", mMatureEase4);
values.put("yesCount", mYesCount);
values.put("noCount", mNoCount);
values.put("spaceUntil", mSpaceUntil);
values.put("isDue", 0);
values.put("type", mType);
values.put("combinedDue", mCombinedDue);
values.put("relativeDelay", mRelativeDelay);
mDeck.getDB().update(mDeck, "cards", values, "id = " + mId, true);
// TODO: Should also write JOINED entries: CardModel and Fact.
}
/**
* Commit question and answer fields to database.
*/
public void updateQAfields() {
setModified();
Map<String, Object> values = new HashMap<String, Object>();
values.put("modified", mModified);
values.put("question", mQuestion);
values.put("answer", mAnswer);
mDeck.getDB().update(mDeck, "cards", values, "id = " + mId);
}
public Map<String, Object> getAnswerValues() {
Map<String, Object> values = new HashMap<String, Object>();
values.put("modified", mModified);
values.put("priority", mPriority);
values.put("interval", mInterval);
values.put("lastInterval", mLastInterval);
values.put("due", mDue);
values.put("lastDue", mLastDue);
values.put("factor", mFactor);
values.put("lastFactor", mLastFactor);
values.put("firstAnswered", mFirstAnswered);
values.put("reps", mReps);
values.put("successive", mSuccessive);
values.put("averageTime", mAverageTime);
values.put("reviewTime", mReviewTime);
values.put("youngEase0", mYoungEase0);
values.put("youngEase1", mYoungEase1);
values.put("youngEase2", mYoungEase2);
values.put("youngEase3", mYoungEase3);
values.put("youngEase4", mYoungEase4);
values.put("matureEase0", mMatureEase0);
values.put("matureEase1", mMatureEase1);
values.put("matureEase2", mMatureEase2);
values.put("matureEase3", mMatureEase3);
values.put("matureEase4", mMatureEase4);
values.put("yesCount", mYesCount);
values.put("noCount", mNoCount);
values.put("type", mType);
values.put("combinedDue", mCombinedDue);
values.put("relativeDelay", mRelativeDelay);
return values;
}
public long getId() {
return mId;
}
public void setLastInterval(double lastInterval) {
mLastInterval = lastInterval;
}
public double getLastInterval() {
return mLastInterval;
}
public void setInterval(double interval) {
mInterval = interval;
}
public double getInterval() {
return mInterval;
}
public void setLastFactor(double lastFactor) {
mLastFactor = lastFactor;
}
public double getLastFactor() {
return mLastFactor;
}
public double getFactor() {
return mFactor;
}
public int getReps() {
return mReps;
}
public int getYesCount() {
return mYesCount;
}
public int getNoCount() {
return mNoCount;
}
public void setQuestion(String question) {
mQuestion = question;
}
public String getQuestion() {
return mQuestion;
}
public void setAnswer(String answer) {
mAnswer = answer;
}
public String getAnswer() {
return mAnswer;
}
public void setModified(double modified) {
mModified = modified;
}
public void setCombinedDue(double combinedDue) {
mCombinedDue = combinedDue;
}
public double getCombinedDue() {
return mCombinedDue;
}
public void setLastDue(double lastDue) {
mLastDue = lastDue;
}
public void setDue(double due) {
mDue = due;
}
public double getDue() {
return mDue;
}
public void setIsDue(int isDue) {
mIsDue = isDue;
}
/**
* Check whether the card is due.
* @return True if the card is due, false otherwise
*/
public boolean isDue() {
return (mIsDue == 1);
}
public long getFactId() {
return mFactId;
}
public void setSpaceUntil(double spaceUntil) {
mSpaceUntil = spaceUntil;
}
public void setRelativeDelay(double relativeDelay) {
mRelativeDelay = relativeDelay;
}
public void setPriority(int priority) {
mPriority = priority;
}
public int getPriority() {
return mPriority;
}
public int getType() {
return mType;
}
public void setType(int type) {
mType = type;
}
public long getCardModelId() {
return mCardModelId;
}
public double nextInterval(Card card, int ease) {
return mDeck.nextInterval(card, ease);
}
// Leech flag
public boolean getLeechFlag() {
return isLeechMarked;
}
public void setLeechFlag(boolean flag) {
isLeechMarked = flag;
}
// Suspended flag
public boolean getSuspendedFlag() {
return isLeechSuspended;
}
public void setSuspendedFlag(boolean flag) {
isLeechSuspended = flag;
}
public int getSuccessive() {
return mSuccessive;
}
/**
* The cardModel defines a field typeAnswer. If it is empty, then no answer should be typed.
* Otherwise a typed answer should be compared to the value of field related to a cards fact.
* A field is found based on the factId in the card and the fieldModelId.
* The fieldModel's id is found by searching with the typeAnswer name and cardModel's modelId
*
* @return 2 dimensional array with answer value at index=0 and fieldModel's class at index=1
* null if typeAnswer is empty (i.e. do not prompt for answer). Otherwise a string (which can be empty) from the actual field value.
* The fieldModel's id is correctly hexafied and formatted for class attribute of span for formatting
*/
public String[] getComparedFieldAnswer() {
String[] returnArray = new String[2];
CardModel myCardModel = this.getCardModel();
String typeAnswer = myCardModel.getTypeAnswer();
// Check if we have a valid field to use as the answer to type.
if (null == typeAnswer || 0 == typeAnswer.trim().length()) {
// no field specified, compare with whole answer
returnArray[0] = mAnswer;
returnArray[1] = "";
return returnArray;
}
Model myModel = Model.getModel(mDeck, myCardModel.getModelId(), true);
TreeMap<Long, FieldModel> fieldModels = myModel.getFieldModels();
FieldModel myFieldModel = null;
long myFieldModelId = 0l;
for (Entry<Long, FieldModel> entry : fieldModels.entrySet()) {
myFieldModel = entry.getValue();
myFieldModelId = myFieldModel.match(myCardModel.getModelId(), typeAnswer);
if (myFieldModelId != 0l) {
break;
}
}
// Just in case we do not find the matching field model.
if (myFieldModelId == 0) {
log.error("could not find field model for type answer: " + typeAnswer);
returnArray[0] = null;
return null;
}
returnArray[0] = Field.fieldValuefromDb(this.mDeck, this.mFactId, myFieldModelId);
returnArray[1] = "fm" + Long.toHexString(myFieldModelId);
return returnArray;
}
}