/* * 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.learn; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Comparator; import java.util.Date; import java.util.LinkedList; import java.util.List; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import jmemorize.core.Main; import jmemorize.core.io.XmlBuilder; import jmemorize.gui.Localization; import org.w3c.dom.Document; /** * Stores the history of learn sessions and provides statistics. * * @author djemili */ public class LearnHistory { public class SessionSummary implements Cloneable { private final Date m_start; private final Date m_end; private final int m_duration; private final float m_passed; private final float m_failed; private final float m_skipped; private final float m_relearned; private SessionSummary(Date start, Date end, float passed, float failed, float skipped, float relearned) { this(start, end, (int)((end.getTime() - start.getTime()) / (1000*60)), passed, failed, skipped, relearned); } private SessionSummary(Date start) { this(start, start, 0.0f, 0.0f, 0.0f, 0.0f); } private SessionSummary(Date start, Date end, int duration, float passed, float failed, float skipped, float relearned) { m_start = start; m_end = end; m_duration = duration; m_passed = passed; m_failed = failed; m_skipped = skipped; m_relearned = relearned; } public Date getStart() { return (Date)m_start.clone(); } public Date getEnd() { return (Date)m_end.clone(); } public int getDuration() { return m_duration; } public float getPassed() { return m_passed; } public float getFailed() { return m_failed; } public float getSkipped() { return m_skipped; } public float getRelearned() { return m_relearned; } /* (non-Javadoc) * @see java.lang.Object#clone() */ public Object clone() throws CloneNotSupportedException { return super.clone(); } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ public boolean equals(Object obj) { if (!(obj instanceof SessionSummary)) { return false; } SessionSummary other = (SessionSummary)obj; return m_passed == other.m_passed && m_failed == other.m_failed && m_skipped == other.m_skipped && m_relearned == other.m_relearned; } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ public int hashCode() { return m_start.hashCode(); } /* (non-Javadoc) * @see java.lang.Object#toString() */ public String toString() { return "summary("+m_start+", "+m_passed+"/"+m_failed+")"; } } public abstract static class CalendarComparator implements Comparator<SessionSummary> { /* (non-Javadoc) * @see java.util.Comparator */ public int compare(SessionSummary s1, SessionSummary s2) { Calendar c1 = Calendar.getInstance(); c1.setTime(s1.getStart()); Calendar c2 = Calendar.getInstance(); c2.setTime(s2.getStart()); long v1 = toValue(c1); long v2 = toValue(c2); return v1 == v2 ? 0 : v1 > v2 ? 1 : -1; } public abstract long toValue(Calendar c); public abstract DateFormat getFormat(); public abstract boolean showRotated(); public abstract void decCalendarValue(Calendar c); } private static class SimpleComparator extends CalendarComparator { public long toValue(Calendar c) { return c.getTimeInMillis(); } public DateFormat getFormat() { return Localization.SHORT_DATE_FORMATER; } public boolean showRotated() { return true; } @Override public void decCalendarValue(Calendar c) { throw new UnsupportedOperationException(); } } private static class DateComparator extends CalendarComparator { public long toValue(Calendar c) { return c.get(Calendar.DAY_OF_YEAR)+ 1000 * c.get(Calendar.YEAR); } public DateFormat getFormat() { return DateFormat.getDateInstance(DateFormat.SHORT); } public boolean showRotated() { return true; } @Override public void decCalendarValue(Calendar c) { c.add(Calendar.DAY_OF_YEAR, -1); } } private static class WeekComparator extends CalendarComparator { public long toValue(Calendar c) { return c.get(Calendar.WEEK_OF_YEAR) + 1000 * c.get(Calendar.YEAR); } public DateFormat getFormat() { return new SimpleDateFormat("w/yyyy"); } public boolean showRotated() { return true; } @Override public void decCalendarValue(Calendar c) { c.add(Calendar.WEEK_OF_YEAR, -1); } } private static class MonthComparator extends CalendarComparator { public long toValue(Calendar c) { return c.get(Calendar.MONTH) + 1000 * c.get(Calendar.YEAR); } public DateFormat getFormat() { return new SimpleDateFormat("M/yyyy"); } public boolean showRotated() { return true; } @Override public void decCalendarValue(Calendar c) { c.add(Calendar.MONTH, -1); } } private static class YearComparator extends CalendarComparator { public long toValue(Calendar c) { return c.get(Calendar.YEAR); } public DateFormat getFormat() { return new SimpleDateFormat("yyyy"); } public boolean showRotated() { return false; } @Override public void decCalendarValue(Calendar c) { c.add(Calendar.YEAR, -1); } } public static final CalendarComparator SIMPLE_COMP = new SimpleComparator(); public static final CalendarComparator DATE_COMP = new DateComparator(); public static final CalendarComparator WEEK_COMP = new WeekComparator(); public static final CalendarComparator MONTH_COMP = new MonthComparator(); public static final CalendarComparator YEAR_COMP = new YearComparator(); // TODO enforce that m_summaries is always sorted in descending date order private List<SessionSummary> m_summaries = new ArrayList<SessionSummary>(); private File m_file; private boolean m_isLoaded; // false, if created from scratch public LearnHistory() { this(null); } public LearnHistory(File file) { try { m_file = file; if (m_file != null) load(m_file); } catch (Exception e) { Main.logThrowable("Could not load learn history.", e); } } public void addSummary(Date start, Date end, int passed, int failed, int skipped, int relearned) { SessionSummary sessionSummary = new SessionSummary( start, end, passed, failed, skipped, relearned); m_summaries.add(sessionSummary); } public void setIsLoaded(boolean loaded) { m_isLoaded = loaded; } public boolean isLoaded() { return m_isLoaded; } public SessionSummary getLastSummary() { if (m_summaries.size() == 0) return null; return (SessionSummary)m_summaries.get(m_summaries.size() - 1); } public List<SessionSummary> getSummaries() { return m_summaries; } public List<SessionSummary> getSummaries(int limit) { int n = Math.min(limit, m_summaries.size()); return m_summaries.subList(m_summaries.size() - n, m_summaries.size()); } public List<SessionSummary> getSummaries(CalendarComparator comp) { List<SessionSummary> list = new LinkedList<SessionSummary>(); SessionSummary lastSummary = null; SessionSummary aggregatedSummary = null; // TODO refactor and use getSummary(date, comp) for (SessionSummary summary : m_summaries) { if (lastSummary == null || comp.compare(summary, lastSummary) != 0) { if (aggregatedSummary != null) list.add(aggregatedSummary); try { aggregatedSummary = (SessionSummary)summary.clone(); } catch (CloneNotSupportedException e) { assert false; } } else { aggregatedSummary = new SessionSummary( aggregatedSummary.m_start, summary.m_end, aggregatedSummary.m_duration + summary.m_duration, aggregatedSummary.m_passed + summary.m_passed, aggregatedSummary.m_failed + summary.m_failed, aggregatedSummary.m_skipped + summary.m_skipped, aggregatedSummary.m_relearned + summary.m_relearned ); } lastSummary = summary; } if (aggregatedSummary != null) list.add(aggregatedSummary); return list; } public List<SessionSummary> getSummaries(CalendarComparator comp, int limit, boolean showEmpty) { if (showEmpty && comp != SIMPLE_COMP) { List<SessionSummary> summaries = new ArrayList<SessionSummary>(limit); Calendar c = Calendar.getInstance(); Date date = c.getTime(); int lastEntry = 0; for (int i=0; i<limit; i++) { SessionSummary summary = getSummary(date, comp); if (summary == null) summary = new SessionSummary(date); else lastEntry = i; summaries.add(0, summary); comp.decCalendarValue(c); date = c.getTime(); } int size = summaries.size(); lastEntry = Math.max(2, lastEntry); // always show at least 3 entries return summaries.subList(size - lastEntry - 1, size); } else { // TODO optimize this; remove version without limit argument List<SessionSummary> summaries = getSummaries(comp); int n = Math.min(limit, summaries.size()); return summaries.subList(summaries.size() - n, summaries.size()); } } public SessionSummary getAverage() { float count = m_summaries.size(); SessionSummary summary = getSessionsSummary(); if (count > 0) { return new SessionSummary(summary.getStart(), summary.getEnd(), (int)(summary.getDuration() / count), summary.getPassed()/count, summary.getFailed()/count, summary.getSkipped()/count, summary.getRelearned()/count); } else { return new SessionSummary(new Date(), new Date(), 0, 0, 0, 0); } } /** * @return a aggregated summary for given date and comparator. */ public SessionSummary getSummary(Date date, CalendarComparator comp) { Calendar c1 = Calendar.getInstance(); c1.setTime(date); Calendar c2 = Calendar.getInstance(); int duration = 0; int failed = 0; int passed = 0; int relearned = 0; int skipped = 0; boolean found = false; for (SessionSummary summary : m_summaries) { c2.setTime(summary.m_start); if (comp.toValue(c1) == comp.toValue(c2)) { duration += summary.m_duration; failed += summary.m_failed; passed += summary.m_passed; relearned += summary.m_relearned; skipped += summary.m_skipped; found = true; } } return !found ? null : new SessionSummary(date, date, duration, passed, failed, skipped, relearned); } public SessionSummary getSessionsSummary() { int duration = 0; float passed = 0; float failed = 0; float skipped = 0; float relearned = 0; for (SessionSummary summary : m_summaries) { duration += summary.getDuration(); passed += summary.getPassed(); failed += summary.getFailed(); skipped += summary.getSkipped(); relearned += summary.getRelearned(); } SessionSummary first = (SessionSummary)m_summaries.get(0); SessionSummary last = (SessionSummary)m_summaries.get(m_summaries.size() - 1); return new SessionSummary(first.getStart(), last.getEnd(), duration, passed, failed, skipped, relearned); } public void load(File file) throws Exception { if (!file.exists()) return; InputStream in = new FileInputStream(file); // get lesson tag try { Document doc = DocumentBuilderFactory.newInstance(). newDocumentBuilder().parse(in); XmlBuilder.loadLearnHistory(doc, this); } finally { if (in != null) { in.close(); } } } public void save(File file) throws Exception { OutputStream out = new FileOutputStream(file); try { Document document = DocumentBuilderFactory.newInstance() .newDocumentBuilder().newDocument(); XmlBuilder.writeLearnHistory(document, this); // transform document for file output Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ transformer.transform(new DOMSource(document), new StreamResult(out)); } finally { if (out != null) { out.close(); } } } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ public boolean equals(Object obj) { if (!(obj instanceof LearnHistory)) { return false; } LearnHistory other = (LearnHistory)obj; return m_summaries.equals(other.m_summaries); } /* (non-Javadoc) * @see java.lang.Object#hashCode() */ public int hashCode() { return m_summaries.hashCode(); } }