Files
Eric Rakestraw 123e8ff763
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
Moved the standards package out of internal/ so it can be imported by downstream consumers.
2026-02-08 09:15:07 -06:00

227 lines
5.2 KiB
Go

package standards
// This file provides small, shared helper functions for reasoning about WMO codes.
// These are intentionally "coarse" categories that are useful for business logic,
// dashboards, and alerting decisions.
//
// Example uses:
// - jogging suitability: precipitation? thunderstorm? freezing precip?
// - quick glance: "is it cloudy?" "is there any precip?"
// - downstream normalizers / aggregators
import (
"fmt"
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
)
type WMODescription struct {
Day string
Night string
}
// WMODescriptions is the canonical internal mapping of WMO code -> day/night text.
// These are used to populate model.WeatherObservation.ConditionText.
var WMODescriptions = map[model.WMOCode]WMODescription{
0: {Day: "Sunny", Night: "Clear"},
1: {Day: "Mainly Sunny", Night: "Mainly Clear"},
2: {Day: "Partly Cloudy", Night: "Partly Cloudy"},
3: {Day: "Cloudy", Night: "Cloudy"},
45: {Day: "Foggy", Night: "Foggy"},
48: {Day: "Rime Fog", Night: "Rime Fog"},
51: {Day: "Light Drizzle", Night: "Light Drizzle"},
53: {Day: "Drizzle", Night: "Drizzle"},
55: {Day: "Heavy Drizzle", Night: "Heavy Drizzle"},
56: {Day: "Light Freezing Drizzle", Night: "Light Freezing Drizzle"},
57: {Day: "Freezing Drizzle", Night: "Freezing Drizzle"},
61: {Day: "Light Rain", Night: "Light Rain"},
63: {Day: "Rain", Night: "Rain"},
65: {Day: "Heavy Rain", Night: "Heavy Rain"},
66: {Day: "Light Freezing Rain", Night: "Light Freezing Rain"},
67: {Day: "Freezing Rain", Night: "Freezing Rain"},
71: {Day: "Light Snow", Night: "Light Snow"},
73: {Day: "Snow", Night: "Snow"},
75: {Day: "Heavy Snow", Night: "Heavy Snow"},
77: {Day: "Snow Grains", Night: "Snow Grains"},
80: {Day: "Light Showers", Night: "Light Showers"},
81: {Day: "Showers", Night: "Showers"},
82: {Day: "Heavy Showers", Night: "Heavy Showers"},
85: {Day: "Light Snow Showers", Night: "Light Snow Showers"},
86: {Day: "Snow Showers", Night: "Snow Showers"},
95: {Day: "Thunderstorm", Night: "Thunderstorm"},
96: {Day: "Light Thunderstorms With Hail", Night: "Light Thunderstorms With Hail"},
99: {Day: "Thunderstorm With Hail", Night: "Thunderstorm With Hail"},
}
// WMOText returns the canonical text description for a WMO code.
// If isDay is nil, it prefers the Day description (if present).
//
// This is intended to be used by drivers after they set ConditionCode.
func WMOText(code model.WMOCode, isDay *bool) string {
if code == model.WMOUnknown {
return "Unknown"
}
desc, ok := WMODescriptions[code]
if !ok {
// Preserve the code in the message so it's diagnosable.
return fmt.Sprintf("Unknown (WMO %d)", int(code))
}
// If day/night is unknown, default to Day if it exists.
if isDay == nil {
if desc.Day != "" {
return desc.Day
}
if desc.Night != "" {
return desc.Night
}
return fmt.Sprintf("Unknown (WMO %d)", int(code))
}
if *isDay {
if desc.Day != "" {
return desc.Day
}
// Fallback
if desc.Night != "" {
return desc.Night
}
return fmt.Sprintf("Unknown (WMO %d)", int(code))
}
// Night
if desc.Night != "" {
return desc.Night
}
// Fallback
if desc.Day != "" {
return desc.Day
}
return fmt.Sprintf("Unknown (WMO %d)", int(code))
}
// IsKnownWMO returns true if the code exists in our mapping table.
func IsKnownWMO(code model.WMOCode) bool {
if code == model.WMOUnknown {
return false
}
_, ok := WMODescriptions[code]
return ok
}
func IsThunderstorm(code model.WMOCode) bool {
switch code {
case 95, 96, 99:
return true
default:
return false
}
}
func IsHail(code model.WMOCode) bool {
switch code {
case 96, 99:
return true
default:
return false
}
}
func IsFog(code model.WMOCode) bool {
switch code {
case 45, 48:
return true
default:
return false
}
}
// IsPrecipitation returns true if the code represents any precipitation
// (drizzle, rain, snow, showers, etc.).
func IsPrecipitation(code model.WMOCode) bool {
switch code {
// Drizzle
case 51, 53, 55, 56, 57:
return true
// Rain
case 61, 63, 65, 66, 67:
return true
// Snow
case 71, 73, 75, 77:
return true
// Showers
case 80, 81, 82, 85, 86:
return true
// Thunderstorm (often includes rain/hail)
case 95, 96, 99:
return true
default:
return false
}
}
func IsRainFamily(code model.WMOCode) bool {
switch code {
// Drizzle + freezing drizzle
case 51, 53, 55, 56, 57:
return true
// Rain + freezing rain
case 61, 63, 65, 66, 67:
return true
// Rain showers
case 80, 81, 82:
return true
// Thunderstorm often implies rain
case 95, 96, 99:
return true
default:
return false
}
}
func IsSnowFamily(code model.WMOCode) bool {
switch code {
// Snow and related
case 71, 73, 75, 77:
return true
// Snow showers
case 85, 86:
return true
default:
return false
}
}
// IsFreezingPrecip returns true if the code represents freezing drizzle/rain.
func IsFreezingPrecip(code model.WMOCode) bool {
switch code {
case 56, 57, 66, 67:
return true
default:
return false
}
}
// IsSkyOnly returns true for codes that represent "sky condition only"
// (clear/mostly/partly/cloudy) rather than fog/precip/etc.
func IsSkyOnly(code model.WMOCode) bool {
switch code {
case 0, 1, 2, 3:
return true
default:
return false
}
}