Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,25 @@ <h3 style="margin-bottom:0">Life Events</h3>
return cutoff.toISOString().slice(0, 10);
}

// SUMMARY-STATS-START (extracted verbatim by tests/js/summary_stats.test.mjs)
function computeSummaryStats(allDays) {
const avgStat = vals => { const c = vals.filter(v => v != null); return c.length ? +(c.reduce((a,b)=>a+b,0)/c.length).toFixed(1) : null; };
// Seed-free min/max: a null seed would coerce to 0 in Math.min/Math.max.
const extreme = (vals, pick) => { const c = vals.filter(v => v != null); return c.length ? pick(...c) : null; };
const bestStepsDay = allDays.reduce((best, d) => (!best || (d.steps||0) > (best.steps||0)) ? d : best, null);
return {
avg_steps: avgStat(allDays.map(d => d.steps)),
avg_sleep_hours: avgStat(allDays.map(d => d.sleep_hours)),
avg_resting_hr: avgStat(allDays.map(d => d.resting_hr)),
avg_hrv: avgStat(allDays.map(d => d.hrv)),
best_sleep: extreme(allDays.map(d => d.sleep_hours), Math.max),
worst_sleep: extreme(allDays.map(d => d.sleep_hours), Math.min),
best_steps_day: bestStepsDay ? bestStepsDay.date : null,
total_days: allDays.length,
};
}
// SUMMARY-STATS-END

async function parseHealthXML(file, onProgress) {
onProgress(0, 'Reading file...');
const text = await file.text();
Expand Down Expand Up @@ -1812,20 +1831,8 @@ <h3 style="margin-bottom:0">Life Events</h3>
onProgress(90, 'Computing summary...');
await new Promise(r => setTimeout(r, 0));

// Summary stats
const allDays = Object.values(filtered);
const avgStat = vals => { const c = vals.filter(v => v != null); return c.length ? +(c.reduce((a,b)=>a+b,0)/c.length).toFixed(1) : null; };
const bestStepsDay = allDays.reduce((best, d) => (!best || (d.steps||0) > (best.steps||0)) ? d : best, null);
const summary = {
avg_steps: avgStat(allDays.map(d => d.steps)),
avg_sleep_hours: avgStat(allDays.map(d => d.sleep_hours)),
avg_resting_hr: avgStat(allDays.map(d => d.resting_hr)),
avg_hrv: avgStat(allDays.map(d => d.hrv)),
best_sleep: allDays.map(d=>d.sleep_hours).filter(v=>v!=null).reduce((a,b)=>Math.max(a,b), null),
worst_sleep: allDays.map(d=>d.sleep_hours).filter(v=>v!=null).reduce((a,b)=>Math.min(a,b), null),
best_steps_day: bestStepsDay ? bestStepsDay.date : null,
total_days: allDays.length,
};
const summary = computeSummaryStats(allDays);

return {
daily: filtered,
Expand Down
42 changes: 42 additions & 0 deletions tests/js/summary_stats.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Run: node --test tests/js/summary_stats.test.mjs
// Extracts computeSummaryStats from the single-file frontend so the real code is tested.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';

const html = readFileSync(fileURLToPath(new URL('../../index.html', import.meta.url)), 'utf8');
const m = html.match(/\/\/ SUMMARY-STATS-START[^\n]*\n([\s\S]*?)\/\/ SUMMARY-STATS-END/);
assert.ok(m, 'summary-stats markers not found in index.html');
const computeSummaryStats = new Function(`${m[1]}; return computeSummaryStats;`)();

const days = sleeps => sleeps.map((s, i) => ({ date: `2025-01-0${i + 1}`, steps: 1000 * (i + 1), sleep_hours: s }));

test('best/worst sleep over positive values', () => {
const s = computeSummaryStats(days([7, 6.5, 8]));
assert.equal(s.worst_sleep, 6.5);
assert.equal(s.best_sleep, 8);
});

test('nulls are ignored', () => {
const s = computeSummaryStats(days([null, 7, null, 5.5]));
assert.equal(s.worst_sleep, 5.5);
assert.equal(s.best_sleep, 7);
});

test('no sleep data stays null, not 0', () => {
for (const input of [[], days([null, null])]) {
const s = computeSummaryStats(input);
assert.equal(s.worst_sleep, null);
assert.equal(s.best_sleep, null);
assert.equal(s.avg_sleep_hours, null);
}
});

test('other stats unchanged', () => {
const s = computeSummaryStats(days([7, 6.5, 8]));
assert.equal(s.best_steps_day, '2025-01-03');
assert.equal(s.total_days, 3);
assert.equal(s.avg_steps, 2000);
assert.equal(computeSummaryStats([]).best_steps_day, null);
});
Loading