Weight NWS weather-code mapping by precipitation probability
NWS text like 'Slight Chance Showers And Thunderstorms' (~17-24%) was mapping straight to WMO 95 / thunderstorm icon, over-stating a mere chance of storms. shortForecastToWmo now takes the period's precip probability (plus chance-like wording) and downgrades low-chance storms to light showers (80) and low-chance rain to light rain (61), keeping real/severe storms at 95. This also stops the false thunderstorm alert banner for low-chance days. Today/Monday for Rogers County now render as showers instead of storms.
This commit is contained in:
parent
32f749444e
commit
ebe81b6bfa
@ -57,14 +57,33 @@ export function parseWind(s) {
|
||||
return Math.max(...nums.map(Number))
|
||||
}
|
||||
|
||||
/** Map an NWS shortForecast string to a WMO code for icons/severity. */
|
||||
export function shortForecastToWmo(text) {
|
||||
/**
|
||||
* Map an NWS shortForecast string to a WMO code for icons/severity.
|
||||
* @param {string} text - NWS shortForecast text, e.g. "Slight Chance Showers And Thunderstorms"
|
||||
* @param {number|null} prob - precipitation probability (0-100) for that period,
|
||||
* so low-chance wording does not render as a full weather condition.
|
||||
*/
|
||||
export function shortForecastToWmo(text, prob = null) {
|
||||
const t = (text || '').toLowerCase()
|
||||
if (/thunderstorm|tstm|severe thunder/.test(t)) return /\bsevere\b/.test(t) ? 96 : 95
|
||||
if (/freez|x-ice|ice pellets/.test(t) && /rain|drizzle|shower/.test(t)) return 66
|
||||
if (/snow|blizzard|flurr|sleet|snow shower/.test(t)) return /heavy|blizzard/.test(t) ? 75 : 71
|
||||
if (/freez|ice/.test(t)) return 66
|
||||
if (/rain|shower|drizzle|sprinkl/.test(t)) return /heavy|strong|torrential/.test(t) ? 65 : /slight|isolated|scattered/.test(t) ? 61 : 63
|
||||
const chanceLike = () =>
|
||||
(prob != null && prob < 30) ||
|
||||
/\bslight chance\b|chance of|scattered|isolated|fewer chance|\bchance\b/.test(t)
|
||||
|
||||
if (/thunderstorm|tstm/.test(t)) {
|
||||
// An actual severe/thunderstorm is a real condition; a mere *chance* of
|
||||
// one is just light showers until something organises.
|
||||
const severe = /\bsevere\b|\bstrong\b|warning|watch/.test(t)
|
||||
if (severe) return 95
|
||||
if (chanceLike()) return 80 // light rain showers, not a full thunderstorm
|
||||
return 95
|
||||
}
|
||||
if (/freez|x-ice|ice pellets|freezing/.test(t) && /rain|drizzle|shower/.test(t)) return 66
|
||||
if (/snow|blizzard|flurr|snow shower/.test(t)) return /heavy|blizzard/.test(t) ? 75 : 71
|
||||
if (/rain|shower|drizzle|sprinkl/.test(t)) {
|
||||
if (/heavy|strong|torrential/.test(t)) return 65
|
||||
if (chanceLike()) return 61
|
||||
return 63
|
||||
}
|
||||
if (/fog|haze|smoke|brume/.test(t)) return 45
|
||||
if (/partly (cloudy|sunny)/.test(t)) return 2
|
||||
if (/overcast|covering|cloudy/.test(t)) return 3
|
||||
@ -123,7 +142,7 @@ function buildDaily(periods, tz, units) {
|
||||
else d.lows.push(c)
|
||||
}
|
||||
d.precip.push(prob(pd))
|
||||
d.codes.push(shortForecastToWmo(pd.shortForecast))
|
||||
d.codes.push(shortForecastToWmo(pd.shortForecast, prob(pd)))
|
||||
const ws = parseWind(pd.windSpeed)
|
||||
if (ws != null) d.winds.push(ws)
|
||||
}
|
||||
@ -166,7 +185,7 @@ export function normalizeNWS(src) {
|
||||
temperature_2m: temp(cur.temperature, units),
|
||||
apparent_temperature: apparent,
|
||||
relative_humidity_2m: cur.relativeHumidity?.value ?? null,
|
||||
weather_code: shortForecastToWmo(cur.shortForecast),
|
||||
weather_code: shortForecastToWmo(cur.shortForecast, prob(cur)),
|
||||
wind_speed_10m: ws != null ? speed(ws, units) : null,
|
||||
wind_direction_10m: compassToDeg(cur.windDirection),
|
||||
wind_gusts_10m: null,
|
||||
@ -182,7 +201,7 @@ export function normalizeNWS(src) {
|
||||
time.push(localTimeString(pd.startTime, tz))
|
||||
temperature_2m.push(temp(pd.temperature, units))
|
||||
precipitation_probability.push(prob(pd))
|
||||
weather_code.push(shortForecastToWmo(pd.shortForecast))
|
||||
weather_code.push(shortForecastToWmo(pd.shortForecast, prob(pd)))
|
||||
const ws = parseWind(pd.windSpeed)
|
||||
wind_speed_10m.push(ws != null ? speed(ws, units) : null)
|
||||
}
|
||||
|
||||
@ -18,12 +18,26 @@ describe('nws helpers', () => {
|
||||
expect(shortForecastToWmo('Mostly Sunny')).toBe(2)
|
||||
expect(shortForecastToWmo('Partly Cloudy')).toBe(2)
|
||||
expect(shortForecastToWmo('Overcast')).toBe(3)
|
||||
expect(shortForecastToWmo('Slight Chance Showers And Thunderstorms')).toBe(95)
|
||||
expect(shortForecastToWmo('Heavy Rain')).toBe(65)
|
||||
expect(shortForecastToWmo('Light Snow')).toBe(71)
|
||||
expect(shortForecastToWmo('Areas of Fog')).toBe(45)
|
||||
})
|
||||
|
||||
it('downgrades low-chance thunderstorms so they are not shown as storms', () => {
|
||||
// The word matters less than the raw probability.
|
||||
// 17% "slight chance" of showers/thunderstorms -> light rain showers, NOT a storm.
|
||||
expect(shortForecastToWmo('Slight Chance Showers And Thunderstorms')).toBe(80)
|
||||
expect(shortForecastToWmo('Slight Chance Showers And Thunderstorms', 17)).toBe(80)
|
||||
expect(shortForecastToWmo('Thunderstorms', 20)).toBe(80)
|
||||
expect(shortForecastToWmo('Isolated Thunderstorms', 20)).toBe(80)
|
||||
// A real storm (wordy + high probability) stays a storm.
|
||||
expect(shortForecastToWmo('Thunderstorms', 70)).toBe(95)
|
||||
expect(shortForecastToWmo('Severe Thunderstorm Warning', 80)).toBe(95)
|
||||
// Low-chance plain rain downgrades to light rain too.
|
||||
expect(shortForecastToWmo('Chance of Rain', 15)).toBe(61)
|
||||
expect(shortForecastToWmo('Showers And Thunderstorms', 50)).toBe(95)
|
||||
})
|
||||
|
||||
it('compassToDeg maps compass points to degrees', () => {
|
||||
expect(compassToDeg('N')).toBe(0)
|
||||
expect(compassToDeg('NE')).toBe(45)
|
||||
@ -69,6 +83,10 @@ describe('normalizeNWS (recorded live fixture for Rogers County, OK)', () => {
|
||||
expect(Math.max(...data.daily.temperature_2m_max)).toBeLessThan(115)
|
||||
expect(data.daily.temperature_2m_min[0]).toBe(75)
|
||||
expect(data.daily.precipitation_probability_max[0]).toBe(24)
|
||||
// Today/Monday are only ~17-24% "slight chance" of storms -> light showers.
|
||||
expect(data.daily.weather_code[0]).toBe(80)
|
||||
expect(data.daily.weather_code[1]).toBe(80)
|
||||
expect(data.current.weather_code).toBe(2) // "Mostly Sunny" hour
|
||||
})
|
||||
|
||||
it('builds a realistic current from the first hourly period', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user