diff --git a/src/pure.js b/src/pure.js index 773696a..6be6ccd 100644 --- a/src/pure.js +++ b/src/pure.js @@ -65,3 +65,41 @@ export function pruneHistory(entries, now = Date.now()) { const cutoff = now - THIRTY_DAYS_MS; return entries.filter((e) => e.ts > cutoff); } + +/** + * Summarise training history. + * + * @param {{ts:number, delta:number}[]} entries + * @param {number} [now] Reference time in ms (defaults to Date.now()). + * @returns {{trainsToday:number, sevenDayAvgPerDay:number, perDay:number}} + */ +export function summary(entries, now = Date.now()) { + if (entries.length === 0) { + return { trainsToday: 0, sevenDayAvgPerDay: 0, perDay: 0 }; + } + + const ONE_DAY = MS_PER_DAY; + const SEVEN_DAYS = 7 * MS_PER_DAY; + + const todayCutoff = now - ONE_DAY; + const weekCutoff = now - SEVEN_DAYS; + + let trainsToday = 0; + let trainsWeek = 0; + let latestDelta = 0; + let latestTs = -Infinity; + + for (const e of entries) { + if (e.ts >= todayCutoff) trainsToday++; + if (e.ts >= weekCutoff) trainsWeek++; + if (e.ts > latestTs) { + latestTs = e.ts; + latestDelta = e.delta; + } + } + + const sevenDayAvgPerDay = trainsWeek / 7; + const perDay = latestDelta > 0 ? Math.floor(sevenDayAvgPerDay * latestDelta) : 0; + + return { trainsToday, sevenDayAvgPerDay, perDay }; +} diff --git a/tests/pure.test.js b/tests/pure.test.js index ba75bb7..a07060d 100644 --- a/tests/pure.test.js +++ b/tests/pure.test.js @@ -1,6 +1,6 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { parseTarget, computeEstimate, pruneHistory, MS_PER_DAY } from '../src/pure.js'; +import { parseTarget, computeEstimate, pruneHistory, summary, MS_PER_DAY } from '../src/pure.js'; const DAY = MS_PER_DAY; const NOW = 1_700_000_000_000; // fixed reference @@ -94,3 +94,48 @@ test('pruneHistory: does not mutate input', () => { test('pruneHistory: empty input', () => { assert.deepEqual(pruneHistory([], NOW), []); }); + +test('summary: empty history returns zeros', () => { + const s = summary([], NOW); + assert.equal(s.trainsToday, 0); + assert.equal(s.sevenDayAvgPerDay, 0); + assert.equal(s.perDay, 0); +}); + +test('summary: counts trains within last 24h', () => { + const entries = [ + { ts: NOW - 1 * 60_000, delta: 100 }, // 1 min ago + { ts: NOW - 12 * 3_600_000, delta: 100 }, // 12h ago + { ts: NOW - 23 * 3_600_000, delta: 100 }, // 23h ago + { ts: NOW - 25 * 3_600_000, delta: 100 }, // 25h ago (not today) + ]; + const s = summary(entries, NOW); + assert.equal(s.trainsToday, 3); +}); + +test('summary: 7-day average is total/7', () => { + // 14 entries in last 7 days (spaced 12h apart, all within window) + const entries = []; + for (let i = 0; i < 14; i++) { + entries.push({ ts: NOW - (i / 2) * DAY, delta: 100 }); + } + const s = summary(entries, NOW); + assert.equal(s.sevenDayAvgPerDay, 2); +}); + +test('summary: perDay uses most recent delta when available', () => { + const entries = [ + { ts: NOW - 1 * 60_000, delta: 247 }, + { ts: NOW - 12 * 3_600_000, delta: 300 }, + ]; + const s = summary(entries, NOW); + // 2 trains in last 7 days → 2/7 avg + // perDay = avg * most recent delta = (2/7) * 247 = ~70.57 + assert.equal(s.perDay, Math.floor((2 / 7) * 247)); +}); + +test('summary: perDay is 0 when no recent delta', () => { + const entries = [{ ts: NOW - 31 * DAY, delta: 100 }]; + const s = summary(entries, NOW); + assert.equal(s.perDay, 0); +});