// A day marked "not counted" leaves the averages but stays on the record. The // subtlety is that marking a day is not the same as deleting its events, and // three panels break if you treat it that way — see the notes in render(). import { load } from "./extract.mjs"; import { suite, eq, report } from "./assert.mjs"; const app = load({ names: [ "ymd", "startOfDay", "EXCLUDED_TYPE", "ALWAYS_COUNTS", "excludedSet", "excludedDays", "isExcluded", "countedEvents", "spansExcluded", "pairWindows", "sleepWindows", "sleepMsInRange", "lastEventOfType", "gapsBetween", ], lets: ["excludedSet"], // gapsBetween defaults its window to the chart picker; the checks pass it in. stubs: { chartDays: () => 7 }, }); const H = 3600_000; const at = (day, hour, min = 0) => new Date(2026, 8, day, hour, min).getTime(); const NOW = at(20, 12); const mark = (day) => ({ id: `x${day}`, type: app.EXCLUDED_TYPE, at: at(day, 12) }); const marking = (events) => { app.set.excludedSet(app.excludedDays(events)); return events; }; suite("what a marked day takes out of the numbers"); { const evs = marking([ { id: "a", type: "pee", at: at(18, 9) }, { id: "b", type: "pee", at: at(19, 9) }, { id: "c", type: "pee", at: at(20, 9) }, mark(19), ]); eq(app.gapsBetween(evs, "pee", 14), [], "a gap reaching across a marked day is discarded rather than measured"); } { const evs = marking([ { id: "d", type: "pee", at: at(18, 8) }, { id: "e", type: "pee", at: at(18, 12) }, { id: "f", type: "pee", at: at(20, 8) }, { id: "g", type: "pee", at: at(20, 12) }, mark(19), ]); eq(app.gapsBetween(evs, "pee", 14), [4 * H, 4 * H], "…while the gaps either side of it survive"); } { const evs = marking([ { id: "a", type: "pee", at: at(19, 9) }, { id: "b", type: "pee", at: at(20, 9) }, { id: "w", type: "weight", at: at(19, 10) }, { id: "n", type: "note", at: at(19, 11) }, mark(19), ]); // The marker sits on the day it marks, so it is filtered out with the rest; // harmless, since excludedSet was read off the full list beforehand. eq(app.countedEvents(evs).map(e => e.id).sort(), ["b", "n", "w"], "behaviour on a marked day is dropped; a weigh-in and a note are not"); app.set.excludedSet(new Set()); eq(app.countedEvents(evs).length, evs.length, "nothing marked means nothing filtered"); } suite("spansExcluded"); { marking([mark(19)]); eq(app.spansExcluded(at(19, 1), at(19, 5)), true, "wholly inside a marked day"); eq(app.spansExcluded(at(18, 23), at(20, 1)), true, "straddling one"); eq(app.spansExcluded(at(20, 1), at(20, 9)), false, "clear of one"); app.set.excludedSet(new Set()); eq(app.spansExcluded(at(18, 1), at(24, 1)), false, "nothing marked, so nothing spans"); } // These three were live bugs: the panels were handed a list with the marked // day's events removed, which answers a different question from the one each // of them is asking. suite("what a marked day must NOT take out"); { const evs = marking([ { id: "p1", type: "pee", at: at(19, 9) }, { id: "p2", type: "pee", at: at(20, 10) }, // on the marked day mark(20), ]); const last = app.lastEventOfType(evs, "pee"); eq((NOW - last.at) / H, 2, "Timing: 'since the last pee' is about now, so it finds the one on the marked day"); } { const evs = marking([ { id: "s1", type: "sleep-start", at: at(20, 1) }, { id: "s2", type: "sleep-end", at: at(20, 3) }, mark(20), ]); eq(app.sleepMsInRange(evs, at(20, 0), NOW) / H, 2, "Sleep trend: the curve for the day you are looking at shows its real hours"); } { const evs = marking([ { id: "s1", type: "sleep-start", at: at(19, 23) }, // marked day { id: "s2", type: "sleep-end", at: at(20, 7) }, // the next, unmarked mark(19), ]); eq(app.sleepMsInRange(evs, at(20, 0), at(20, 24)) / H, 7, "a nap crossing midnight still counts for the unmarked day it ends on"); } export default report("excluded-days");