raycer/frontend/public/js/stats.js
Spencer Flagg 392bd6416e fix: current streak should not reset to zero before today's check-in
currentStreak() was starting its backward walk from today, so if the
user hadn't checked in yet today the first lookup missed and it
returned 0 — even with a perfect streak on previous days.

Now when today's entry is absent and the goal is still active, the
count starts from yesterday instead. Once the goal window has closed
the original end-of-window anchor is preserved.
2026-04-25 13:29:43 +02:00

107 lines
2.8 KiB
JavaScript

/* Pure helpers for streaks, totals, and the goal-window countdown. */
const DAY_MS = 86_400_000;
export function todayISO(d = new Date()) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
export function isoToDate(iso) {
const [y, m, d] = iso.split('-').map(Number);
return new Date(y, m - 1, d);
}
export function addDays(iso, n) {
const d = isoToDate(iso);
d.setDate(d.getDate() + n);
return todayISO(d);
}
export function eachDay(startISO, endISO) {
const out = [];
let cur = startISO;
while (cur <= endISO) {
out.push(cur);
cur = addDays(cur, 1);
}
return out;
}
export function clampWindowEnd(goal, refISO = todayISO()) {
return goal.end_date < refISO ? goal.end_date : refISO;
}
export function daysRemaining(endISO, refISO = todayISO()) {
if (endISO < refISO) return 0;
return Math.round((isoToDate(endISO) - isoToDate(refISO)) / DAY_MS);
}
/**
* Build a Set of completed ISO dates for (user, goal) from a flat completions array.
*/
export function completedSet(completions, user, goalId) {
const set = new Set();
for (const c of completions) {
if (c.user === user && c.goal_id === goalId && c.completed) set.add(c.date);
}
return set;
}
/**
* Current streak ending at (or just before) `refISO`, walking back day-by-day
* while completions are present, never crossing the goal start.
*/
export function currentStreak(completedSetForUser, goal, refISO = todayISO()) {
const start = goal.start_date;
let end = clampWindowEnd(goal, refISO);
if (refISO < start) return 0;
// If today isn't completed yet and the goal is still active,
// start from yesterday — the day isn't over so don't break the streak.
if (end === refISO && !completedSetForUser.has(end)) {
end = addDays(end, -1);
}
let cur = end;
let n = 0;
while (cur >= start && completedSetForUser.has(cur)) {
n += 1;
cur = addDays(cur, -1);
}
return n;
}
export function longestStreak(completedSetForUser, goal, refISO = todayISO()) {
const start = goal.start_date;
const end = clampWindowEnd(goal, refISO);
if (end < start) return 0;
let best = 0;
let run = 0;
for (const d of eachDay(start, end)) {
if (completedSetForUser.has(d)) {
run += 1;
if (run > best) best = run;
} else {
run = 0;
}
}
return best;
}
export function totalCompleted(completedSetForUser, goal, refISO = todayISO()) {
const start = goal.start_date;
const end = clampWindowEnd(goal, refISO);
if (end < start) return 0;
let n = 0;
for (const d of eachDay(start, end)) {
if (completedSetForUser.has(d)) n += 1;
}
return n;
}
export function windowLength(goal) {
return eachDay(goal.start_date, goal.end_date).length;
}