mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
chore(analytics): drop unreferenced movingAverage and weeklyAverages exports
Neither function had any production caller. The chart components in src/components/charts/* implement a local movingAverageByPoints rather than reaching for the lib helper, and weeklyAverages was never imported outside its own test file. detectAnomalies stays — it is reached transitively through summarize() which the analytics, insights, and measurements-series routes all consume.
This commit is contained in:
1 parent
2bff80aaf9
commit
9df23c3c87
4 files changed
+1
-174
No files matched your search
@@ -1,10 +1,6 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { DataPoint } from "../trends";
|
||||
import {
|
||||
pairByTimestamp,
|
||||
pearsonCorrelation,
|
||||
weeklyAverages,
|
||||
} from "../correlations";
|
||||
import { pairByTimestamp, pearsonCorrelation } from "../correlations";
|
||||
|
||||
function makePoints(values: number[], startDaysAgo = 30): DataPoint[] {
|
||||
const now = Date.now();
|
||||
@@ -107,57 +103,3 @@ describe("pearsonCorrelation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("weeklyAverages", () => {
|
||||
it("returns empty for empty input", () => {
|
||||
expect(weeklyAverages([])).toEqual([]);
|
||||
});
|
||||
|
||||
it("groups data points by ISO week", () => {
|
||||
const data = makePoints(
|
||||
[70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83],
|
||||
14,
|
||||
);
|
||||
const result = weeklyAverages(data);
|
||||
// 14 days = 2-3 weeks
|
||||
expect(result.length).toBeGreaterThanOrEqual(2);
|
||||
expect(result.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
it("averages values within same week", () => {
|
||||
const now = new Date();
|
||||
// Find the Monday of current week
|
||||
const day = now.getDay();
|
||||
const monday = new Date(now);
|
||||
monday.setDate(now.getDate() - day + (day === 0 ? -6 : 1));
|
||||
monday.setHours(12, 0, 0, 0);
|
||||
|
||||
const tuesday = new Date(monday);
|
||||
tuesday.setDate(monday.getDate() + 1);
|
||||
|
||||
const data: DataPoint[] = [
|
||||
{ date: monday, value: 70 },
|
||||
{ date: tuesday, value: 80 },
|
||||
];
|
||||
const result = weeklyAverages(data);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].value).toBe(75);
|
||||
});
|
||||
|
||||
// v3 audit caught a TZ bug: a Sunday-evening Berlin reading (e.g. 22:00
|
||||
// local = 21:00 UTC in summer / 21:00 UTC in winter via DST) bucketed
|
||||
// into next week on UTC servers because `Date.getDay()` was system-local.
|
||||
// The Berlin-TZ-aware implementation puts the same point into THIS week.
|
||||
it("buckets Sunday-evening Berlin into the same ISO week as the preceding Monday", () => {
|
||||
// 2024-08-04 is a Sunday; 22:00 Berlin = 20:00 UTC (CEST). The Monday
|
||||
// of the same Berlin week is 2024-07-29.
|
||||
const sundayEveningBerlin = new Date("2024-08-04T20:00:00.000Z");
|
||||
const mondayMidday = new Date("2024-07-29T11:00:00.000Z");
|
||||
const result = weeklyAverages([
|
||||
{ date: mondayMidday, value: 70 },
|
||||
{ date: sundayEveningBerlin, value: 90 },
|
||||
]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].value).toBe(80); // (70 + 90) / 2
|
||||
expect(result[0].date.toISOString()).toBe("2024-07-29T00:00:00.000Z");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
movingAverage,
|
||||
trendSlope,
|
||||
detectAnomalies,
|
||||
summarize,
|
||||
@@ -15,27 +14,6 @@ function makePoints(values: number[], startDaysAgo = 30): DataPoint[] {
|
||||
}));
|
||||
}
|
||||
|
||||
describe("movingAverage", () => {
|
||||
it("returns empty for empty input", () => {
|
||||
expect(movingAverage([], 7)).toEqual([]);
|
||||
});
|
||||
|
||||
it("calculates moving average over window", () => {
|
||||
const data = makePoints([70, 71, 72, 73, 74, 75, 76], 7);
|
||||
const result = movingAverage(data, 3);
|
||||
expect(result).toHaveLength(7);
|
||||
// Last point should be avg of last 3 days
|
||||
expect(result[result.length - 1].value).toBeCloseTo(75, 0);
|
||||
});
|
||||
|
||||
it("handles single data point", () => {
|
||||
const data = makePoints([100], 1);
|
||||
const result = movingAverage(data, 7);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].value).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("trendSlope", () => {
|
||||
it("returns null for insufficient data", () => {
|
||||
expect(trendSlope([], 7)).toBeNull();
|
||||
|
||||
@@ -105,69 +105,3 @@ export function pearsonCorrelation(
|
||||
return { r, strength, n };
|
||||
}
|
||||
|
||||
const BERLIN_DATE_PARTS = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: "Europe/Berlin",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
weekday: "short",
|
||||
});
|
||||
|
||||
/**
|
||||
* Aggregate data into weekly averages, ISO-Monday-based, in Europe/Berlin.
|
||||
*
|
||||
* v3 audit caught a TZ bug here: the previous implementation used
|
||||
* `Date.getDay()` / `Date.getDate()` (system local) so on a UTC server, a
|
||||
* Sunday-evening Berlin reading bucketed into the next week. We now derive
|
||||
* year/month/day/weekday in Berlin via Intl.DateTimeFormat and rebuild the
|
||||
* Monday key from there. The returned `date` for each bucket is the Monday
|
||||
* UTC midnight — sortable, comparable across DST.
|
||||
*/
|
||||
export function weeklyAverages(data: DataPoint[]): DataPoint[] {
|
||||
if (data.length === 0) return [];
|
||||
|
||||
const sorted = [...data].sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
|
||||
const weeks = new Map<string, { sum: number; count: number; date: Date }>();
|
||||
|
||||
const WEEKDAY_INDEX: Record<string, number> = {
|
||||
Mon: 1,
|
||||
Tue: 2,
|
||||
Wed: 3,
|
||||
Thu: 4,
|
||||
Fri: 5,
|
||||
Sat: 6,
|
||||
Sun: 7,
|
||||
};
|
||||
|
||||
for (const point of sorted) {
|
||||
const parts = BERLIN_DATE_PARTS.formatToParts(point.date);
|
||||
const yearStr = parts.find((p) => p.type === "year")?.value ?? "1970";
|
||||
const monthStr = parts.find((p) => p.type === "month")?.value ?? "01";
|
||||
const dayStr = parts.find((p) => p.type === "day")?.value ?? "01";
|
||||
const weekdayStr = parts.find((p) => p.type === "weekday")?.value ?? "Mon";
|
||||
|
||||
const isoWeekday = WEEKDAY_INDEX[weekdayStr] ?? 1;
|
||||
// Days to subtract to land on Monday of the same ISO week in Berlin.
|
||||
const offsetDays = isoWeekday - 1;
|
||||
|
||||
const dayUtc = new Date(`${yearStr}-${monthStr}-${dayStr}T00:00:00.000Z`);
|
||||
const monday = new Date(dayUtc.getTime() - offsetDays * 86_400_000);
|
||||
const key = monday.toISOString().slice(0, 10);
|
||||
|
||||
const existing = weeks.get(key);
|
||||
if (existing) {
|
||||
existing.sum += point.value;
|
||||
existing.count++;
|
||||
} else {
|
||||
weeks.set(key, { sum: point.value, count: 1, date: monday });
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(weeks.values())
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
.map((w) => ({
|
||||
date: w.date,
|
||||
value: Math.round((w.sum / w.count) * 100) / 100,
|
||||
}));
|
||||
}
|
||||
@@ -8,33 +8,6 @@ export interface DataPoint {
|
||||
value: number;
|
||||
}
|
||||
|
||||
// ── Moving Average ───────────────────────────────────────
|
||||
|
||||
export function movingAverage(
|
||||
data: DataPoint[],
|
||||
windowDays: number,
|
||||
): DataPoint[] {
|
||||
if (data.length === 0) return [];
|
||||
|
||||
const sorted = [...data].sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
const result: DataPoint[] = [];
|
||||
const windowMs = windowDays * 24 * 60 * 60 * 1000;
|
||||
|
||||
for (const point of sorted) {
|
||||
const windowStart = point.date.getTime() - windowMs;
|
||||
const windowPoints = sorted.filter(
|
||||
(p) =>
|
||||
p.date.getTime() > windowStart &&
|
||||
p.date.getTime() <= point.date.getTime(),
|
||||
);
|
||||
const avg =
|
||||
windowPoints.reduce((sum, p) => sum + p.value, 0) / windowPoints.length;
|
||||
result.push({ date: point.date, value: Math.round(avg * 100) / 100 });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Trend Slope ──────────────────────────────────────────
|
||||
|
||||
export interface TrendSlope {
|
||||
|
||||
Reference in new issue
Block a user