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:
Marc-André Bombeck committed 2026-05-21 12:04:07 +02:00
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();
-66
View File
@@ -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,
}));
}
-27
View File
@@ -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 {