/** * Utility functions for computing climate normals from historical data * * Climate normals are 30-year averages (1991-2020) used to provide * context for current weather conditions. * * Hybrid Strategy: * 1. Try NCEI (US only, requires token) for official normals * 2. Fall back to Open-Meteo (global, no token) computed normals */ import type { OpenMeteoHistoricalResponse, ClimateNormals } from '../types/openmeteo.js'; import { UnitPreferences } from '../config/units.js'; import type { OpenMeteoService } from '../services/openmeteo.js'; import type { NCEIService } from '../services/ncei.js'; /** * One slot of the 366-slot climate-normals table. Values are **unrounded**, * canonical imperial (°F, inches) — D5 moves all rounding to render time, so * these are the raw converted means, not the display-ready numbers * `formatNormals`/`normalTempToPref`/`normalPrecipToPref` produce. * * `sampleCount` is the number of samples the slot's mean was computed from — * specifically the minimum count across the three variables, since a slot is * available only when *every* variable clears its minimum (A1, all-or-nothing: * `ClimateNormals` has no partial-data shape), so the minimum is the binding * constraint. */ export interface NormalsTableSlot { tempHigh: number; tempLow: number; precipitation: number; sampleCount: number; } /** * A full climate-normals table, keyed `"MM-DD"` (zero-padded, e.g. `"01-05"`, * `"02-29"`). `computeNormalsTable` always populates all 366 keys (using a * leap-year calendar so `"02-29"` exists), so a valid key's value is always * one of `NormalsTableSlot | null` — `null` marks an explicitly unavailable * slot (too few qualifying samples), distinguishable from an absent/mistyped * key, which would read `undefined` and never occurs for a real `"MM-DD"`. */ export type NormalsTable = Record; /** * Compute a full 366-slot climate-normals table from one full-year * (1991-01-01…2020-12-31) archive response, per D1/D2/D5. * * A single pass buckets each day's samples by its `"MM-DD"` key; each * variable's mean uses only its own non-null, non-undefined samples (a day * with a null precipitation reading still contributes its temperature * samples). A slot is unavailable (`null`) when *any* of the three * variables has fewer than `NORMALS_MIN_SAMPLES` (15) qualifying samples — * except `"02-29"`, whose minimum is `NORMALS_MIN_SAMPLES_FEB29` (6). * Available slots store unrounded canonical-imperial floats (°F/inches); * rounding happens only at render time (D5). * * A response with no `daily`/`daily.time` returns the all-unavailable table * defensively (the open-ocean HTTP-200-with-nulls precedent extended to a * missing/malformed payload) rather than throwing — the table is garnish * data indexed per-date downstream, and an all-null table degrades the same * way a partially-null one does. */ export declare function computeNormalsTable(response: OpenMeteoHistoricalResponse): NormalsTable; /** * Generate the per-location cache key for a full normals table (D1) — one * entry per location instead of up to 366 per-date keys. * * @param latitude - Latitude (rounded to 2 decimals) * @param longitude - Longitude (rounded to 2 decimals) * @returns Cache key string */ export declare function getNormalsTableCacheKey(latitude: number, longitude: number): string; /** * Calculate departure from normal * * @param actual - Actual temperature value * @param normal - Normal (average) temperature value * @returns Departure with sign (e.g., +10, -5) */ export declare function calculateDeparture(actual: number, normal: number): string; /** * Format climate normals for display * * @param normals - Climate normals data * @param currentTemp - Optional current temperature for comparison * @returns Formatted markdown string */ export declare function formatNormals(normals: ClimateNormals, currentTemp?: { high?: number; low?: number; }, prefs?: UnitPreferences): string; /** * Render the climate-normals section for a location and date, or the * unavailable note if the normals can't be had (D6). * * This owns the try/catch, the `getClimateNormals` call, and both outcomes — * the five handler blocks that used to duplicate it (two in * `forecastHandler`, three in `currentConditionsHandler`) differ only in how * they derive `month`/`day` and `currentTemps`, so that derivation stays with * them and everything downstream of it lives here. * * Normals are garnish: this function never throws, so a failure renders the * note and leaves the parent forecast/current response intact. * * @param currentTemps - Actual/forecast high and low **already in the * caller's units**, for the departure lines. Pass `{}` where no comparable * temperatures exist (the METAR path). * @returns The section markdown, ready to append to the handler's output */ export declare function renderNormalsSection(openMeteoService: OpenMeteoService, nceiService: NCEIService | undefined, latitude: number, longitude: number, month: number, day: number, currentTemps: { high?: number; low?: number; }, prefs?: UnitPreferences): Promise; /** * Get date components from Date object or ISO string * * @param date - Date object or ISO string * @returns Object with month (1-12) and day (1-31) */ export declare function getDateComponents(date: Date | string): { month: number; day: number; }; /** * Get climate normals using hybrid strategy * * Strategy: * 1. If location is in US and NCEI token available, try NCEI first * 2. Fall back to Open-Meteo computed normals (always works) * * @param openMeteoService - Open-Meteo service instance * @param nceiService - NCEI service instance (optional) * @param latitude - Latitude * @param longitude - Longitude * @param month - Month (1-12) * @param day - Day of month (1-31) * @returns Climate normals with source indication */ export declare function getClimateNormals(openMeteoService: OpenMeteoService, nceiService: NCEIService | undefined, latitude: number, longitude: number, month: number, day: number): Promise; //# sourceMappingURL=normals.d.ts.map