From 2e1ba4cb9f26bc0f90a9423e4e9e5edf00476c92 Mon Sep 17 00:00:00 2001 From: Meerav Shah Date: Sat, 3 Oct 2026 15:51:07 -0400 Subject: [PATCH] Fix browser-side worst_sleep always being 0 Math.min(null, x) coerces null to 0, so the reduce seeded with null made worst_sleep 0 for every upload parsed in the browser. Extract the summary computation into computeSummaryStats() and compute min/max over the non-null values, returning null when there is no sleep data. best_sleep had the same null-seed pattern (only masked because values are positive) and gets the same treatment. Add a node test that extracts the function from index.html. Co-Authored-By: Claude Sonnet 5.5 --- index.html | 33 ++++++++++++++++---------- tests/js/summary_stats.test.mjs | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 tests/js/summary_stats.test.mjs diff --git a/index.html b/index.html index a84a99e..deb494f 100644 --- a/index.html +++ b/index.html @@ -1725,6 +1725,25 @@

Life Events

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(); @@ -1812,20 +1831,8 @@

Life Events

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, diff --git a/tests/js/summary_stats.test.mjs b/tests/js/summary_stats.test.mjs new file mode 100644 index 0000000..3f5ad45 --- /dev/null +++ b/tests/js/summary_stats.test.mjs @@ -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); +});