mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(sleep): let a zero-minute granular row leave the bare night alone
A REM row of no length counted as the granular partition and stripped the bare aggregate beside it, so a six-hour night read as no sleep while the SQL page cut of the list counted it as a night.
This commit is contained in:
1 parent
b79bd8370d
commit
6328117bd2
2 files changed
+23
-5
No files matched your search
@@ -655,6 +655,21 @@ describe("the one nap rule", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("a zero-minute granular row", () => {
|
||||
it("does not strip the bare aggregate that carries the night", () => {
|
||||
// A writer's bare 6-hour night beside a REM row of no length: the REM
|
||||
// row partitions nothing, so the night is the aggregate's six hours.
|
||||
const rows: SleepStageRow[] = [
|
||||
row("2026-06-04T06:00:00.000Z", "ASLEEP", 360),
|
||||
row("2026-06-04T06:00:00.000Z", "REM", 0),
|
||||
];
|
||||
const [session] = reconstructSleepSessions(rows, "UTC");
|
||||
expect(session.asleepMinutes).toBe(360);
|
||||
expect(session.stages.ASLEEP).toBe(360);
|
||||
expect(reconstructSleepNights(rows, "UTC")[0].asleepMinutes).toBe(360);
|
||||
});
|
||||
});
|
||||
|
||||
describe("summarizeSleepNights", () => {
|
||||
it("summarises per-night totals, not per-stage rows", () => {
|
||||
const rows: SleepStageRow[] = [
|
||||
|
||||
@@ -177,7 +177,10 @@ export const ASLEEP_STAGES: readonly SleepStage[] = [
|
||||
function sawGranularStage(rows: readonly SleepStageRow[]): boolean {
|
||||
for (const r of rows) {
|
||||
const stage = r.sleepStage;
|
||||
if (stage != null && GRANULAR_ASLEEP_STAGES.has(stage)) return true;
|
||||
// A zero-minute granular row partitions nothing: it must not strip the
|
||||
// bare aggregate that does carry the night.
|
||||
if (stage != null && GRANULAR_ASLEEP_STAGES.has(stage) && r.value > 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -214,7 +217,7 @@ function asleepMinutesOf(rows: readonly SleepStageRow[]): number {
|
||||
const stage = seg.stage;
|
||||
if (stage != null && GRANULAR_ASLEEP_STAGES.has(stage)) {
|
||||
granular += seg.minutes;
|
||||
sawGranular = true;
|
||||
if (seg.minutes > 0) sawGranular = true;
|
||||
} else if (stage === "ASLEEP" || stage == null) {
|
||||
// Bare unspecified aggregate (`ASLEEP`) or a stage-less legacy/manual
|
||||
// row — only used when no granular stage carries the night.
|
||||
@@ -350,12 +353,12 @@ export interface SleepStageRow {
|
||||
*/
|
||||
source?: MeasurementSource | null;
|
||||
/**
|
||||
* Device-type tag of the stage row (`Measurement.deviceType`, v1.4.25
|
||||
* W8c). Distinguishes WRITERS behind one source: several HealthKit apps
|
||||
* Device-type tag of the stage row (`Measurement.deviceType`, v1.4.25).
|
||||
* Distinguishes WRITERS behind one source: several HealthKit apps
|
||||
* write sleep under `source = APPLE_HEALTH` (watch granular stages, phone
|
||||
* coarse in-bed, vendor-app re-exports), and collapsing on source alone
|
||||
* blends them. Optional — callers that don't select the column (and every
|
||||
* row stored before W8c) collapse per source exactly as before.
|
||||
* row stored before v1.4.25) collapse per source exactly as before.
|
||||
*/
|
||||
deviceType?: string | null;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user