diff --git a/app/src/util/series.ts b/app/src/util/series.ts new file mode 100644 index 000000000..02453bc98 --- /dev/null +++ b/app/src/util/series.ts @@ -0,0 +1,46 @@ +export function estimate(series: { year: number, value: number }[], year: number): number | null { + + // if it's in the series, just return the value + + const exact = series.find(e => e.year === year); + if (exact) { + return exact.value; + } + + // we need at least two points to interpolate or extrapolate + + if (series.length < 2) { + return null; + } + + if (year < series[0].year) { // extrapolate backwards + let first = series[0] + let next = series.find(e => e.year - first.year >= first.year - year) + if (!next) { + return null + } + let rate = (next.value - first.value) / (next.year - first.year); + let value = first.value - rate * (first.year - year) + return value + } else if (year > series[series.length - 1].year) { // extrapolate forwards + let last = series[series.length - 1] + let prev = series.findLast(e => last.year - e.year >= year - last.year) + if (!prev) { + return null + } + let rate = (last.value - prev.value) / (last.year - prev.year); + let value = last.value + rate * (year - last.year) + return value + } else { // interpolate + let prev = series.findLast(e => e.year < year) + let next = series.find(e => e.year > year) + if (!prev || !next) { + return null + } + let rate = (next.value - prev.value) / (next.year - prev.year); + let value = prev.value + rate * (year - prev.year) + return value + } + + return null; +} \ No newline at end of file diff --git a/app/tests/series.test.ts b/app/tests/series.test.ts new file mode 100644 index 000000000..76da43a60 --- /dev/null +++ b/app/tests/series.test.ts @@ -0,0 +1,60 @@ +import { describe, it } from "node:test" +import assert from "node:assert/strict" +import { estimate } from "@/util/series" + +describe("Series", () => { + let series0 = [ + { year: 2000, value: 100 } + ]; + + let series = [ + { year: 2000, value: 100 }, + { year: 2010, value: 200 }, + { year: 2020, value: 400 } + ]; + + it("should return exact value for small series on match", () => { + let result = estimate(series0, 2000); + assert.strictEqual(result, 100); + }) + + it("should return null for small series on no match", () => { + let result = estimate(series0, 2001); + assert.strictEqual(result, null); + }) + + it("should return exact value", () => { + let result = estimate(series, 2010); + assert.strictEqual(result, 200); + }) + + it("should return interpolated value at low rate", () => { + let result = estimate(series, 2005); + assert.strictEqual(result, 150); + }) + + it("should return interpolated value at high rate", () => { + let result = estimate(series, 2015); + assert.strictEqual(result, 300); + }) + + it("should return extrapolated value at low rate", () => { + let result = estimate(series, 1995); + assert.strictEqual(result, 50); + }) + + it("should return extrapolated value at high rate", () => { + let result = estimate(series, 2025); + assert.strictEqual(result, 500); + }) + + it("should return null if below safe extrapolation range", () => { + let result = estimate(series, 1979); + assert.strictEqual(result, null); + }) + + it("should return null if above safe extrapolation range", () => { + let result = estimate(series, 2041); + assert.strictEqual(result, null); + }) +}) \ No newline at end of file