/*
* jMemorize - Learning made easy (and fun) - A Leitner flashcards tool
* Copyright(C) 2004-2008 Riad Djemili and contributors
*
* 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 1, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package jmemorize.core;
import java.util.Date;
import java.util.List;
import jmemorize.core.CardSide.CardSideObserver;
/**
* A flash card that has a front/flip side and can be learned.
*
* @author djemili
* @version $Id: Card.java 1048 2008-01-21 21:40:00Z djemili $
*/
public class Card implements Events, Cloneable
{
public static final long ONE_DAY = 1000 * 60 * 60 * 24;
public static final boolean CLONE_DATES = Main.isDevel();
private Category m_category;
private int m_level;
// content
private CardSide m_frontSide = new CardSide();
private CardSide m_backSide = new CardSide();
// dates
private Date m_dateTested;
private Date m_dateExpired;
private Date m_dateCreated;
private Date m_dateModified;
private Date m_dateTouched; //this date is used internaly to order cards
// stats
private int m_testsTotal;
private int m_testsHit; //succesfull learn repetitions
private int m_frontHitsCorrect;
private int m_backHitsCorrect;
/**
* Assumes formatted front- and backsides
*/
public Card(String front, String back)
{
this(FormattedText.formatted(front), FormattedText.formatted(back));
}
public Card(FormattedText front, FormattedText back)
{
this(new Date(), front, back);
}
/**
* The card sides are given in a formatted state.
*/
public Card(Date created, String front, String back)
{
this(created, FormattedText.formatted(front), FormattedText.formatted(back));
}
public Card(Date created, FormattedText front, FormattedText back)
{
this(created, new CardSide(front), new CardSide(back));
}
public Card(Date created, CardSide frontSide, CardSide backSide)
{
m_dateCreated = cloneDate(created);
m_dateModified = cloneDate(created);
m_dateTouched = cloneDate(created);
m_frontSide = frontSide;
m_backSide = backSide;
attachCardSideObservers();
}
/**
* The given card sides are assumend to be unformatted.
*
* @throws IllegalArgumentException If frontSide or backSide has no text.
*/
public void setSides(String front, String back)
{
FormattedText frontSide = FormattedText.unformatted(front);
FormattedText backSide = FormattedText.unformatted(back);
setSides(frontSide, backSide);
}
/**
* @throws IllegalArgumentException If front or back has no text.
*/
public void setSides(FormattedText front, FormattedText back)
throws IllegalArgumentException
{
if (front.equals(m_frontSide.getText()) &&
back.equals(m_backSide.getText()))
{
return;
}
m_frontSide.setText(front);
m_backSide.setText(back);
if (m_category != null)
{
m_dateModified = new Date();
m_category.fireCardEvent(EDITED_EVENT, this, getCategory(), m_level);
}
}
/**
* Get the number of times a specific card side was already learned in its
* deck.
*
* @param frontside <code>true</code> if it should deliver the fronside
* value, <code>false</code> if it should deliver the backside value.
*
* @return the amount of times that the specified side was learned in this
* deck.
*/
public int getLearnedAmount(boolean frontside)
{
// TODO move to CardSide class
return frontside ? m_frontHitsCorrect : m_backHitsCorrect;
}
/**
* Set the number of times a specific card side was already learned in its
* deck.
*
* @param frontside <code>true</code> if it should deliver the fronside
* value, <code>false</code> if it should deliver the backside value.
*
* @param amount the amount of times that the specified side was learned in
* this deck.
*/
public void setLearnedAmount(boolean frontside, int amount)
{
// TODO move to CardSide class
if (frontside)
{
m_frontHitsCorrect = amount;
}
else
{
m_backHitsCorrect = amount;
}
if (m_category != null)
{
m_category.fireCardEvent(DECK_EVENT, this, getCategory(), m_level);
}
}
/**
* Increment the number of times a specific card side was already learned in
* its deck by one.
*
* @param frontside <code>true</code> if it should deliver the fronside
* value, <code>false</code> if it should deliver the backside value.
*/
public void incrementLearnedAmount(boolean frontside)
{
// TODO move to CardSide class
setLearnedAmount(frontside, getLearnedAmount(frontside) + 1);
}
/**
* Resets the amount of times that the card sides were learned in this deck
* to 0.
*/
public void resetLearnedAmount()
{
setLearnedAmount(true, 0);
setLearnedAmount(false, 0);
}
public CardSide getFrontSide()
{
return m_frontSide;
}
public CardSide getBackSide()
{
return m_backSide;
}
/**
* @return the date that this card appeared the last time in a test and was
* either passed or failed (skip doesn't count).
*/
public Date getDateTested()
{
return cloneDate(m_dateTested);
}
public void setDateTested(Date date)
{
m_dateTested = cloneDate(date);
m_dateTouched = cloneDate(date);
}
/**
* @return can be <code>null</code>.
*/
public Date getDateExpired()
{
return cloneDate(m_dateExpired);
}
/**
* @param date can be <code>null</code>.
*/
public void setDateExpired(Date date) // CHECK should this throw a event?
{
m_dateExpired = cloneDate(date);
}
/**
* @return the creation date. Is never <code>null</code>.
*/
public Date getDateCreated()
{
return cloneDate(m_dateCreated);
}
public void setDateCreated(Date date)
{
if (date == null)
throw new NullPointerException();
m_dateCreated = cloneDate(date);
}
/**
* @return the modification date. Is never <code>null</code>.
*/
public Date getDateModified()
{
return m_dateModified;
}
/**
* @param date must be equal or after the creation date.
*/
public void setDateModified(Date date)
{
if (date.before(m_dateCreated))
throw new IllegalArgumentException(
"Modification date can't be before creation date.");
m_dateModified = date;
}
/**
* @return DateTouched is the date that this card was learned, skipped,
* reset or created the last time. This value is used to sort cards by a
* global value that is unique for all categories and decks.
*/
public Date getDateTouched()
{
return cloneDate(m_dateTouched);
}
public void setDateTouched(Date date)
{
m_dateTouched = cloneDate(date);
}
/**
* @return Number of times this card has been tested.
*/
public int getTestsTotal()
{
return m_testsTotal;
}
/**
* @return Number of times this card has been tested succesfully.
*/
public int getTestsPassed()
{
return m_testsHit;
}
/**
* @return The percentage of times that this card has passed learn tests in
* comparison to failed tests.
*/
public int getPassRatio()
{
return (int)Math.round(100.0 * m_testsHit / m_testsTotal);
}
public void incStats(int hit, int total)
{
m_testsTotal += total;
m_testsHit += hit;
}
public void resetStats()
{
m_testsTotal = 0;
m_testsHit = 0;
m_frontHitsCorrect = 0;
m_backHitsCorrect = 0;
}
public Category getCategory()
{
return m_category;
}
protected void setCategory(Category category)
{
m_category = category;
}
/**
* A card is expired when it was learned/repeated succesfully, but its learn
* time has expired (is in the past from current perspective).
*
* @return True if the card has expired.
*/
public boolean isExpired()
{
Date now = Main.getNow();
return m_dateExpired != null &&
(m_dateExpired.before(now) || m_dateExpired.equals(now));
}
/**
* A card is learned when it was learned/repeated succesfully and its learn
* time hasnt expired.
*
* @return True if the card is learned.
*/
public boolean isLearned()
{
return m_dateExpired != null && m_dateExpired.after(Main.getNow());
}
/**
* A card is unlearned when it wasnt succesfully repeated or never been l
* earned at all.
*
* @return True if the card is unlearned.
*/
public boolean isUnlearned()
{
return m_dateExpired == null;
}
/**
* @return Returns the level.
*/
public int getLevel()
{
return m_level;
}
/**
* @param level The level to set.
*/
protected void setLevel(int level)
{
m_level = level;
}
/* (non-Javadoc)
* @see java.lang.Object#clone()
*/
public Object clone()
{
Card card = null;
try
{
card = (Card)super.clone();
card.m_frontSide = (CardSide)m_frontSide.clone();
card.m_backSide = (CardSide)m_backSide.clone();
card.m_dateCreated = cloneDate(m_dateCreated);
card.m_dateExpired = cloneDate(m_dateExpired);
card.m_dateModified = cloneDate(m_dateModified);
card.m_dateTested = cloneDate(m_dateTested);
card.m_dateTouched = cloneDate(m_dateTouched);
card.m_category = null; // don't clone category
}
catch (CloneNotSupportedException e)
{
assert false;
}
return card;
}
/**
* Clones the card without copying its user-dependent progress stats.
*
* This includes the following data: Fronside, Flipside, Creation date.
* Setting the right category needs to be handled from the out side.
*/
public Card cloneWithoutProgress()
{
try
{
return new Card(m_dateCreated,
(CardSide)m_frontSide.clone(), (CardSide)m_backSide.clone());
}
catch (CloneNotSupportedException e)
{
assert false;
return null; // satisfy compiler
}
}
/**
* @see java.lang.Object#toString()
*/
public String toString()
{
return "("+m_frontSide+"/"+m_backSide+")";
}
private void attachCardSideObservers()
{
CardSideObserver observer = new CardSideObserver() {
public void onImagesChanged(CardSide cardSide, List<String> imageIDs)
{
if (m_category != null)
{
m_dateModified = new Date();
m_category.fireCardEvent(EDITED_EVENT, Card.this, getCategory(), m_level);
}
}
public void onTextChanged(CardSide cardSide, FormattedText text)
{
// already handled by set sides
// TODO handle event notfying here
}
};
m_frontSide.addObserver(observer);
m_backSide.addObserver(observer);
}
/**
* @return clone of given date or <code>null</code> if given date was
* <code>null</code>.
*/
private Date cloneDate(Date date)
{
if (CLONE_DATES)
{
return date == null ? null : (Date)date.clone();
}
return date;
}
}