/****************************************************************************************
* 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.anki.stats;
import android.content.res.Resources;
import android.database.Cursor;
import android.webkit.WebView;
import com.ichi2.anki.R;
import com.ichi2.libanki.Collection;
import com.ichi2.libanki.Stats;
import com.ichi2.libanki.Utils;
import com.ichi2.themes.Themes;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import timber.log.Timber;
public class OverviewStatsBuilder {
private static final int CARDS_INDEX = 0;
private static final int THETIME_INDEX = 1;
private static final int FAILED_INDEX = 2;
private static final int LRN_INDEX = 3;
private static final int REV_INDEX = 4;
private static final int RELRN_INDEX = 5;
private static final int FILT_INDEX = 6;
private static final int MCNT_INDEX = 7;
private static final int MSUM_INDEX = 8;
private final WebView mWebView; //for resources access
private final Collection mCol;
private final boolean mWholeCollection;
private final Stats.AxisType mType;
public class OverviewStats {
public int forecastTotalReviews;
public double forecastAverageReviews;
public int forecastDueTomorrow;
public double reviewsPerDayOnAll;
public double reviewsPerDayOnStudyDays;
public int allDays;
public int daysStudied;
public double timePerDayOnAll;
public double timePerDayOnStudyDays;
public double totalTime;
public int totalReviews;
public double newCardsPerDay;
public int totalNewCards;
public double averageInterval;
public double longestInterval;
}
public OverviewStatsBuilder(WebView chartView, Collection collectionData, boolean isWholeCollection, Stats.AxisType mStatType) {
mWebView = chartView;
mCol = collectionData;
mWholeCollection = isWholeCollection;
mType = mStatType;
}
public String createInfoHtmlString() {
int textColorInt = Themes.getColorFromAttr(mWebView.getContext(), android.R.attr.textColor);
String textColor = String.format("#%06X", (0xFFFFFF & textColorInt)); // Color to hex string
String css = "<style>\n" +
"h1, h3 { margin-bottom: 0; margin-top: 1em; text-transform: capitalize; }\n" +
".pielabel { text-align:center; padding:0px; color:white; }\n" +
"body {color:" + textColor + ";}\n" +
"</style>";
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("<center>");
stringBuilder.append(css);
appendTodaysStats(stringBuilder);
appendOverViewStats(stringBuilder);
stringBuilder.append("</center>");
return stringBuilder.toString();
}
private void appendOverViewStats(StringBuilder stringBuilder) {
Stats stats = new Stats(mCol, mWholeCollection);
OverviewStats oStats = new OverviewStats();
stats.calculateOverviewStatistics(mType, oStats);
Resources res = mWebView.getResources();
stringBuilder.append(_title(res.getString(mType.descriptionId)));
boolean allDaysStudied = oStats.daysStudied == oStats.allDays;
String daysStudied = res.getString(R.string.stats_overview_days_studied,
(int) ((float) oStats.daysStudied / (float) oStats.allDays * 100),
oStats.daysStudied, oStats.allDays);
// FORECAST
// Fill in the forecast summaries first
calculateForecastOverview(mType, oStats);
stringBuilder.append(_subtitle(res.getString(R.string.stats_forecast).toUpperCase()));
stringBuilder.append(res.getString(R.string.stats_overview_forecast_total, oStats.forecastTotalReviews));
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_forecast_average, oStats.forecastAverageReviews));
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_forecast_due_tomorrow, oStats.forecastDueTomorrow));
stringBuilder.append("<br>");
// REVIEW COUNT
stringBuilder.append(_subtitle(res.getString(R.string.stats_review_count).toUpperCase()));
stringBuilder.append(daysStudied);
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_total_reviews, oStats.totalReviews));
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_reviews_per_day_studydays, oStats.reviewsPerDayOnStudyDays));
if (!allDaysStudied) {
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_reviews_per_day_all, oStats.reviewsPerDayOnAll));
}
stringBuilder.append("<br>");
//REVIEW TIME
stringBuilder.append(_subtitle(res.getString(R.string.stats_review_time).toUpperCase()));
stringBuilder.append(daysStudied);
stringBuilder.append("<br>");
// TODO: Total: x minutes
stringBuilder.append(res.getString(R.string.stats_overview_time_per_day_studydays, oStats.timePerDayOnStudyDays));
if (!allDaysStudied) {
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_time_per_day_all, oStats.timePerDayOnAll));
}
// TODO: Average answer time: x.xs (x.x cards/minute)
stringBuilder.append("<br>");
// ADDED
stringBuilder.append(_subtitle(res.getString(R.string.stats_added).toUpperCase()));
stringBuilder.append(res.getString(R.string.stats_overview_total_new_cards, oStats.totalNewCards));
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_new_cards_per_day, oStats.newCardsPerDay));
stringBuilder.append("<br>");
// INTERVALS
stringBuilder.append(_subtitle(res.getString(R.string.stats_review_intervals).toUpperCase()));
stringBuilder.append(res.getString(R.string.stats_overview_average_interval));
stringBuilder.append(Utils.roundedTimeSpan(mWebView.getContext(), (int) Math.round(oStats.averageInterval * Stats.SECONDS_PER_DAY)));
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_overview_longest_interval));
stringBuilder.append(Utils.roundedTimeSpan(mWebView.getContext(), (int) Math.round(oStats.longestInterval * Stats.SECONDS_PER_DAY)));
}
private void appendTodaysStats(StringBuilder stringBuilder) {
Stats stats = new Stats(mCol, mWholeCollection);
int[] todayStats = stats.calculateTodayStats();
stringBuilder.append(_title(mWebView.getResources().getString(R.string.stats_today)));
Resources res = mWebView.getResources();
final int minutes = (int) Math.round(todayStats[THETIME_INDEX] / 60.0);
final String span = res.getQuantityString(R.plurals.time_span_minutes, minutes, minutes);
stringBuilder.append(res.getQuantityString(R.plurals.stats_today_cards,
todayStats[CARDS_INDEX], todayStats[CARDS_INDEX], span));
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_today_again_count, todayStats[FAILED_INDEX]));
if (todayStats[CARDS_INDEX] > 0) {
stringBuilder.append(" ");
stringBuilder.append(res.getString(R.string.stats_today_correct_count, (((1 - todayStats[FAILED_INDEX] / (float) (todayStats[CARDS_INDEX])) * 100.0))));
}
stringBuilder.append("<br>");
stringBuilder.append(res.getString(R.string.stats_today_type_breakdown, todayStats[LRN_INDEX], todayStats[REV_INDEX], todayStats[RELRN_INDEX], todayStats[FILT_INDEX]));
stringBuilder.append("<br>");
if (todayStats[MCNT_INDEX] != 0) {
stringBuilder.append(res.getString(R.string.stats_today_mature_cards, todayStats[MSUM_INDEX], todayStats[MCNT_INDEX], (todayStats[MSUM_INDEX] / (float) (todayStats[MCNT_INDEX]) * 100.0)));
} else {
stringBuilder.append(res.getString(R.string.stats_today_no_mature_cards));
}
}
private String _title(String title) {
return "<h1>" + title + "</h1>";
}
private String _subtitle(String title) {
return "<h3>" + title + "</h3>";
}
// This is a copy of Stats#calculateDue that is more similar to the original desktop version which
// allows us to easily fetch the values required for the summary. In the future, this version
// should replace the one in Stats.java.
private void calculateForecastOverview(Stats.AxisType type, OverviewStats oStats) {
Integer start = null;
Integer end = null;
int chunk = 0;
switch (type) {
case TYPE_MONTH:
start = 0; end = 31; chunk = 1;
break;
case TYPE_YEAR:
start = 0; end = 52; chunk = 7;
break;
case TYPE_LIFE:
start = 0; end = null; chunk = 30;
break;
}
List<int[]> d = _due(start, end, chunk);
List<int[]> yng = new ArrayList<>();
List<int[]> mtr = new ArrayList<>();
int tot = 0;
List<int[]> totd = new ArrayList<>();
for (int[] day : d) {
yng.add(new int[] {day[0], day[1]});
mtr.add(new int[] {day[0], day[2]});
tot += day[1]+day[2];
totd.add(new int[] {day[0], tot});
}
// Fill in the overview stats
oStats.forecastTotalReviews = tot;
oStats.forecastAverageReviews = totd.size() == 0 ? 0 : (double) tot / (totd.size() * chunk);
oStats.forecastDueTomorrow = mCol.getDb().queryScalar(String.format(Locale.US,
"select count() from cards where did in %s and queue in (2,3) " +
"and due = ?", _limit()), new String[]{Integer.toString(mCol.getSched().getToday() + 1)});
}
private List<int[]> _due(Integer start, Integer end, int chunk) {
String lim = "";
if (start != null) {
lim += String.format(Locale.US, " and due-%d >= %d", mCol.getSched().getToday(), start);
}
if (end != null) {
lim += String.format(Locale.US, " and day < %d", end);
}
List<int[]> d = new ArrayList<>();
Cursor cur = null;
try {
String query;
query = String.format(Locale.US,
"select (due-%d)/%d as day,\n" +
"sum(case when ivl < 21 then 1 else 0 end), -- yng\n" +
"sum(case when ivl >= 21 then 1 else 0 end) -- mtr\n" +
"from cards\n" +
"where did in %s and queue in (2,3)\n" +
"%s\n" +
"group by day order by day",
mCol.getSched().getToday(), chunk, _limit(), lim);
cur = mCol.getDb().getDatabase().rawQuery(query, null);
while (cur.moveToNext()) {
d.add(new int[]{cur.getInt(0), cur.getInt(1), cur.getInt(2)});
}
} finally {
if (cur != null && !cur.isClosed()) {
cur.close();
}
}
return d;
}
private String _limit() {
if (mWholeCollection) {
ArrayList<Long> ids = new ArrayList<>();
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();
}
}
}