Java tutorial
/**************************************************************************************** * 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(); } } }