Validate precipitation probabilities and ice wording
This commit is contained in:
@@ -192,6 +192,9 @@ func BuildDailySummary(bundle *weatherdata.Bundle, date time.Time, location *tim
|
||||
if bundle.Hourly == nil || len(bundle.Hourly.Periods) == 0 {
|
||||
return nil, fmt.Errorf("hourly forecast data is required")
|
||||
}
|
||||
if err := ValidatePrecipitationProbabilities(bundle.Hourly.Periods); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
day := timeutil.CivilDay(date, location)
|
||||
windows, err := ResolveDayparts(date, location, dayparts)
|
||||
if err != nil {
|
||||
@@ -247,6 +250,15 @@ func SelectDiscussion(bundle *weatherdata.Bundle) *weatherdata.Discussion {
|
||||
return bundle.Discussion
|
||||
}
|
||||
|
||||
func ValidatePrecipitationProbabilities(periods []weatherdata.ForecastPeriod) error {
|
||||
for i, period := range periods {
|
||||
if !period.HasValidPrecipitationProbability() {
|
||||
return fmt.Errorf("hourly forecast period %d has an invalid precipitation probability", i+1)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SummarizeDaypart(name string, period timeutil.Period, periods []weatherdata.ForecastPeriod) DaypartSummary {
|
||||
summary := DaypartSummary{
|
||||
Name: name,
|
||||
@@ -357,12 +369,24 @@ func indicatorsForText(text string) Indicators {
|
||||
lower := strings.ToLower(text)
|
||||
return Indicators{
|
||||
Snow: strings.Contains(lower, "snow"),
|
||||
Ice: strings.Contains(lower, "ice") || strings.Contains(lower, "freezing") || strings.Contains(lower, "sleet"),
|
||||
Ice: mentionsIce(lower),
|
||||
Fog: strings.Contains(lower, "fog"),
|
||||
Wind: strings.Contains(lower, "wind") || strings.Contains(lower, "gust"),
|
||||
}
|
||||
}
|
||||
|
||||
func mentionsIce(text string) bool {
|
||||
for _, token := range strings.FieldsFunc(text, func(r rune) bool {
|
||||
return !('a' <= r && r <= 'z')
|
||||
}) {
|
||||
switch token {
|
||||
case "ice", "icy", "freezing", "sleet":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mentionsThunder(text string) bool {
|
||||
return strings.Contains(strings.ToLower(text), "thunder")
|
||||
}
|
||||
|
||||
@@ -297,6 +297,41 @@ func TestBuildDailySummaryRequiresHourlyData(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDailySummaryRejectsInvalidPrecipitationProbability(t *testing.T) {
|
||||
location := time.FixedZone("Test", -5*60*60)
|
||||
for _, probability := range []float64{-1, 101, math.NaN()} {
|
||||
bundle := &weatherdata.Bundle{Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{
|
||||
hour(location, "2026-05-29T06:00:00-05:00", "2026-05-29T07:00:00-05:00", "Showers", 70, nil, ptr(probability), nil, nil),
|
||||
}}}
|
||||
_, err := BuildDailySummary(bundle, mustParse("2026-05-29T12:00:00-05:00"), location, []DaypartDefinition{{Name: "morning", Start: "06:00", End: "12:00"}})
|
||||
if err == nil {
|
||||
t.Fatalf("BuildDailySummary(%v) error = nil, want invalid precipitation probability", probability)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndicatorsForTextClassifyIceVocabulary(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
text string
|
||||
want Indicators
|
||||
}{
|
||||
{name: "ice", text: "Ice likely", want: Indicators{Ice: true}},
|
||||
{name: "icy mixed case", text: "ICY roads", want: Indicators{Ice: true}},
|
||||
{name: "freezing punctuation", text: "Freezing-rain possible", want: Indicators{Ice: true}},
|
||||
{name: "sleet punctuation", text: "Sleet, then rain", want: Indicators{Ice: true}},
|
||||
{name: "unrelated nice", text: "Nice weather"},
|
||||
{name: "unrelated spicy", text: "Spicy conditions"},
|
||||
{name: "other conditions unchanged", text: "Snow and fog with gusts", want: Indicators{Snow: true, Fog: true, Wind: true}},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := indicatorsForText(tt.text); got != tt.want {
|
||||
t.Fatalf("indicatorsForText(%q) = %#v, want %#v", tt.text, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertOverlap(t *testing.T) {
|
||||
location := time.FixedZone("Test", -5*60*60)
|
||||
raw := json.RawMessage(`{"event":"Flood Watch","headline":"Flooding possible","severity":"Moderate","effective":"2026-05-29T07:00:00-05:00","expires":"2026-05-29T10:00:00-05:00"}`)
|
||||
|
||||
Reference in New Issue
Block a user