raycer/frontend/public/js/stats.js

108 lines
2.8 KiB
JavaScript
Raw Normal View History

/* 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;
}