/**************************************************************************************** * 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.lang.reflect.Field; import java.sql.Date; import java.sql.ResultSet; import java.sql.SQLException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.ichi2.anki.Utils; /** * Deck statistics. */ public class Stats { private static Logger log = LoggerFactory.getLogger(Stats.class); public static final int STATS_LIFE = 0; public static final int STATS_DAY = 1; // BEGIN: SQL table columns private long mId; private int mType; private Date mDay; private int mReps; private double mAverageTime; private double mReviewTime; // Next two columns no longer used private double mDistractedTime; private int mDistractedReps; private int mNewEase0; private int mNewEase1; private int mNewEase2; private int mNewEase3; private int mNewEase4; private int mYoungEase0; private int mYoungEase1; private int mYoungEase2; private int mYoungEase3; private int mYoungEase4; private int mMatureEase0; private int mMatureEase1; private int mMatureEase2; private int mMatureEase3; private int mMatureEase4; // END: SQL table columns private Deck mDeck; public Stats(Deck deck) { mDeck = deck; mDay = null; mReps = 0; mAverageTime = 0; mReviewTime = 0; mDistractedTime = 0; mDistractedReps = 0; mNewEase0 = 0; mNewEase1 = 0; mNewEase2 = 0; mNewEase3 = 0; mNewEase4 = 0; mYoungEase0 = 0; mYoungEase1 = 0; mYoungEase2 = 0; mYoungEase3 = 0; mMatureEase0 = 0; mMatureEase1 = 0; mMatureEase2 = 0; mMatureEase3 = 0; mMatureEase4 = 0; } public void fromDB(long id) { ResultSet result = null; try { log.info("Reading stats from DB..."); result = mDeck.getDB().rawQuery( "SELECT * " + "FROM stats WHERE id = " + String.valueOf(id)); if (!result.next()) { return; } int i = 1; mId = result.getLong(i++); mType = result.getInt(i++); mDay = Date.valueOf(result.getString(i++)); mReps = result.getInt(i++); mAverageTime = result.getDouble(i++); mReviewTime = result.getDouble(i++); mDistractedTime = result.getDouble(i++); mDistractedReps = result.getInt(i++); mNewEase0 = result.getInt(i++); mNewEase1 = result.getInt(i++); mNewEase2 = result.getInt(i++); mNewEase3 = result.getInt(i++); mNewEase4 = result.getInt(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++); } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } } public void create(int type, Date day) { log.info("Creating new stats for " + day.toString() + "..."); mType = type; mDay = day; Map<String, Object> values = new HashMap<String, Object>(); values.put("type", type); values.put("day", day.toString()); values.put("reps", 0); values.put("averageTime", 0); values.put("reviewTime", 0); values.put("distractedTime", 0); values.put("distractedReps", 0); values.put("newEase0", 0); values.put("newEase1", 0); values.put("newEase2", 0); values.put("newEase3", 0); values.put("newEase4", 0); values.put("youngEase0", 0); values.put("youngEase1", 0); values.put("youngEase2", 0); values.put("youngEase3", 0); values.put("youngEase4", 0); values.put("matureEase0", 0); values.put("matureEase1", 0); values.put("matureEase2", 0); values.put("matureEase3", 0); values.put("matureEase4", 0); mId = mDeck.getDB().insert(mDeck, "stats", null, values); } public void toDB() { mDeck.getDB().update(mDeck, "stats", getValues(), "id = " + mId); } public void toDB(Map<String, Object> oldValues) { mDeck.getDB().update(mDeck, "stats", getValues(), "id = " + mId, true, (HashMap<String, Object>[]) new Object[] {oldValues}, new String[] {"id = " + mId}); } private Map<String, Object> getValues() { Map<String, Object> values = new HashMap<String, Object>(); values.put("type", mType); values.put("day", mDay.toString()); values.put("reps", mReps); values.put("averageTime", mAverageTime); values.put("reviewTime", mReviewTime); values.put("newEase0", mNewEase0); values.put("newEase1", mNewEase1); values.put("newEase2", mNewEase2); values.put("newEase3", mNewEase3); values.put("newEase4", mNewEase4); 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); return values; } public static void updateAllStats(Stats global, Stats daily, Card card, int ease, String oldState) { updateStats(global, card, ease, oldState); updateStats(daily, card, ease, oldState); } public static void updateStats(Stats stats, Card card, int ease, String oldState) { Map<String, Object> oldValues = new HashMap<String, Object>(); char[] newState = oldState.toCharArray(); stats.mReps += 1; double delay = card.totalTime(); if (delay >= 60) { stats.mReviewTime += 60; } else { stats.mReviewTime += delay; stats.mAverageTime = (stats.mReviewTime / stats.mReps); } // update eases // We want attr to be of the form mYoungEase3 newState[0] = Character.toUpperCase(newState[0]); StringBuilder attr = new StringBuilder(); attr.append("m").append(String.valueOf(newState)).append(String.format("Ease%d", ease)); try { Field f = stats.getClass().getDeclaredField(attr.toString()); f.setInt(stats, f.getInt(stats) + 1); } catch (Exception e) { log.error("Failed to update " + attr.toString() + " : " + e.getMessage()); } stats.toDB(oldValues); } public JSONObject bundleJson() { JSONObject bundledStat = new JSONObject(); try { bundledStat.put("type", mType); bundledStat.put("day", Utils.dateToOrdinal(mDay)); bundledStat.put("reps", mReps); bundledStat.put("averageTime", mAverageTime); bundledStat.put("reviewTime", mReviewTime); bundledStat.put("distractedTime", mDistractedTime); bundledStat.put("distractedReps", mDistractedReps); bundledStat.put("newEase0", mNewEase0); bundledStat.put("newEase1", mNewEase1); bundledStat.put("newEase2", mNewEase2); bundledStat.put("newEase3", mNewEase3); bundledStat.put("newEase4", mNewEase4); bundledStat.put("youngEase0", mYoungEase0); bundledStat.put("youngEase1", mYoungEase1); bundledStat.put("youngEase2", mYoungEase2); bundledStat.put("youngEase3", mYoungEase3); bundledStat.put("youngEase4", mYoungEase4); bundledStat.put("matureEase0", mMatureEase0); bundledStat.put("matureEase1", mMatureEase1); bundledStat.put("matureEase2", mMatureEase2); bundledStat.put("matureEase3", mMatureEase3); bundledStat.put("matureEase4", mMatureEase4); } catch (JSONException e) { log.info("JSONException = " + e.getMessage()); } return bundledStat; } public void updateFromJson(JSONObject remoteStat) { try { mAverageTime = remoteStat.getDouble("averageTime"); mDay = Utils.ordinalToDate(remoteStat.getInt("day")); mDistractedReps = remoteStat.getInt("distractedReps"); mDistractedTime = remoteStat.getDouble("distractedTime"); mMatureEase0 = remoteStat.getInt("matureEase0"); mMatureEase1 = remoteStat.getInt("matureEase1"); mMatureEase2 = remoteStat.getInt("matureEase2"); mMatureEase3 = remoteStat.getInt("matureEase3"); mMatureEase4 = remoteStat.getInt("matureEase4"); mNewEase0 = remoteStat.getInt("newEase0"); mNewEase1 = remoteStat.getInt("newEase1"); mNewEase2 = remoteStat.getInt("newEase2"); mNewEase3 = remoteStat.getInt("newEase3"); mNewEase4 = remoteStat.getInt("newEase4"); mReps = remoteStat.getInt("reps"); mReviewTime = remoteStat.getDouble("reviewTime"); mType = remoteStat.getInt("type"); mYoungEase0 = remoteStat.getInt("youngEase0"); mYoungEase1 = remoteStat.getInt("youngEase1"); mYoungEase2 = remoteStat.getInt("youngEase2"); mYoungEase3 = remoteStat.getInt("youngEase3"); mYoungEase4 = remoteStat.getInt("youngEase4"); toDB(); } catch (JSONException e) { log.info("JSONException = " + e.getMessage()); } } public static Stats globalStats(Deck deck) { log.info("Getting global stats..."); int type = STATS_LIFE; Date today = Utils.genToday(deck.getUtcOffset()); ResultSet result = null; Stats stats = null; try { result = deck.getDB().rawQuery( "SELECT id " + "FROM stats WHERE type = " + String.valueOf(type)); if (result.next()) { stats = new Stats(deck); stats.fromDB(result.getLong(1)); return stats; } } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } stats = new Stats(deck); stats.create(type, today); stats.mType = type; return stats; } public static Stats dailyStats(Deck deck) { Date today = Utils.genToday(deck.getUtcOffset()); return getStats(deck, today); } public static Stats dailyStats(Deck deck, int dayD) { Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); cal.add(Calendar.DAY_OF_YEAR, -dayD); return getStats(deck, Utils.genDate(cal.getTime().getTime(), deck.getUtcOffset())); } public static Stats getStats(Deck deck, Date date) { log.info("Getting daily stats..."); int type = STATS_DAY; Stats stats = null; ResultSet result = null; try { log.info("Trying to get stats for " + date.toString()); result = deck.getDB().rawQuery( "SELECT id " + "FROM stats " + "WHERE type = " + String.valueOf(type) + " and day = \"" + date.toString() + "\""); if (result.next()) { stats = new Stats(deck); stats.fromDB(result.getLong(1)); return stats; } } catch (SQLException e) { e.printStackTrace(); } finally { if (result != null) { try { result.close(); } catch (SQLException e) { } } } stats = new Stats(deck); stats.create(type, date); stats.mType = type; return stats; } /** * @return the reps */ public int getReps() { return mReps; } /** * @return the reps */ public int getYesReps() { return mReps - mNewEase0 - mNewEase1 - mMatureEase0 - mMatureEase1 - mYoungEase0 - mYoungEase1; } /** * @return the average time */ public double getAverageTime() { return mAverageTime; } public double getReviewTime() { return mReviewTime; } /** * @return the day */ public Date getDay() { return mDay; } /** * @return the share of no answers on young cards */ public double getYesShare() { if (mReps != 0) { return 1 - (((double)(mNewEase0 + mNewEase1 + mYoungEase0 + mYoungEase1 + mMatureEase0 + mMatureEase1)) / (double)mReps); } else { return 0; } } /** * @return the share of no answers on young cards */ public double getMatureYesShare() { double matureNo = mMatureEase0 + mMatureEase1; double matureTotal = matureNo + mMatureEase2 + mMatureEase3 + mMatureEase4; if (matureTotal != 0) { return 1 - (matureNo / matureTotal); } else { return 0; } } /** * @return the share of no answers on mature cards */ public double getYoungNoShare() { double youngNo = mYoungEase0 + mYoungEase1; double youngTotal = youngNo + mYoungEase2 + mYoungEase3 + mYoungEase4; if (youngTotal != 0) { return youngNo / youngTotal; } else { return 0; } } /** * @return the total number of cards marked as new */ public int getNewCardsCount() { return mNewEase0 + mNewEase1 + mNewEase2 + mNewEase3 + mNewEase4; } }