/** * earth-now.co * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: apps/ingest/src/sources/noaa-co2.ts * Purpose: NOAA GML Mauna Loa monthly CO₂ (co2_mm_mlo.txt) — pure text parser and fetcher (Keeling curve) */ import { type FetchLike, defaultFetch } from "../fetch-like.js"; export const NOAA_CO2_URL = "https://gml.noaa.gov/webdata/ccgg/trends/co2/co2_mm_mlo.txt"; /** NOAA marks missing monthly means with -9.99 (and -1 / -9.99 in the auxiliary columns). */ export const NOAA_MISSING_SENTINEL = -9.99; export interface MonthlyObservation { /** ISO 8601 UTC, pinned to the 15th of the month (monthly means have no finer resolution). */ time: string; /** CO₂ mole fraction in dry air, ppm. */ value: number; } /** * Pure parser for the NOAA GML co2_mm_mlo.txt format: * - comment lines start with '#' * - data lines: year month decimal-date average deseasonalized ndays stdev unc (whitespace-separated) * - average == -9.99 → missing month, skipped. * Chosen degraded-case behavior: a truncated/malformed data line THROWS a descriptive error * (a partial file must never silently feed the fitting pipeline). */ export function parseNoaaMonthlyCo2(text: string): MonthlyObservation[] { const out: MonthlyObservation[] = []; const lines = text.split(/\r?\n/); for (let i = 0; i < lines.length; i++) { const line = (lines[i] ?? "").trim(); if (line === "" || line.startsWith("#")) continue; const fields = line.split(/\s+/); if (fields.length < 8) throw new Error( `NOAA co2_mm_mlo parse error at line ${i + 1}: expected 8 columns ` + `(year month decimal-date average deseasonalized ndays stdev unc), got ${fields.length} ` + `— file truncated or format changed`, ); const year = Number(fields[0]); const month = Number(fields[1]); const average = Number(fields[3]); if (!Number.isInteger(year) || year < 1950 || year > 2200) throw new Error(`NOAA co2_mm_mlo parse error at line ${i + 1}: invalid year '${fields[0]}'`); if (!Number.isInteger(month) || month < 1 || month > 12) throw new Error(`NOAA co2_mm_mlo parse error at line ${i + 1}: invalid month '${fields[1]}'`); if (!Number.isFinite(average)) throw new Error( `NOAA co2_mm_mlo parse error at line ${i + 1}: non-numeric average '${fields[3]}'`, ); // Missing month: -9.99 sentinel (any negative average is physically impossible for CO₂ ppm). if (average < 0) continue; out.push({ time: new Date(Date.UTC(year, month - 1, 15)).toISOString(), value: average }); } if (out.length === 0) throw new Error("NOAA co2_mm_mlo parse error: no valid data rows found in payload"); return out; } /** Fetch and parse the current Mauna Loa monthly series. Inject fetchImpl in tests. */ export async function fetchNoaaCo2( fetchImpl: FetchLike = defaultFetch, ): Promise { const res = await fetchImpl(NOAA_CO2_URL); if (!res.ok) throw new Error(`NOAA co2_mm_mlo fetch failed: HTTP ${res.status}`); return parseNoaaMonthlyCo2(await res.text()); }