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%
1/**2 * earth-now.co3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: apps/ingest/src/sources/noaa-co2.ts6 * Purpose: NOAA GML Mauna Loa monthly CO₂ (co2_mm_mlo.txt) — pure text parser and fetcher (Keeling curve)7 */89import { type FetchLike, defaultFetch } from "../fetch-like.js";1011export const NOAA_CO2_URL = "https://gml.noaa.gov/webdata/ccgg/trends/co2/co2_mm_mlo.txt";1213/** NOAA marks missing monthly means with -9.99 (and -1 / -9.99 in the auxiliary columns). */14export const NOAA_MISSING_SENTINEL = -9.99;1516export interface MonthlyObservation {17 /** ISO 8601 UTC, pinned to the 15th of the month (monthly means have no finer resolution). */18 time: string;19 /** CO₂ mole fraction in dry air, ppm. */20 value: number;21}2223/**24 * Pure parser for the NOAA GML co2_mm_mlo.txt format:25 * - comment lines start with '#'26 * - data lines: year month decimal-date average deseasonalized ndays stdev unc (whitespace-separated)27 * - average == -9.99 → missing month, skipped.28 * Chosen degraded-case behavior: a truncated/malformed data line THROWS a descriptive error29 * (a partial file must never silently feed the fitting pipeline).30 */31export function parseNoaaMonthlyCo2(text: string): MonthlyObservation[] {32 const out: MonthlyObservation[] = [];33 const lines = text.split(/\r?\n/);34 for (let i = 0; i < lines.length; i++) {35 const line = (lines[i] ?? "").trim();36 if (line === "" || line.startsWith("#")) continue;37 const fields = line.split(/\s+/);38 if (fields.length < 8)39 throw new Error(40 `NOAA co2_mm_mlo parse error at line ${i + 1}: expected 8 columns ` +41 `(year month decimal-date average deseasonalized ndays stdev unc), got ${fields.length} ` +42 `— file truncated or format changed`,43 );44 const year = Number(fields[0]);45 const month = Number(fields[1]);46 const average = Number(fields[3]);47 if (!Number.isInteger(year) || year < 1950 || year > 2200)48 throw new Error(`NOAA co2_mm_mlo parse error at line ${i + 1}: invalid year '${fields[0]}'`);49 if (!Number.isInteger(month) || month < 1 || month > 12)50 throw new Error(`NOAA co2_mm_mlo parse error at line ${i + 1}: invalid month '${fields[1]}'`);51 if (!Number.isFinite(average))52 throw new Error(53 `NOAA co2_mm_mlo parse error at line ${i + 1}: non-numeric average '${fields[3]}'`,54 );55 // Missing month: -9.99 sentinel (any negative average is physically impossible for CO₂ ppm).56 if (average < 0) continue;57 out.push({ time: new Date(Date.UTC(year, month - 1, 15)).toISOString(), value: average });58 }59 if (out.length === 0)60 throw new Error("NOAA co2_mm_mlo parse error: no valid data rows found in payload");61 return out;62}6364/** Fetch and parse the current Mauna Loa monthly series. Inject fetchImpl in tests. */65export async function fetchNoaaCo2(66 fetchImpl: FetchLike = defaultFetch,67): Promise<MonthlyObservation[]> {68 const res = await fetchImpl(NOAA_CO2_URL);69 if (!res.ok) throw new Error(`NOAA co2_mm_mlo fetch failed: HTTP ${res.status}`);70 return parseNoaaMonthlyCo2(await res.text());71}72