Java tutorial
/**************************************************************************************** * Copyright (c) 2011 Norbert Nagold <norbert.nagold@gmail.com> * * Copyright (c) 2014 Michael Goldbach <michael@m-goldbach.net> * * * * 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.libanki; import android.content.Context; import android.database.Cursor; import com.ichi2.anki.AnkiDroidApp; import com.ichi2.anki.R; import org.json.JSONException; import org.json.JSONObject; import java.text.SimpleDateFormat; import java.util.*; import timber.log.Timber; /** * Deck statistics. */ public class Stats { public static final int TYPE_MONTH = 0; public static final int TYPE_YEAR = 1; public static final int TYPE_LIFE = 2; public static enum ChartType { FORECAST, REVIEW_COUNT, REVIEW_TIME, INTERVALS, HOURLY_BREAKDOWN, WEEKLY_BREAKDOWN, ANSWER_BUTTONS, CARDS_TYPES, OTHER }; private static Stats sCurrentInstance; private Collection mCol; private boolean mWholeCollection; private boolean mDynamicAxis = false; private boolean mIsPieChart = false; private double[][] mSeriesList; private boolean mHasColoredCumulative = false; private int mType; private int mTitle; private boolean mBackwards; private int[] mValueLabels; private int[] mColors; private int[] mAxisTitles; private int mMaxCards = 0; private int mMaxElements = 0; private double mFirstElement = 0; private double mLastElement = 0; private int mZeroIndex = 0; private boolean mFoundLearnCards = false; private boolean mFoundCramCards = false; private boolean mFoundRelearnCards; private double[][] mCumulative = null; private String mAverage; private String mLongest; private double mPeak; private double mMcount; public Stats(Collection col, boolean wholeCollection) { mCol = col; mWholeCollection = wholeCollection; sCurrentInstance = this; } public static Stats currentStats() { return sCurrentInstance; } public double[][] getSeriesList() { return mSeriesList; } public double[][] getCumulative() { return mCumulative; } public Object[] getMetaInfo() { String title; if (mWholeCollection) { title = AnkiDroidApp.getInstance().getResources().getString(R.string.card_browser_all_decks); } else { try { title = mCol.getDecks().current().getString("name"); } catch (JSONException e) { throw new RuntimeException(e); } } return new Object[] { /*0*/ mType, /*1*/mTitle, /*2*/mBackwards, /*3*/mValueLabels, /*4*/mColors, /*5*/mAxisTitles, /*6*/title, /*7*/mMaxCards, /*8*/mMaxElements, /*9*/mFirstElement, /*10*/mLastElement, /*11*/mZeroIndex, /*12*/mFoundLearnCards, /*13*/mFoundCramCards, /*14*/mFoundRelearnCards, /*15*/mAverage, /*16*/mLongest, /*17*/mPeak, /*18*/mMcount, /*19*/mHasColoredCumulative, /*20*/mDynamicAxis }; } /** * Todays statistics */ public int[] calculateTodayStats() { String lim = _revlogLimit(); if (lim.length() > 0) lim = " and " + lim; Cursor cur = null; String query = "select count(), sum(time)/1000, " + "sum(case when ease = 1 then 1 else 0 end), " + /* failed */ "sum(case when type = 0 then 1 else 0 end), " + /* learning */ "sum(case when type = 1 then 1 else 0 end), " + /* review */ "sum(case when type = 2 then 1 else 0 end), " + /* relearn */ "sum(case when type = 3 then 1 else 0 end) " + /* filter */ "from revlog where id > " + ((mCol.getSched().getDayCutoff() - 86400) * 1000) + " " + lim; Timber.d("todays statistics query: %s", query); int cards, thetime, failed, lrn, rev, relrn, filt; try { cur = mCol.getDb().getDatabase().rawQuery(query, null); cur.moveToFirst(); cards = cur.getInt(0); thetime = cur.getInt(1); failed = cur.getInt(2); lrn = cur.getInt(3); rev = cur.getInt(4); relrn = cur.getInt(5); filt = cur.getInt(6); } finally { if (cur != null && !cur.isClosed()) { cur.close(); } } query = "select count(), sum(case when ease = 1 then 0 else 1 end) from revlog " + "where lastIvl >= 21 and id > " + ((mCol.getSched().getDayCutoff() - 86400) * 1000) + " " + lim; Timber.d("todays statistics query 2: %s", query); int mcnt, msum; try { cur = mCol.getDb().getDatabase().rawQuery(query, null); cur.moveToFirst(); mcnt = cur.getInt(0); msum = cur.getInt(1); } finally { if (cur != null && !cur.isClosed()) { cur.close(); } } return new int[] { cards, thetime, failed, lrn, rev, relrn, filt, mcnt, msum }; } /** * Due and cumulative due * *********************************************************************************************** */ public boolean calculateDue(int type) { mHasColoredCumulative = false; mType = type; mDynamicAxis = true; mBackwards = false; mTitle = R.string.stats_forecast; mValueLabels = new int[] { R.string.statistics_young, R.string.statistics_mature }; mColors = new int[] { R.color.stats_young, R.color.stats_mature }; mAxisTitles = new int[] { type, R.string.stats_cards, R.string.stats_cumulative_cards }; int end = 0; int chunk = 0; switch (type) { case TYPE_MONTH: end = 31; chunk = 1; break; case TYPE_YEAR: end = 52; chunk = 7; break; case TYPE_LIFE: end = -1; chunk = 30; break; } String lim = "";// AND due - " + mCol.getSched().getToday() + " >= " + start; // leave this out in order to show // card too which were due the days before if (end != -1) { lim += " AND day <= " + end; } ArrayList<int[]> dues = new ArrayList<int[]>(); Cursor cur = null; try { String query; query = "SELECT (due - " + mCol.getSched().getToday() + ")/" + chunk + " AS day, " // day + "count(), " // all cards + "sum(CASE WHEN ivl >= 21 THEN 1 ELSE 0 END) " // mature cards + "FROM cards WHERE did IN " + _limit() + " AND queue IN (2,3)" + lim + " GROUP BY day ORDER BY day"; Timber.d("Forecast query: %s", query); cur = mCol.getDb().getDatabase().rawQuery(query, null); while (cur.moveToNext()) { dues.add(new int[] { cur.getInt(0), cur.getInt(1), cur.getInt(2) }); } } finally { if (cur != null && !cur.isClosed()) { cur.close(); } } // small adjustment for a proper chartbuilding with achartengine if (dues.size() == 0 || dues.get(0)[0] > 0) { dues.add(0, new int[] { 0, 0, 0 }); } if (end == -1 && dues.size() < 2) { end = 31; } if (type != TYPE_LIFE && dues.get(dues.size() - 1)[0] < end) { dues.add(new int[] { end, 0, 0 }); } else if (type == TYPE_LIFE && dues.size() < 2) { dues.add(new int[] { Math.max(12, dues.get(dues.size() - 1)[0] + 1), 0, 0 }); } mSeriesList = new double[3][dues.size()]; for (int i = 0; i < dues.size(); i++) { int[] data = dues.get(i); if (data[1] > mMaxCards) mMaxCards = data[1]; mSeriesList[0][i] = data[0]; mSeriesList[1][i] = data[1]; mSeriesList[2][i] = data[2]; if (data[0] > mLastElement) mLastElement = data[0]; if (data[0] == 0) { mZeroIndex = i; } } mMaxElements = dues.size() - 1; switch (mType) { case TYPE_MONTH: mLastElement = 31; break; case TYPE_YEAR: mLastElement = 52; break; default: } mFirstElement = 0; mHasColoredCumulative = false; mCumulative = Stats.createCumulative(new double[][] { mSeriesList[0], mSeriesList[1] }, mZeroIndex); mMcount = mCumulative[1][mCumulative[1].length - 1]; //some adjustments to not crash the chartbuilding with emtpy data if (mMaxElements == 0) { mMaxElements = 10; } if (mMcount == 0) { mMcount = 10; } if (mFirstElement == mLastElement) { mFirstElement = 0; mLastElement = 6; } if (mMaxCards == 0) mMaxCards = 10; return dues.size() > 0; } /* only needed for studyoptions small chart */ public static double[][] getSmallDueStats(Collection col) { ArrayList<int[]> dues = new ArrayList<int[]>(); Cursor cur = null; try { cur = col.getDb().getDatabase().rawQuery("SELECT (due - " + col.getSched().getToday() + ") AS day, " // day + "count(), " // all cards + "sum(CASE WHEN ivl >= 21 THEN 1 ELSE 0 END) " // mature cards + "FROM cards WHERE did IN " + col.getSched()._deckLimit() + " AND queue IN (2,3) AND day <= 7 GROUP BY day ORDER BY day", null); while (cur.moveToNext()) { dues.add(new int[] { cur.getInt(0), cur.getInt(1), cur.getInt(2) }); } } finally { if (cur != null && !cur.isClosed()) { cur.close(); } } // small adjustment for a proper chartbuilding with achartengine if (dues.size() == 0 || dues.get(0)[0] > 0) { dues.add(0, new int[] { 0, 0, 0 }); } if (dues.get(dues.size() - 1)[0] < 7) { dues.add(new int[] { 7, 0, 0 }); } double[][] serieslist = new double[3][dues.size()]; for (int i = 0; i < dues.size(); i++) { int[] data = dues.get(i); serieslist[0][i] = data[0]; serieslist[1][i] = data[1]; serieslist[2][i] = data[2]; } return serieslist; } public boolean calculateDone(int type, boolean reps) { mHasColoredCumulative = true; mDynamicAxis = true; mType = type; mBackwards = true; if (reps) { mTitle = R.string.stats_review_count; mAxisTitles = new int[] { type, R.string.stats_answers, R.string.stats_cumulative_answers }; } else { mTitle = R.string.stats_review_time; } mValueLabels = new int[] { R.string.statistics_learn, R.string.statistics_relearn, R.string.statistics_young, R.string.statistics_mature, R.string.statistics_cram }; mColors = new int[] { R.color.stats_learn, R.color.stats_relearn, R.color.stats_young, R.color.stats_mature, R.color.stats_cram }; int num = 0; int chunk = 0; switch (type) { case TYPE_MONTH: num = 31; chunk = 1; break; case TYPE_YEAR: num = 52; chunk = 7; break; case TYPE_LIFE: num = -1; chunk = 30; break; } ArrayList<String> lims = new ArrayList<String>(); if (num != -1) { lims.add("id > " + ((mCol.getSched().getDayCutoff() - ((num + 1) * chunk * 86400)) * 1000)); } String lim = _revlogLimit().replaceAll("[\\[\\]]", ""); if (lim.length() > 0) { lims.add(lim); } if (lims.size() > 0) { lim = "WHERE "; while (lims.size() > 1) { lim += lims.remove(0) + " AND "; } lim += lims.remove(0); } else { lim = ""; } String ti; String tf; if (!reps) { ti = "time/1000"; if (mType == TYPE_MONTH) { tf = "/60.0"; // minutes mAxisTitles = new int[] { type, R.string.stats_minutes, R.string.stats_cumulative_time_minutes }; } else { tf = "/3600.0"; // hours mAxisTitles = new int[] { type, R.string.stats_hours, R.string.stats_cumulative_time_hours }; } } else { ti = "1"; tf = ""; } ArrayList<double[]> list = new ArrayList<double[]>(); Cursor cur = null; String query = "SELECT (cast((id/1000 - " + mCol.getSched().getDayCutoff() + ") / 86400.0 AS INT))/" + chunk + " AS day, " + "sum(CASE WHEN type = 0 THEN " + ti + " ELSE 0 END)" + tf + ", " // lrn + "sum(CASE WHEN type = 1 AND lastIvl < 21 THEN " + ti + " ELSE 0 END)" + tf + ", " // yng + "sum(CASE WHEN type = 1 AND lastIvl >= 21 THEN " + ti + " ELSE 0 END)" + tf + ", " // mtr + "sum(CASE WHEN type = 2 THEN " + ti + " ELSE 0 END)" + tf + ", " // lapse + "sum(CASE WHEN type = 3 THEN " + ti + " ELSE 0 END)" + tf // cram + " FROM revlog " + lim + " GROUP BY day ORDER BY day"; Timber.d("ReviewCount query: %s", query); try { cur = mCol.getDb().getDatabase().rawQuery(query, null); while (cur.moveToNext()) { list.add(new double[] { cur.getDouble(0), cur.getDouble(1), cur.getDouble(4), cur.getDouble(2), cur.getDouble(3), cur.getDouble(5) }); } } finally { if (cur != null && !cur.isClosed()) { cur.close(); } } // small adjustment for a proper chartbuilding with achartengine if (type != TYPE_LIFE && (list.size() == 0 || list.get(0)[0] > -num)) { list.add(0, new double[] { -num, 0, 0, 0, 0, 0 }); } else if (type == TYPE_LIFE && list.size() == 0) { list.add(0, new double[] { -12, 0, 0, 0, 0, 0 }); } if (list.get(list.size() - 1)[0] < 0) { list.add(new double[] { 0, 0, 0, 0, 0, 0 }); } mSeriesList = new double[6][list.size()]; for (int i = 0; i < list.size(); i++) { double[] data = list.get(i); mSeriesList[0][i] = data[0]; // day mSeriesList[1][i] = data[1] + data[2] + data[3] + data[4] + data[5]; // lrn mSeriesList[2][i] = data[2] + data[3] + data[4] + data[5]; // relearn mSeriesList[3][i] = data[3] + data[4] + data[5]; // young mSeriesList[4][i] = data[4] + data[5]; // mature mSeriesList[5][i] = data[5]; // cram if (mSeriesList[1][i] > mMaxCards) mMaxCards = (int) Math.round(data[1] + data[2] + data[3] + data[4] + data[5]); if (data[5] >= 0.999) mFoundCramCards = true; if (data[1] >= 0.999) mFoundLearnCards = true; if (data[2] >= 0.999) mFoundRelearnCards = true; if (data[0] > mLastElement) mLastElement = data[0]; if (data[0] < mFirstElement) mFirstElement = data[0]; if (data[0] == 0) { mZeroIndex = i; } } mMaxElements = list.size() - 1; mCumulative = new double[6][]; mCumulative[0] = mSeriesList[0]; for (int i = 1; i < mSeriesList.length; i++) { mCumulative[i] = createCumulative(mSeriesList[i]); if (i > 1) { for (int j = 0; j < mCumulative[i - 1].length; j++) { mCumulative[i - 1][j] -= mCumulative[i][j]; } } } switch (mType) { case TYPE_MONTH: mFirstElement = -31; break; case TYPE_YEAR: mFirstElement = -52; break; default: } mMcount = 0; // we could assume the last element to be the largest, // but on some collections that may not be true due some negative values //so we search for the largest element: for (int i = 1; i < mCumulative.length; i++) { for (int j = 0; j < mCumulative[i].length; j++) { if (mMcount < mCumulative[i][j]) mMcount = mCumulative[i][j]; } } //some adjustments to not crash the chartbuilding with emtpy data if (mMaxCards == 0) mMaxCards = 10; if (mMaxElements == 0) { mMaxElements = 10; } if (mMcount == 0) { mMcount = 10; } if (mFirstElement == mLastElement) { mFirstElement = -10; mLastElement = 0; } return list.size() > 0; } /** * Intervals *********************************************************************************************** */ public boolean calculateIntervals(Context context, int type) { mDynamicAxis = true; mType = type; double all = 0, avg = 0, max_ = 0; mBackwards = false; mTitle = R.string.stats_review_intervals; mAxisTitles = new int[] { type, R.string.stats_cards, R.string.stats_percentage }; mValueLabels = new int[] { R.string.stats_cards_intervals }; mColors = new int[] { R.color.stats_interval }; int num = 0; String lim = ""; int chunk = 0; switch (type) { case TYPE_MONTH: num = 31; chunk = 1; lim = " and grp <= 30"; break; case TYPE_YEAR: num = 52; chunk = 7; lim = " and grp <= 52"; break; case TYPE_LIFE: num = -1; chunk = 30; lim = ""; break; } ArrayList<double[]> list = new ArrayList<double[]>(); Cursor cur = null; try { cur = mCol.getDb().getDatabase().rawQuery("select ivl / " + chunk + " as grp, count() from cards " + "where did in " + _limit() + " and queue = 2 " + lim + " " + "group by grp " + "order by grp", null); while (cur.moveToNext()) { list.add(new double[] { cur.getDouble(0), cur.getDouble(1) }); } if (cur != null && !cur.isClosed()) { cur.close(); } cur = mCol.getDb().getDatabase().rawQuery( "select count(), avg(ivl), max(ivl) from cards where did in " + _limit() + " and queue = 2", null); cur.moveToFirst(); all = cur.getDouble(0); avg = cur.getDouble(1); max_ = cur.getDouble(2); } finally { if (cur != null && !cur.isClosed()) { cur.close(); } } // small adjustment for a proper chartbuilding with achartengine if (list.size() == 0 || list.get(0)[0] > 0) { list.add(0, new double[] { 0, 0, 0 }); } if (num == -1 && list.size() < 2) { num = 31; } if (type != TYPE_LIFE && list.get(list.size() - 1)[0] < num) { list.add(new double[] { num, 0 }); } else if (type == TYPE_LIFE && list.size() < 2) { list.add(new double[] { Math.max(12, list.get(list.size() - 1)[0] + 1), 0 }); } mLastElement = 0; mSeriesList = new double[2][list.size()]; for (int i = 0; i < list.size(); i++) { double[] data = list.get(i); mSeriesList[0][i] = data[0]; // grp mSeriesList[1][i] = data[1]; // cnt if (mSeriesList[1][i] > mMaxCards) mMaxCards = (int) Math.round(data[1]); if (data[0] > mLastElement) mLastElement = data[0]; } mCumulative = createCumulative(mSeriesList); for (int i = 0; i < list.size(); i++) { mCumulative[1][i] /= all / 100; } mMcount = 100; switch (mType) { case TYPE_MONTH: mLastElement = 31; break; case TYPE_YEAR: mLastElement = 52; break; default: } mFirstElement = 0; mMaxElements = list.size() - 1; mAverage = Utils.timeSpan(context, (int) Math.round(avg * 86400)); mLongest = Utils.timeSpan(context, (int) Math.round(max_ * 86400)); //some adjustments to not crash the chartbuilding with emtpy data if (mMaxElements == 0) { mMaxElements = 10; } if (mMcount == 0) { mMcount = 10; } if (mFirstElement == mLastElement) { mFirstElement = 0; mLastElement = 6; } if (mMaxCards == 0) mMaxCards = 10; return list.size() > 0; } /** * Hourly Breakdown */ public boolean calculateBreakdown(int type) { mTitle = R.string.stats_breakdown; mAxisTitles = new int[] { R.string.stats_time_of_day, R.string.stats_percentage_correct, R.string.stats_reviews }; mValueLabels = new int[] { R.string.stats_percentage_correct, R.string.stats_answers }; mColors = new int[] { R.color.stats_counts, R.color.stats_hours }; mType = type; String lim = _revlogLimit().replaceAll("[\\[\\]]", ""); if (lim.length() > 0) { lim = " and " + lim; } Calendar sd = GregorianCalendar.getInstance(); sd.setTimeInMillis(mCol.getCrt() * 1000); int pd = _periodDays(); if (pd > 0) { lim += " and id > " + ((mCol.getSched().getDayCutoff() - (86400 * pd)) * 1000); } long cutoff = mCol.getSched().getDayCutoff(); long cut = cutoff - sd.get(Calendar.HOUR_OF_DAY) * 3600; ArrayList<double[]> list = new ArrayList<double[]>(); Cursor cur = null; String query = "select " + "23 - ((cast((" + cut + " - id/1000) / 3600.0 as int)) % 24) as hour, " + "sum(case when ease = 1 then 0 else 1 end) / " + "cast(count() as float) * 100, " + "count() " + "from revlog where type in (0,1,2) " + lim + " " + "group by hour having count() > 30 order by hour"; Timber.d(sd.get(Calendar.HOUR_OF_DAY) + " : " + cutoff + " breakdown query: %s", query); try { cur = mCol.getDb().getDatabase().rawQuery(query, null); while (cur.moveToNext()) { list.add(new double[] { cur.getDouble(0), cur.getDouble(1), cur.getDouble(2) }); } } finally { if (cur != null && !cur.isClosed()) { cur.close(); } } //TODO adjust for breakdown, for now only copied from intervals //small adjustment for a proper chartbuilding with achartengine if (list.size() == 0) { list.add(0, new double[] { 0, 0, 0 }); } for (int i = 0; i < list.size(); i++) { double[] data = list.get(i); int intHour = (int) data[0]; int hour = (intHour - 4) % 24; if (hour < 0) hour += 24; data[0] = hour; list.set(i, data); } Collections.sort(list, new Comparator<double[]>() { @Override public int compare(double[] s1, double[] s2) { if (s1[0] < s2[0]) return -1; if (s1[0] > s2[0]) return 1; return 0; } }); mSeriesList = new double[4][list.size()]; mPeak = 0.0; mMcount = 0.0; double minHour = Double.MAX_VALUE; double maxHour = 0; for (int i = 0; i < list.size(); i++) { double[] data = list.get(i); int hour = (int) data[0]; //double hour = data[0]; if (hour < minHour) minHour = hour; if (hour > maxHour) maxHour = hour; double pct = data[1]; if (pct > mPeak) mPeak = pct; mSeriesList[0][i] = hour; mSeriesList[1][i] = pct; mSeriesList[2][i] = data[2]; if (i == 0) { mSeriesList[3][i] = pct; } else { double prev = mSeriesList[3][i - 1]; double diff = pct - prev; diff /= 3.0; diff = Math.round(diff * 10.0) / 10.0; mSeriesList[3][i] = prev + diff; } if (data[2] > mMcount) mMcount = data[2]; if (mSeriesList[1][i] > mMaxCards) mMaxCards = (int) mSeriesList[1][i]; } mFirstElement = mSeriesList[0][0]; mLastElement = mSeriesList[0][mSeriesList[0].length - 1]; mMaxElements = (int) (maxHour - minHour); //some adjustments to not crash the chartbuilding with emtpy data if (mMaxElements == 0) { mMaxElements = 10; } if (mMcount == 0) { mMcount = 10; } if (mFirstElement == mLastElement) { mFirstElement = 0; mLastElement = 23; } if (mMaxCards == 0) mMaxCards = 10; return list.size() > 0; } /** * Weekly Breakdown */ public boolean calculateWeeklyBreakdown(int type) { mTitle = R.string.stats_weekly_breakdown; mAxisTitles = new int[] { R.string.stats_day_of_week, R.string.stats_percentage_correct, R.string.stats_reviews }; mValueLabels = new int[] { R.string.stats_percentage_correct, R.string.stats_answers }; mColors = new int[] { R.color.stats_counts, R.color.stats_hours }; mType = type; String lim = _revlogLimit().replaceAll("[\\[\\]]", ""); if (lim.length() > 0) { lim = " and " + lim; } Calendar sd = GregorianCalendar.getInstance(); sd.setTimeInMillis(mCol.getSched().getDayCutoff() * 1000); int pd = _periodDays(); if (pd > 0) { lim += " and id > " + ((mCol.getSched().getDayCutoff() - (86400 * pd)) * 1000); } long cutoff = mCol.getSched().getDayCutoff(); long cut = cutoff - sd.get(Calendar.HOUR_OF_DAY) * 3600; ArrayList<double[]> list = new ArrayList<double[]>(); Cursor cur = null; String query = "SELECT strftime('%w',datetime( cast(id/ 1000 -" + sd.get(Calendar.HOUR_OF_DAY) * 3600 + " as int), 'unixepoch')) as wd, " + "sum(case when ease = 1 then 0 else 1 end) / " + "cast(count() as float) * 100, " + "count() " + "from revlog " + "where type in (0,1,2) " + lim + " " + "group by wd " + "order by wd"; Timber.d(sd.get(Calendar.HOUR_OF_DAY) + " : " + cutoff + " weekly breakdown query: %s", query); try { cur = mCol.getDb().getDatabase().rawQuery(query, null); while (cur.moveToNext()) { list.add(new double[] { cur.getDouble(0), cur.getDouble(1), cur.getDouble(2) }); } } finally { if (cur != null && !cur.isClosed()) { cur.close(); } } //TODO adjust for breakdown, for now only copied from intervals // small adjustment for a proper chartbuilding with achartengine if (list.size() == 0) { list.add(0, new double[] { 0, 0, 0 }); } mSeriesList = new double[4][list.size()]; mPeak = 0.0; mMcount = 0.0; double minHour = Double.MAX_VALUE; double maxHour = 0; for (int i = 0; i < list.size(); i++) { double[] data = list.get(i); int hour = (int) data[0]; //double hour = data[0]; if (hour < minHour) minHour = hour; if (hour > maxHour) maxHour = hour; double pct = data[1]; if (pct > mPeak) mPeak = pct; mSeriesList[0][i] = hour; mSeriesList[1][i] = pct; mSeriesList[2][i] = data[2]; if (i == 0) { mSeriesList[3][i] = pct; } else { double prev = mSeriesList[3][i - 1]; double diff = pct - prev; diff /= 3.0; diff = Math.round(diff * 10.0) / 10.0; mSeriesList[3][i] = prev + diff; } if (data[2] > mMcount) mMcount = data[2]; if (mSeriesList[1][i] > mMaxCards) mMaxCards = (int) mSeriesList[1][i]; } mFirstElement = mSeriesList[0][0]; mLastElement = mSeriesList[0][mSeriesList[0].length - 1]; mMaxElements = (int) (maxHour - minHour); //some adjustments to not crash the chartbuilding with emtpy data if (mMaxElements == 0) { mMaxElements = 10; } if (mMcount == 0) { mMcount = 10; } if (mFirstElement == mLastElement) { mFirstElement = 0; mLastElement = 6; } if (mMaxCards == 0) mMaxCards = 10; return list.size() > 0; } /** * Answer Buttons */ public boolean calculateAnswerButtons(int type) { mHasColoredCumulative = true; mTitle = R.string.stats_answer_buttons; mAxisTitles = new int[] { R.string.stats_answer_type, R.string.stats_answers, R.string.stats_cumulative_correct_percentage }; mValueLabels = new int[] { R.string.statistics_learn, R.string.statistics_young, R.string.statistics_mature }; mColors = new int[] { R.color.stats_learn, R.color.stats_young, R.color.stats_mature }; mType = type; String lim = _revlogLimit().replaceAll("[\\[\\]]", ""); Vector<String> lims = new Vector<String>(); int days = 0; if (lim.length() > 0) lims.add(lim); if (type == TYPE_MONTH) days = 30; else if (type == TYPE_YEAR) days = 365; else days = -1; if (days > 0) lims.add("id > " + ((mCol.getSched().getDayCutoff() - (days * 86400)) * 1000)); if (lims.size() > 0) { lim = "where " + lims.get(0); for (int i = 1; i < lims.size(); i++) lim += " and " + lims.get(i); } else lim = ""; ArrayList<double[]> list = new ArrayList<double[]>(); Cursor cur = null; String query = "select (case " + " when type in (0,2) then 0 " + " when lastIvl < 21 then 1 " + " else 2 end) as thetype, " + " (case when type in (0,2) and ease = 4 then 3 else ease end), count() from revlog " + lim + " " + " group by thetype, ease " + " order by thetype, ease"; Timber.d("AnswerButtons query: %s", query); try { cur = mCol.getDb().getDatabase().rawQuery(query, null); while (cur.moveToNext()) { list.add(new double[] { cur.getDouble(0), cur.getDouble(1), cur.getDouble(2) }); } } finally { if (cur != null && !cur.isClosed()) { cur.close(); } } //TODO adjust for AnswerButton, for now only copied from intervals // small adjustment for a proper chartbuilding with achartengine if (list.size() == 0) { list.add(0, new double[] { 0, 1, 0 }); } double[] totals = new double[3]; for (int i = 0; i < list.size(); i++) { double[] data = list.get(i); int currentType = (int) data[0]; double ease = data[1]; double cnt = data[2]; totals[currentType] += cnt; } int badNew = 0; int badYoung = 0; int badMature = 0; mSeriesList = new double[4][list.size() + 1]; for (int i = 0; i < list.size(); i++) { double[] data = list.get(i); int currentType = (int) data[0]; double ease = data[1]; double cnt = data[2]; if (currentType == 1) ease += 5; else if (currentType == 2) ease += 10; if ((int) ease == 1) { badNew = i; } if ((int) ease == 6) { badYoung = i; } if ((int) ease == 11) { badMature = i; } mSeriesList[0][i] = ease; mSeriesList[1 + currentType][i] = cnt; if (cnt > mMaxCards) mMaxCards = (int) cnt; } mSeriesList[0][list.size()] = 15; mCumulative = new double[4][]; mCumulative[0] = mSeriesList[0]; mCumulative[1] = createCumulativeInPercent(mSeriesList[1], totals[0], badNew); mCumulative[2] = createCumulativeInPercent(mSeriesList[2], totals[1], badYoung); mCumulative[3] = createCumulativeInPercent(mSeriesList[3], totals[2], badMature); mFirstElement = 0.5; mLastElement = 14.5; mMcount = 100; mMaxElements = 15; //bars are positioned from 1 to 14 if (mMaxCards == 0) mMaxCards = 10; return list.size() > 0; } /** * Cards Types */ public boolean calculateCardsTypes(int type) { mTitle = R.string.stats_cards_types; mIsPieChart = true; mAxisTitles = new int[] { R.string.stats_answer_type, R.string.stats_answers, R.string.stats_cumulative_correct_percentage }; mValueLabels = new int[] { R.string.statistics_mature, R.string.statistics_young_and_learn, R.string.statistics_unlearned, R.string.statistics_suspended }; mColors = new int[] { R.color.stats_mature, R.color.stats_young, R.color.stats_unseen, R.color.stats_suspended }; mType = type; ArrayList<double[]> list = new ArrayList<double[]>(); double[] pieData; Cursor cur = null; String query = "select " + "sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr\n" + "sum(case when queue in (1,3) or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn\n" + "sum(case when queue=0 then 1 else 0 end), -- new\n" + "sum(case when queue<0 then 1 else 0 end) -- susp\n" + "from cards where did in " + _limit(); Timber.d("CardsTypes query: %s", query); try { cur = mCol.getDb().getDatabase().rawQuery(query, null); cur.moveToFirst(); pieData = new double[] { cur.getDouble(0), cur.getDouble(1), cur.getDouble(2), cur.getDouble(3) }; } finally { if (cur != null && !cur.isClosed()) { cur.close(); } } //TODO adjust for CardsTypes, for now only copied from intervals // small adjustment for a proper chartbuilding with achartengine // if (list.size() == 0 || list.get(0)[0] > 0) { // list.add(0, new double[] { 0, 0, 0 }); // } // if (num == -1 && list.size() < 2) { // num = 31; // } // if (type != Utils.TYPE_LIFE && list.get(list.size() - 1)[0] < num) { // list.add(new double[] { num, 0, 0 }); // } else if (type == Utils.TYPE_LIFE && list.size() < 2) { // list.add(new double[] { Math.max(12, list.get(list.size() - 1)[0] + 1), 0, 0 }); // } mSeriesList = new double[1][4]; mSeriesList[0] = pieData; mFirstElement = 0.5; mLastElement = 9.5; mMcount = 100; mMaxElements = 10; //bars are positioned from 1 to 14 if (mMaxCards == 0) mMaxCards = 10; return list.size() > 0; } /** * Tools *********************************************************************************************** */ private String _limit() { if (mWholeCollection) { ArrayList<Long> ids = new ArrayList<Long>(); for (JSONObject d : mCol.getDecks().all()) { try { ids.add(d.getLong("id")); } catch (JSONException e) { throw new RuntimeException(e); } } return Utils.ids2str(Utils.arrayList2array(ids)); } else { return mCol.getSched()._deckLimit(); } } private String _revlogLimit() { if (mWholeCollection) { return ""; } else { return "cid IN (SELECT id FROM cards WHERE did IN " + Utils.ids2str(mCol.getDecks().active()) + ")"; } } public static double[][] createCumulative(double[][] nonCumulative) { double[][] cumulativeValues = new double[2][nonCumulative[0].length]; cumulativeValues[0][0] = nonCumulative[0][0]; cumulativeValues[1][0] = nonCumulative[1][0]; for (int i = 1; i < nonCumulative[0].length; i++) { cumulativeValues[0][i] = nonCumulative[0][i]; cumulativeValues[1][i] = cumulativeValues[1][i - 1] + nonCumulative[1][i]; } return cumulativeValues; } public static double[][] createCumulative(double[][] nonCumulative, int startAtIndex) { double[][] cumulativeValues = new double[2][nonCumulative[0].length - startAtIndex]; cumulativeValues[0][0] = nonCumulative[0][startAtIndex]; cumulativeValues[1][0] = nonCumulative[1][startAtIndex]; for (int i = startAtIndex + 1; i < nonCumulative[0].length; i++) { cumulativeValues[0][i - startAtIndex] = nonCumulative[0][i]; cumulativeValues[1][i - startAtIndex] = cumulativeValues[1][i - 1 - startAtIndex] + nonCumulative[1][i]; } return cumulativeValues; } public static double[] createCumulative(double[] nonCumulative) { double[] cumulativeValues = new double[nonCumulative.length]; cumulativeValues[0] = nonCumulative[0]; for (int i = 1; i < nonCumulative.length; i++) { cumulativeValues[i] = cumulativeValues[i - 1] + nonCumulative[i]; } return cumulativeValues; } public static double[] createCumulativeInPercent(double[] nonCumulative, double total) { return createCumulativeInPercent(nonCumulative, total, -1); } //use -1 on ignoreIndex if you do not want to exclude anything public static double[] createCumulativeInPercent(double[] nonCumulative, double total, int ignoreIndex) { double[] cumulativeValues = new double[nonCumulative.length]; if (total < 1) cumulativeValues[0] = 0; else if (0 != ignoreIndex) cumulativeValues[0] = nonCumulative[0] / total * 100.0; for (int i = 1; i < nonCumulative.length; i++) { if (total < 1) { cumulativeValues[i] = 0; } else if (i != ignoreIndex) cumulativeValues[i] = cumulativeValues[i - 1] + nonCumulative[i] / total * 100.0; else cumulativeValues[i] = cumulativeValues[i - 1]; } return cumulativeValues; } private int _periodDays() { switch (mType) { case TYPE_MONTH: return 30; case TYPE_YEAR: return 365; default: case TYPE_LIFE: return -1; } } }