From 27423d3092a5dee6ff9861deaa86d761736c6ec2 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 15 Apr 2024 09:22:50 -0400 Subject: [PATCH 1/2] feat: function for estimating value from a series Signed-off-by: Evan Prodromou --- app/src/util/series.ts | 46 ++++++++++++++++++++++++++++++ app/tests/series.test.ts | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 app/src/util/series.ts create mode 100644 app/tests/series.test.ts diff --git a/app/src/util/series.ts b/app/src/util/series.ts new file mode 100644 index 000000000..4f53ea722 --- /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 = (1.0 * (next.value - first.value)) / (next.year - first.year); + let value = first.value - rate * (first.year - year) + return Math.round(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 = (1.0 * (last.value - prev.value)) / (last.year - prev.year); + let value = last.value + rate * (year - last.year) + return Math.round(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 = (1.0 * (next.value - prev.value)) / (next.year - prev.year); + let value = prev.value + rate * (year - prev.year) + return Math.round(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 From 5e1619e3f13e71f4b112d29eed9063ad031c6e59 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 16 Apr 2024 09:15:29 -0400 Subject: [PATCH 2/2] fix: remove unnecessary casts, rounding from series.estimate Signed-off-by: Evan Prodromou --- app/src/util/series.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/util/series.ts b/app/src/util/series.ts index 4f53ea722..02453bc98 100644 --- a/app/src/util/series.ts +++ b/app/src/util/series.ts @@ -19,27 +19,27 @@ export function estimate(series: { year: number, value: number }[], year: number if (!next) { return null } - let rate = (1.0 * (next.value - first.value)) / (next.year - first.year); + let rate = (next.value - first.value) / (next.year - first.year); let value = first.value - rate * (first.year - year) - return Math.round(value) + 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 = (1.0 * (last.value - prev.value)) / (last.year - prev.year); + let rate = (last.value - prev.value) / (last.year - prev.year); let value = last.value + rate * (year - last.year) - return Math.round(value) + 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 = (1.0 * (next.value - prev.value)) / (next.year - prev.year); + let rate = (next.value - prev.value) / (next.year - prev.year); let value = prev.value + rate * (year - prev.year) - return Math.round(value) + return value } return null;