mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
refactor(measurements): drop the in-memory range aggregation nothing calls
It bucketed by UTC day and was described as the route's fallback; no route has called it since the aggregation moved into SQL.
This commit is contained in:
1 parent
d91eb905fe
commit
537a73d3b2
2 files changed
-142
No files matched your search
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
aggregateRows,
|
||||
pickAggregateGrain,
|
||||
rangeLengthDays,
|
||||
DAILY_AGGREGATE_THRESHOLD_DAYS,
|
||||
@@ -70,72 +69,4 @@ describe("range-aggregation", () => {
|
||||
expect(rangeLengthDays(t, t)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("aggregateRows", () => {
|
||||
it("collapses to one row per day per type with mean values", () => {
|
||||
const t0 = new Date("2026-05-01T08:00:00.000Z");
|
||||
const t1 = new Date("2026-05-01T18:00:00.000Z");
|
||||
const t2 = new Date("2026-05-02T08:00:00.000Z");
|
||||
const rows = [
|
||||
{ type: "PULSE", value: 60, measuredAt: t0 },
|
||||
{ type: "PULSE", value: 80, measuredAt: t1 },
|
||||
{ type: "PULSE", value: 70, measuredAt: t2 },
|
||||
];
|
||||
|
||||
const out = aggregateRows(rows, "daily");
|
||||
expect(out).toHaveLength(2);
|
||||
expect(out[0].avg).toBe(70);
|
||||
expect(out[0].count).toBe(2);
|
||||
expect(out[1].avg).toBe(70);
|
||||
expect(out[1].count).toBe(1);
|
||||
});
|
||||
|
||||
it("collapses to one row per ISO week (Monday-anchored) with weekly", () => {
|
||||
// 2026-05-04 is a Monday; surrounding days share the same bucket.
|
||||
const monday = new Date("2026-05-04T10:00:00.000Z");
|
||||
const tuesday = new Date("2026-05-05T10:00:00.000Z");
|
||||
const nextMonday = new Date("2026-05-11T10:00:00.000Z");
|
||||
const rows = [
|
||||
{ type: "WEIGHT", value: 75.0, measuredAt: monday },
|
||||
{ type: "WEIGHT", value: 75.5, measuredAt: tuesday },
|
||||
{ type: "WEIGHT", value: 76.0, measuredAt: nextMonday },
|
||||
];
|
||||
const out = aggregateRows(rows, "weekly");
|
||||
expect(out).toHaveLength(2);
|
||||
expect(out[0].count).toBe(2);
|
||||
expect(out[0].avg).toBeCloseTo(75.25, 5);
|
||||
expect(out[1].count).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps per-type buckets separate on the same day", () => {
|
||||
const t = new Date("2026-05-01T12:00:00.000Z");
|
||||
const rows = [
|
||||
{ type: "PULSE", value: 70, measuredAt: t },
|
||||
{ type: "WEIGHT", value: 75, measuredAt: t },
|
||||
];
|
||||
const out = aggregateRows(rows, "daily");
|
||||
expect(out).toHaveLength(2);
|
||||
const types = new Set(out.map((r) => r.type));
|
||||
expect(types).toEqual(new Set(["PULSE", "WEIGHT"]));
|
||||
});
|
||||
|
||||
it("collapses to one row per UTC calendar month with monthly", () => {
|
||||
const a = new Date("2025-01-05T10:00:00.000Z");
|
||||
const b = new Date("2025-01-20T10:00:00.000Z");
|
||||
const c = new Date("2025-02-10T10:00:00.000Z");
|
||||
const rows = [
|
||||
{ type: "WEIGHT", value: 75, measuredAt: a },
|
||||
{ type: "WEIGHT", value: 76, measuredAt: b },
|
||||
{ type: "WEIGHT", value: 77, measuredAt: c },
|
||||
];
|
||||
const out = aggregateRows(rows, "monthly");
|
||||
expect(out).toHaveLength(2);
|
||||
expect(out[0].count).toBe(2);
|
||||
expect(out[0].avg).toBeCloseTo(75.5, 5);
|
||||
expect(out[1].count).toBe(1);
|
||||
expect(out[1].avg).toBe(77);
|
||||
expect(out[0].bucketStart.toISOString()).toBe("2025-01-01T00:00:00.000Z");
|
||||
expect(out[1].bucketStart.toISOString()).toBe("2025-02-01T00:00:00.000Z");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -69,76 +69,3 @@ export function rangeLengthDays(from: Date, to: Date): number {
|
||||
const ms = to.getTime() - from.getTime();
|
||||
return Math.max(1, Math.ceil(ms / 86_400_000));
|
||||
}
|
||||
|
||||
export interface AggregatedRow {
|
||||
bucketStart: Date;
|
||||
type: string;
|
||||
avg: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface RawRow {
|
||||
type: string;
|
||||
value: number;
|
||||
measuredAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory daily / weekly / monthly aggregation. Used by the route's
|
||||
* fallback path and by the test harness — the production path runs the
|
||||
* `aggregateRowsSql` query so the take cap never applies before
|
||||
* bucketising (v1.4.28 R4-CODE-C1).
|
||||
*/
|
||||
export function aggregateRows(
|
||||
rows: RawRow[],
|
||||
grain: "daily" | "weekly" | "monthly",
|
||||
): AggregatedRow[] {
|
||||
const buckets = new Map<
|
||||
string,
|
||||
{ bucketStart: Date; type: string; sum: number; count: number }
|
||||
>();
|
||||
|
||||
for (const row of rows) {
|
||||
const bucketStart = bucketStartFor(row.measuredAt, grain);
|
||||
const key = `${row.type}@${bucketStart.getTime()}`;
|
||||
const slot = buckets.get(key);
|
||||
if (slot) {
|
||||
slot.sum += row.value;
|
||||
slot.count += 1;
|
||||
} else {
|
||||
buckets.set(key, {
|
||||
bucketStart,
|
||||
type: row.type,
|
||||
sum: row.value,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(buckets.values())
|
||||
.map((s) => ({
|
||||
bucketStart: s.bucketStart,
|
||||
type: s.type,
|
||||
avg: s.sum / s.count,
|
||||
count: s.count,
|
||||
}))
|
||||
.sort((a, b) => a.bucketStart.getTime() - b.bucketStart.getTime());
|
||||
}
|
||||
|
||||
function bucketStartFor(d: Date, grain: "daily" | "weekly" | "monthly"): Date {
|
||||
if (grain === "daily") {
|
||||
return new Date(
|
||||
Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()),
|
||||
);
|
||||
}
|
||||
if (grain === "monthly") {
|
||||
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1));
|
||||
}
|
||||
// Weekly — anchor to the start of the ISO week (Monday) in UTC.
|
||||
const day = d.getUTCDay() || 7; // Sunday → 7
|
||||
const monday = new Date(
|
||||
Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()),
|
||||
);
|
||||
monday.setUTCDate(monday.getUTCDate() - (day - 1));
|
||||
return monday;
|
||||
}
|
||||
Reference in new issue
Block a user