From 537a73d3b211fd1c90820d2e8c3bbbfec324176c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 20 Oct 2026 09:58:03 +0200 Subject: [PATCH] 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. --- .../__tests__/range-aggregation.test.ts | 69 ------------------ src/lib/measurements/range-aggregation.ts | 73 ------------------- 2 files changed, 142 deletions(-) diff --git a/src/lib/measurements/__tests__/range-aggregation.test.ts b/src/lib/measurements/__tests__/range-aggregation.test.ts index dfa7d99d7..06e493f3d 100644 --- a/src/lib/measurements/__tests__/range-aggregation.test.ts +++ b/src/lib/measurements/__tests__/range-aggregation.test.ts @@ -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"); - }); - }); }); diff --git a/src/lib/measurements/range-aggregation.ts b/src/lib/measurements/range-aggregation.ts index 71bc0f5fe..fc758a8c4 100644 --- a/src/lib/measurements/range-aggregation.ts +++ b/src/lib/measurements/range-aggregation.ts @@ -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; -}