Commit 6a530eed authored by Lückemeyer's avatar Lückemeyer
Browse files

claude added streak to stats, 2 iterations

parent 924d7bf9
...@@ -20,7 +20,7 @@ public interface TryDao { ...@@ -20,7 +20,7 @@ public interface TryDao {
// ── Stats queries ────────────────────────────────────────────────────── // ── Stats queries ──────────────────────────────────────────────────────
/** Success rate per volet (all-time). */ /** Success rate per volet (all-time), sorted by rate ascending. */
@Query("SELECT v.id AS voletId, v.title AS voletTitle, " + @Query("SELECT v.id AS voletId, v.title AS voletTitle, " +
"u.title AS unitTitle, b.title AS bookTitle, " + "u.title AS unitTitle, b.title AS bookTitle, " +
"COUNT(tr.id) AS total, SUM(tr.correct) AS correct " + "COUNT(tr.id) AS total, SUM(tr.correct) AS correct " +
...@@ -31,9 +31,24 @@ public interface TryDao { ...@@ -31,9 +31,24 @@ public interface TryDao {
"JOIN unit u ON u.id = v.unit_id " + "JOIN unit u ON u.id = v.unit_id " +
"JOIN book b ON b.id = u.book_id " + "JOIN book b ON b.id = u.book_id " +
"GROUP BY v.id " + "GROUP BY v.id " +
"ORDER BY (CAST(SUM(tr.correct) AS REAL) / COUNT(tr.id)) DESC") "ORDER BY (CAST(SUM(tr.correct) AS REAL) / COUNT(tr.id)) ASC")
List<VoletStat> getVoletStatsSync(); List<VoletStat> getVoletStatsSync();
/** Success rate per volet per calendar day, sorted by day desc then volet. */
@Query("SELECT v.id AS voletId, v.title AS voletTitle, " +
"u.title AS unitTitle, b.title AS bookTitle, " +
"date(tr.responded_at, 'unixepoch') AS day, " +
"COUNT(tr.id) AS total, SUM(tr.correct) AS correct " +
"FROM try_record tr " +
"JOIN leitner_state ls ON ls.id = tr.leitner_state_id " +
"JOIN card c ON c.id = ls.card_id " +
"JOIN volet v ON v.id = c.volet_id " +
"JOIN unit u ON u.id = v.unit_id " +
"JOIN book b ON b.id = u.book_id " +
"GROUP BY v.id, day " +
"ORDER BY day DESC, b.id ASC, u.position ASC, v.position ASC")
List<VoletDayStat> getVoletDayStatsSync();
/** Success rate per calendar day. */ /** Success rate per calendar day. */
@Query("SELECT date(responded_at, 'unixepoch') AS day, " + @Query("SELECT date(responded_at, 'unixepoch') AS day, " +
"COUNT(id) AS total, SUM(correct) AS correct " + "COUNT(id) AS total, SUM(correct) AS correct " +
...@@ -41,6 +56,38 @@ public interface TryDao { ...@@ -41,6 +56,38 @@ public interface TryDao {
"GROUP BY day ORDER BY day DESC") "GROUP BY day ORDER BY day DESC")
List<DayStat> getDayStatsSync(); List<DayStat> getDayStatsSync();
/**
* All try_record rows for a volet, ordered by time — used to compute
* the longest correct streak per volet in Java.
*/
@Query("SELECT tr.correct FROM try_record tr " +
"JOIN leitner_state ls ON ls.id = tr.leitner_state_id " +
"JOIN card c ON c.id = ls.card_id " +
"WHERE c.volet_id = :voletId " +
"ORDER BY tr.responded_at ASC")
List<Integer> getCorrectSequenceForVoletSync(int voletId);
/**
* All try_record rows for a calendar day, ordered by time — used to
* compute the longest correct streak for that day in Java.
*/
@Query("SELECT correct FROM try_record " +
"WHERE date(responded_at, 'unixepoch') = :day " +
"ORDER BY responded_at ASC")
List<Integer> getCorrectSequenceForDaySync(String day);
/**
* All try_record rows for a volet on a specific day, ordered by time —
* used to compute the streak for the by-day volet table.
*/
@Query("SELECT tr.correct FROM try_record tr " +
"JOIN leitner_state ls ON ls.id = tr.leitner_state_id " +
"JOIN card c ON c.id = ls.card_id " +
"WHERE c.volet_id = :voletId " +
"AND date(tr.responded_at, 'unixepoch') = :day " +
"ORDER BY tr.responded_at ASC")
List<Integer> getCorrectSequenceForVoletDaySync(int voletId, String day);
class VoletStat { class VoletStat {
public int voletId; public int voletId;
public String voletTitle; public String voletTitle;
...@@ -50,6 +97,16 @@ public interface TryDao { ...@@ -50,6 +97,16 @@ public interface TryDao {
public int correct; public int correct;
} }
class VoletDayStat {
public int voletId;
public String voletTitle;
public String unitTitle;
public String bookTitle;
public String day;
public int total;
public int correct;
}
class DayStat { class DayStat {
public String day; public String day;
public int total; public int total;
......
...@@ -107,8 +107,9 @@ public class TrainingRepository { ...@@ -107,8 +107,9 @@ public class TrainingRepository {
// ── Stats ────────────────────────────────────────────────────────────────── // ── Stats ──────────────────────────────────────────────────────────────────
public List<TryDao.VoletStat> getVoletStats() { return db.tryDao().getVoletStatsSync(); } public List<TryDao.VoletStat> getVoletStats() { return db.tryDao().getVoletStatsSync(); }
public List<TryDao.DayStat> getDayStats() { return db.tryDao().getDayStatsSync(); } public List<TryDao.VoletDayStat> getVoletDayStats() { return db.tryDao().getVoletDayStatsSync(); }
public List<TryDao.DayStat> getDayStats() { return db.tryDao().getDayStatsSync(); }
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────
......
...@@ -5,11 +5,12 @@ import android.view.*; ...@@ -5,11 +5,12 @@ import android.view.*;
import android.widget.*; import android.widget.*;
import androidx.annotation.*; import androidx.annotation.*;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import dev.lueckemeyer.vocoach.R;
import dev.lueckemeyer.vocoach.databinding.FragmentStatsBinding; import dev.lueckemeyer.vocoach.databinding.FragmentStatsBinding;
import dev.lueckemeyer.vocoach.db.VocabDatabase;
import dev.lueckemeyer.vocoach.db.dao.TryDao; import dev.lueckemeyer.vocoach.db.dao.TryDao;
import dev.lueckemeyer.vocoach.repository.TrainingRepository; import dev.lueckemeyer.vocoach.repository.TrainingRepository;
...@@ -17,8 +18,19 @@ public class StatsFragment extends Fragment { ...@@ -17,8 +18,19 @@ public class StatsFragment extends Fragment {
private FragmentStatsBinding binding; private FragmentStatsBinding binding;
private TrainingRepository repo; private TrainingRepository repo;
private VocabDatabase db;
private final ExecutorService exec = Executors.newSingleThreadExecutor(); private final ExecutorService exec = Executors.newSingleThreadExecutor();
/** true = all-time volet table (toggle right), false = by-day (toggle left) */
private boolean voletAllTime = true;
// ── Simple display-ready row holder ──────────────────────────────────────
/** A fully-formatted table row, ready to hand to the UI thread. */
private static class DisplayRow {
final String[] cells;
DisplayRow(String... cells) { this.cells = cells; }
}
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) { ViewGroup container, Bundle savedInstanceState) {
...@@ -30,56 +42,141 @@ public class StatsFragment extends Fragment { ...@@ -30,56 +42,141 @@ public class StatsFragment extends Fragment {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
repo = new TrainingRepository(requireContext()); repo = new TrainingRepository(requireContext());
db = VocabDatabase.getInstance(requireContext());
// Toggle: unchecked = By day (left), checked = All time (right, default)
binding.toggleVoletPeriod.setChecked(true);
binding.toggleVoletPeriod.setOnCheckedChangeListener((btn, checked) -> {
voletAllTime = checked;
loadVoletTable();
});
loadStats(); loadStats();
} }
// ── Data loading — all DB work stays on the executor thread ───────────────
private void loadStats() { private void loadStats() {
binding.progressStats.setVisibility(View.VISIBLE); binding.progressStats.setVisibility(View.VISIBLE);
exec.execute(() -> { exec.execute(() -> {
List<TryDao.VoletStat> voletStats = repo.getVoletStats(); // Build day table rows (including streak) entirely on background thread
List<TryDao.DayStat> dayStats = repo.getDayStats(); List<TryDao.DayStat> dayStats = repo.getDayStats();
List<DisplayRow> dayRows = new ArrayList<>();
for (TryDao.DayStat s : dayStats) {
String rate = s.total == 0 ? "—"
: String.format("%.0f%%", 100.0 * s.correct / s.total);
int streak = longestStreak(
db.tryDao().getCorrectSequenceForDaySync(s.day));
dayRows.add(new DisplayRow(
s.day,
String.valueOf(s.correct),
String.valueOf(s.total),
rate,
String.valueOf(streak)));
}
requireActivity().runOnUiThread(() -> { requireActivity().runOnUiThread(() -> {
binding.progressStats.setVisibility(View.GONE); binding.progressStats.setVisibility(View.GONE);
populateVoletTable(voletStats); renderDayTable(dayRows);
populateDayTable(dayStats);
}); });
// Also kick off the volet table on the same background thread
loadVoletTableOnBackground();
}); });
} }
private void populateVoletTable(List<TryDao.VoletStat> rows) { private void loadVoletTable() {
TableLayout table = binding.tableVolet; binding.progressStats.setVisibility(View.VISIBLE);
// Header binding.tableVolet.removeAllViews();
table.addView(makeRow(true, "Book", "Unit", "Volet", "Correct", "Rate")); exec.execute(this::loadVoletTableOnBackground);
if (rows.isEmpty()) { }
table.addView(makeRow(false, "—", "—", "No data yet", "—", "—"));
return; /** Must only be called from the executor thread. */
private void loadVoletTableOnBackground() {
final List<DisplayRow> rows;
final boolean allTime = voletAllTime;
if (allTime) {
List<TryDao.VoletStat> stats = repo.getVoletStats();
rows = new ArrayList<>();
for (TryDao.VoletStat s : stats) {
String rate = s.total == 0 ? "—"
: String.format("%.0f%%", 100.0 * s.correct / s.total);
int streak = longestStreak(
db.tryDao().getCorrectSequenceForVoletSync(s.voletId));
rows.add(new DisplayRow(
s.bookTitle, s.unitTitle, s.voletTitle,
s.correct + "/" + s.total, rate,
String.valueOf(streak)));
}
} else {
List<TryDao.VoletDayStat> stats = repo.getVoletDayStats();
rows = new ArrayList<>();
for (TryDao.VoletDayStat s : stats) {
String rate = s.total == 0 ? "—"
: String.format("%.0f%%", 100.0 * s.correct / s.total);
int streak = longestStreak(
db.tryDao().getCorrectSequenceForVoletDaySync(s.voletId, s.day));
rows.add(new DisplayRow(
s.day, s.voletTitle,
s.correct + "/" + s.total, rate,
String.valueOf(streak), ""));
}
} }
for (TryDao.VoletStat s : rows) {
String rate = s.total == 0 ? "—" requireActivity().runOnUiThread(() -> {
: String.format("%.0f%%", 100.0 * s.correct / s.total); binding.progressStats.setVisibility(View.GONE);
table.addView(makeRow(false, s.bookTitle, s.unitTitle, renderVoletTable(allTime, rows);
s.voletTitle, s.correct + "/" + s.total, rate)); });
}
// ── Table rendering — UI thread only, no DB access ────────────────────────
private void renderVoletTable(boolean allTime, List<DisplayRow> rows) {
TableLayout table = binding.tableVolet;
table.removeAllViews();
if (allTime) {
table.addView(makeRow(true,
"Book", "Unit", "Volet", "Correct", "Rate", "Streak"));
if (rows.isEmpty()) {
table.addView(makeRow(false, "—", "—", "No data yet", "—", "—", "—"));
}
} else {
table.addView(makeRow(true,
"Day", "Volet", "Correct", "Rate", "Streak", ""));
if (rows.isEmpty()) {
table.addView(makeRow(false, "—", "—", "No data yet", "—", "—", ""));
}
} }
for (DisplayRow r : rows) table.addView(makeRow(false, r.cells));
} }
private void populateDayTable(List<TryDao.DayStat> rows) { private void renderDayTable(List<DisplayRow> rows) {
TableLayout table = binding.tableDay; TableLayout table = binding.tableDay;
table.addView(makeRow(true, "Date", "Correct", "Total", "Rate", "")); table.removeAllViews();
table.addView(makeRow(true, "Date", "Correct", "Total", "Rate", "Streak"));
if (rows.isEmpty()) { if (rows.isEmpty()) {
table.addView(makeRow(false, "—", "—", "No data yet", "—", "")); table.addView(makeRow(false, "—", "—", "No data yet", "—", "—"));
return;
} }
for (TryDao.DayStat s : rows) { for (DisplayRow r : rows) table.addView(makeRow(false, r.cells));
String rate = s.total == 0 ? "—" }
: String.format("%.0f%%", 100.0 * s.correct / s.total);
table.addView(makeRow(false, s.day, // ── Streak computation ────────────────────────────────────────────────────
String.valueOf(s.correct), String.valueOf(s.total), rate, ""));
/** Longest consecutive run of correct answers (value=1) in the ordered sequence. */
private static int longestStreak(List<Integer> sequence) {
int max = 0, current = 0;
for (int v : sequence) {
if (v == 1) { current++; max = Math.max(max, current); }
else { current = 0; }
} }
return max;
} }
// ── Row builder ───────────────────────────────────────────────────────────
private TableRow makeRow(boolean header, String... cells) { private TableRow makeRow(boolean header, String... cells) {
TableRow row = new TableRow(requireContext()); TableRow row = new TableRow(requireContext());
int bg = header ? 0xFF6200EE : android.R.color.transparent;
if (header) row.setBackgroundColor(0xFF6200EE); if (header) row.setBackgroundColor(0xFF6200EE);
for (String cell : cells) { for (String cell : cells) {
TextView tv = new TextView(requireContext()); TextView tv = new TextView(requireContext());
......
...@@ -22,14 +22,50 @@ ...@@ -22,14 +22,50 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<!-- Volet table section -->
<TextView <TextView
android:id="@+id/tv_volet_caption"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Success rate by Volet (all-time)" android:text="Success rate by Volet"
android:textAppearance="?attr/textAppearanceSubtitle1" android:textAppearance="?attr/textAppearanceSubtitle1"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginBottom="8dp"/> android:layout_marginBottom="8dp"/>
<!-- By day / All time toggle -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="By day"
android:textAppearance="?attr/textAppearanceBody2"
android:gravity="end"
android:paddingEnd="12dp"/>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/toggle_volet_period"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="All time"
android:textAppearance="?attr/textAppearanceBody2"
android:gravity="start"
android:paddingStart="12dp"/>
</LinearLayout>
<HorizontalScrollView <HorizontalScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment