SPB Git

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%
3.1 KB · 72 lines typescript
Raw Blame History
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