open-navigator / web_app /src /utils /inflation.ts
jcbowyer's picture
Clean HuggingFace deployment without binary files
e59d91d
Raw
History Blame Contribute Delete
2.9 kB
/**
* Inflation deflator β€” converts nominal dollars from one year into constant
* dollars of another year using an annual CPI index map.
*
* Math: ``value_in_target_year_dollars = value * (cpi[target] / cpi[from])``.
*
* One national CPI series is applied uniformly to every geography β€” see
* ``api/routes/cpi.py`` and the design note in
* ``scripts/datasources/bls/load_bls_cpi.py``: deflating each place by a
* local CPI would bake regional inflation into the yardstick and break
* cross-place real-dollar comparisons. One tape measure for everyone.
*/
export type CpiByYear = Record<string, number>
/** Deflate ``value`` from ``fromYear`` dollars into ``toYear`` dollars.
* Returns ``null`` if either CPI reading is missing or zero β€” callers
* should fall back to nominal in that case rather than show a misleading
* number. */
export function deflate(
value: number | null | undefined,
fromYear: number | string | null | undefined,
toYear: number | string | null | undefined,
cpi: CpiByYear | null | undefined,
): number | null {
if (value == null || !Number.isFinite(value as number)) return null
if (fromYear == null || toYear == null || !cpi) return null
const fromIdx = cpi[String(fromYear)]
const toIdx = cpi[String(toYear)]
if (
typeof fromIdx !== 'number' ||
!Number.isFinite(fromIdx) ||
fromIdx === 0 ||
typeof toIdx !== 'number' ||
!Number.isFinite(toIdx)
)
return null
// Same year is a no-op (cpi[from]==cpi[to], ratio = 1) β€” but spelling it
// out avoids a needless float multiply and the resulting trailing-digit
// jitter when the headline value should be visually unchanged.
if (String(fromYear) === String(toYear)) return value as number
return (value as number) * (toIdx / fromIdx)
}
/** Year in ``points`` with the maximum ``y``, ``null`` if the series is
* empty or the peak coincides with the latest displayed year (in which
* case "peaked in YYYY" is not interesting β€” the value IS the peak). */
export function peakYearOf(
points: { x: number; y: number }[],
latestDisplayedYear: number | string | null | undefined,
): number | null {
if (!points.length) return null
let peakX = points[0]!.x
let peakY = points[0]!.y
for (const p of points) {
if (p.y > peakY) {
peakY = p.y
peakX = p.x
}
}
if (latestDisplayedYear != null && String(peakX) === String(latestDisplayedYear)) {
return null
}
return peakX
}
/** Census metric slugs whose values are dollars and therefore subject to
* the Nominal / Real toggle. Anything not listed renders nominal-only. */
const DOLLAR_METRIC_SLUGS = new Set<string>([
'median_home_value',
'median_household_income',
'median_gross_rent',
'per_capita_income',
])
export function isDollarMetric(slug: string | null | undefined): boolean {
if (!slug) return false
return DOLLAR_METRIC_SLUGS.has(slug)
}