SPB Git

spb/earth-now Public License

earth-now.co — real-time planetary dashboard: live world metrics modeled, not streamed.

TypeScript 93% Shell 2.3% SQL 1.4% JavaScript 1.3% Dockerfile 1.2% CSS 0.8%
5.6 KB · 184 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    packages/counter/test/value.test.ts6 * Purpose: Golden tests for counterValue/rateAt across all RateFunction kinds at precise instants t7 */89import { describe, expect, it } from "vitest";10import {11  type CounterModel,12  counterValue,13  rateAt,14  startOfUtcDay,15  startOfUtcYear,16  windowValue,17  YEAR_SECONDS,18  SEASONAL_EPOCH_MS,19} from "../src/index";2021const T0 = "2026-01-01T00:00:00.000Z";22const t0 = Date.parse(T0);2324function base(over: Partial<CounterModel>): CounterModel {25  return {26    metricId: "test_metric",27    anchorValue: 0,28    anchorTime: T0,29    rateFn: { kind: "linear", perSecond: 1 },30    observedAt: T0,31    sourceId: "test_source",32    modelVersion: "test-v1",33    displayHints: { decimals: 0, unit: "units" },34    ...over,35  };36}3738describe("linear", () => {39  it("value(anchorTime) === anchorValue", () => {40    const m = base({ anchorValue: 42 });41    expect(counterValue(m, t0)).toBe(42);42  });4344  it("advances by perSecond", () => {45    const m = base({ rateFn: { kind: "linear", perSecond: 2.5 } });46    expect(counterValue(m, t0 + 10_000)).toBeCloseTo(25, 10);47    expect(counterValue(m, t0 - 4_000)).toBeCloseTo(-10, 10);48    expect(rateAt(m, t0)).toBe(2.5);49  });50});5152describe("piecewise", () => {53  const m = base({54    anchorValue: 100,55    rateFn: {56      kind: "piecewise",57      segments: [58        { from: "2026-01-01T00:00:00.000Z", perSecond: 1 },59        { from: "2026-01-01T00:01:00.000Z", perSecond: 3 },60      ],61    },62  });6364  it("integrates across the junction", () => {65    // 60 s at 1/s + 30 s at 3/s = 60 + 90.66    expect(counterValue(m, t0 + 90_000)).toBeCloseTo(250, 10);67  });6869  it("is continuous at the junction", () => {70    const eps = 1;71    const before = counterValue(m, t0 + 60_000 - eps);72    const after = counterValue(m, t0 + 60_000 + eps);73    expect(Math.abs(after - before)).toBeLessThan(0.01);74  });7576  it("reports the active segment's rate", () => {77    expect(rateAt(m, t0 + 30_000)).toBe(1);78    expect(rateAt(m, t0 + 61_000)).toBe(3);79  });80});8182describe("seasonal", () => {83  const m = base({84    anchorValue: 1000,85    rateFn: {86      kind: "seasonal",87      base: 2,88      harmonics: [{ period: "year", order: 1, amplitude: 1, phase: 0 }],89    },90  });9192  it("value(anchorTime) === anchorValue", () => {93    expect(counterValue(m, t0)).toBeCloseTo(1000, 9);94  });9596  it("harmonic integrates to ~zero over a full year (only base remains)", () => {97    const oneYearMs = YEAR_SECONDS * 1000;98    const v = counterValue(m, t0 + oneYearMs);99    expect(v).toBeCloseTo(1000 + 2 * YEAR_SECONDS, 4);100  });101102  it("rate oscillates around base with the harmonic amplitude", () => {103    // At the seasonal epoch itself, cos(0 + 0) = 1 → rate = base + amplitude.104    expect(rateAt(m, SEASONAL_EPOCH_MS)).toBeCloseTo(3, 9);105  });106});107108describe("seasonal week period", () => {109  const m = base({110    rateFn: {111      kind: "seasonal",112      base: 10,113      harmonics: [{ period: "week", order: 1, amplitude: 2, phase: 0 }],114    },115  });116117  it("weekly harmonic integrates to ~zero over a full week", () => {118    const oneWeekMs = 604_800_000;119    expect(counterValue(m, t0 + oneWeekMs)).toBeCloseTo(10 * 604_800, 4);120  });121122  it("weekly phase is anchored to Saturday 00:00 UTC (the 2000-01-01 epoch)", () => {123    // 2026-01-03 is a Saturday: cos(0) = 1 → rate = base + amplitude.124    expect(rateAt(m, Date.parse("2026-01-03T00:00:00Z"))).toBeCloseTo(12, 9);125    // Half a week later (Tue 12:00): cos(π) = −1 → base − amplitude.126    expect(rateAt(m, Date.parse("2026-01-06T12:00:00Z"))).toBeCloseTo(8, 9);127  });128});129130describe("spline (PCHIP)", () => {131  const m = base({132    anchorValue: 20,133    anchorTime: "2026-01-02T00:00:00.000Z",134    rateFn: {135      kind: "spline",136      knots: [137        ["2026-01-01T00:00:00.000Z", 10],138        ["2026-01-02T00:00:00.000Z", 20],139        ["2026-01-03T00:00:00.000Z", 40],140        ["2026-01-04T00:00:00.000Z", 45],141      ],142    },143  });144145  it("interpolates exactly at knots and anchor", () => {146    expect(counterValue(m, Date.parse("2026-01-01T00:00:00Z"))).toBeCloseTo(10, 9);147    expect(counterValue(m, Date.parse("2026-01-02T00:00:00Z"))).toBeCloseTo(20, 9);148    expect(counterValue(m, Date.parse("2026-01-04T00:00:00Z"))).toBeCloseTo(45, 9);149  });150151  it("never overshoots between monotone knots", () => {152    const a = Date.parse("2026-01-01T00:00:00Z");153    const b = Date.parse("2026-01-04T00:00:00Z");154    for (let i = 0; i <= 200; i++) {155      const v = counterValue(m, a + ((b - a) * i) / 200);156      expect(v).toBeGreaterThanOrEqual(10 - 1e-9);157      expect(v).toBeLessThanOrEqual(45 + 1e-9);158    }159  });160161  it("extrapolates linearly beyond the last knot", () => {162    const end = Date.parse("2026-01-04T00:00:00Z");163    const slopeEnd = rateAt(m, end);164    const v = counterValue(m, end + 3_600_000);165    expect(v).toBeCloseTo(45 + slopeEnd * 3600, 6);166  });167});168169describe("windows", () => {170  it("today/ytd windows subtract the model value at window start", () => {171    const m = base({ rateFn: { kind: "linear", perSecond: 1 } });172    const t = Date.parse("2026-03-10T06:00:00Z");173    expect(windowValue(m, t, "today")).toBeCloseTo(6 * 3600, 6);174    expect(windowValue(m, t, "ytd")).toBeCloseTo((t - t0) / 1000, 6);175    expect(windowValue(m, t, "session", t - 90_000)).toBeCloseTo(90, 9);176  });177178  it("UTC boundaries are correct", () => {179    const t = Date.parse("2026-03-10T06:07:08Z");180    expect(new Date(startOfUtcDay(t)).toISOString()).toBe("2026-03-10T00:00:00.000Z");181    expect(new Date(startOfUtcYear(t)).toISOString()).toBe("2026-01-01T00:00:00.000Z");182  });183});184