// Plain-language summary of today's forecast ("Scattered showers today", // "Rain likely later", "Don't forget your umbrella"...). Neither Met Office's Global // Spot API nor Open-Meteo return anything like this — both are purely numeric/coded // (temps, weather codes, precip%) — so this is generated from today's already-fetched // hourly points (icon + precip_prob), rule-based, no extra API call and no LLM. // // Computed fresh per-request in routes/forecast.js (not stored/cached at fetch time), // since "later" is relative to whoever's looking at the screen right now, not to when // the hourly data was last fetched. const RAIN_ICONS = new Set(['drizzle', 'rain-light', 'rain-heavy', 'sleet', 'hail']) const SNOW_ICONS = new Set(['snow-light', 'snow-heavy']) // summarizeToday(points, nowHour) — `points` is today's hourly forecast_points rows // (already icon-normalized), `nowHour` is 0-23 in Europe/London. Returns null if // there's nothing to summarize (no data yet). export function summarizeToday(points, nowHour) { if (!points.length) return null const isRain = p => RAIN_ICONS.has(p.icon) const isSnow = p => SNOW_ICONS.has(p.icon) const isThunder = p => p.icon === 'thunder' const isHeavyRain = p => p.icon === 'rain-heavy' const rainPoints = points.filter(isRain) const snowPoints = points.filter(isSnow) const thunderPoints = points.filter(isThunder) const rainFraction = rainPoints.length / points.length const snowFraction = snowPoints.length / points.length const laterPoints = points.filter(p => hourOf(p) >= nowHour) const earlierPoints = points.filter(p => hourOf(p) < nowHour) const laterWetFraction = laterPoints.length ? laterPoints.filter(p => isRain(p) || isThunder(p)).length / laterPoints.length : 0 const earlierWetCount = earlierPoints.filter(p => isRain(p) || isThunder(p)).length let headline if (snowFraction > 0.3) { headline = 'Snow expected today' } else if (rainPoints.length === 0 && thunderPoints.length === 0) { headline = 'Dry today' } else if (rainPoints.some(isHeavyRain) && rainFraction > 0.5) { headline = 'Heavy rain today' } else if (rainFraction > 0.6) { headline = 'Rain likely for much of the day' } else if (laterWetFraction > 0.4 && earlierWetCount === 0) { headline = 'Rain likely later' } else if (rainFraction > 0) { headline = 'Scattered showers today' } else { headline = 'Thunderstorms possible' } // Append rather than replace the headline when thunder shows up alongside a // rain pattern that already earned its own headline above. const note = thunderPoints.length > 0 && headline !== 'Thunderstorms possible' ? 'Thunderstorms possible' : null // Umbrella nudge: real rain chance either right now or coming up later today. const umbrella = laterWetFraction > 0.25 || rainFraction > 0.25 return { headline, note, umbrella } } function hourOf(point) { return parseInt(new Date(point.ts).toLocaleString('en-GB', { timeZone: 'Europe/London', hour: '2-digit', hour12: false })) }