Preserve metric forecast units and overnight alerts
This commit is contained in:
@@ -2,6 +2,7 @@ package forecast
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -99,11 +100,17 @@ func TestBuildDailySummaryFromFixtureBundle(t *testing.T) {
|
||||
func TestOvernightGroupingAcrossMidnight(t *testing.T) {
|
||||
location := time.FixedZone("Test", -5*60*60)
|
||||
date := time.Date(2026, 5, 29, 12, 0, 0, 0, location)
|
||||
bundle := &weatherdata.Bundle{Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{
|
||||
hour(location, "2026-05-29T23:00:00-05:00", "2026-05-30T00:00:00-05:00", "Snow", 31, nil, nil, nil, nil),
|
||||
hour(location, "2026-05-30T05:00:00-05:00", "2026-05-30T06:00:00-05:00", "Fog", 30, nil, nil, nil, nil),
|
||||
hour(location, "2026-05-30T06:00:00-05:00", "2026-05-30T07:00:00-05:00", "Clear", 35, nil, nil, nil, nil),
|
||||
}}}
|
||||
bundle := &weatherdata.Bundle{
|
||||
Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{
|
||||
hour(location, "2026-05-29T23:00:00-05:00", "2026-05-30T00:00:00-05:00", "Snow", 31, nil, nil, nil, nil),
|
||||
hour(location, "2026-05-30T05:00:00-05:00", "2026-05-30T06:00:00-05:00", "Fog", 30, nil, nil, nil, nil),
|
||||
hour(location, "2026-05-30T06:00:00-05:00", "2026-05-30T07:00:00-05:00", "Clear", 35, nil, nil, nil, nil),
|
||||
}},
|
||||
Alerts: &weatherdata.AlertRun{Alerts: []json.RawMessage{
|
||||
json.RawMessage(`{"event":"Overnight Advisory","onset":"2026-05-30T01:00:00-05:00","ends":"2026-05-30T05:00:00-05:00"}`),
|
||||
json.RawMessage(`{"event":"After Night","onset":"2026-05-30T06:00:00-05:00","ends":"2026-05-30T07:00:00-05:00"}`),
|
||||
}},
|
||||
}
|
||||
|
||||
summary, err := BuildDailySummary(bundle, date, location, []DaypartDefinition{
|
||||
{Name: "night", Start: "22:00", End: "06:00"},
|
||||
@@ -118,6 +125,142 @@ func TestOvernightGroupingAcrossMidnight(t *testing.T) {
|
||||
if !night.Indicators.Snow || !night.Indicators.Fog || !night.Indicators.Cold {
|
||||
t.Fatalf("night indicators = %#v, want snow, fog, and cold", night.Indicators)
|
||||
}
|
||||
if len(summary.AlertOverlaps) != 0 {
|
||||
t.Fatalf("daily AlertOverlaps = %#v, want next-day alerts excluded", summary.AlertOverlaps)
|
||||
}
|
||||
if len(night.AlertOverlaps) != 1 || night.AlertOverlaps[0].Event != "Overnight Advisory" {
|
||||
t.Fatalf("night AlertOverlaps = %#v, want complete overnight overlap", night.AlertOverlaps)
|
||||
}
|
||||
if night.AlertOverlaps[0].Overlap.Start.Format(time.RFC3339) != "2026-05-30T01:00:00-05:00" || night.AlertOverlaps[0].Overlap.End.Format(time.RFC3339) != "2026-05-30T05:00:00-05:00" {
|
||||
t.Fatalf("night alert overlap = %#v, want source alert clipped to the overnight window", night.AlertOverlaps[0].Overlap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeDaypartNormalizesMetricFallbacks(t *testing.T) {
|
||||
period := timeutil.Period{Start: mustParse("2026-05-29T06:00:00-05:00"), End: mustParse("2026-05-29T07:00:00-05:00")}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
temperature float64
|
||||
windSpeed float64
|
||||
windGust float64
|
||||
want Indicators
|
||||
}{
|
||||
{name: "below thresholds", temperature: 94, windSpeed: 24, windGust: 34},
|
||||
{name: "at thresholds", temperature: 95, windSpeed: 25, windGust: 35, want: Indicators{Heat: true, Wind: true}},
|
||||
{name: "above thresholds", temperature: 96, windSpeed: 26, windGust: 36, want: Indicators{Heat: true, Wind: true}},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
us := weatherdata.ForecastPeriod{TemperatureF: ptr(tt.temperature), WindSpeedMph: ptr(tt.windSpeed), WindGustMph: ptr(tt.windGust)}
|
||||
metric := weatherdata.ForecastPeriod{TemperatureC: ptr((tt.temperature - 32) * 5 / 9), WindSpeedKmh: ptr(tt.windSpeed * 1.609344), WindGustKmh: ptr(tt.windGust * 1.609344)}
|
||||
both := metric
|
||||
both.TemperatureF = ptr(tt.temperature)
|
||||
both.WindSpeedMph = ptr(tt.windSpeed)
|
||||
both.WindGustMph = ptr(tt.windGust)
|
||||
both.TemperatureC = ptr(-40)
|
||||
both.WindSpeedKmh = ptr(0)
|
||||
both.WindGustKmh = ptr(0)
|
||||
|
||||
for _, source := range []struct {
|
||||
name string
|
||||
period weatherdata.ForecastPeriod
|
||||
}{
|
||||
{name: "us customary", period: us},
|
||||
{name: "metric", period: metric},
|
||||
{name: "both", period: both},
|
||||
} {
|
||||
t.Run(source.name, func(t *testing.T) {
|
||||
if got := numericIndicators(source.period); got != tt.want {
|
||||
t.Fatalf("numericIndicators() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
temperature float64
|
||||
want Indicators
|
||||
}{
|
||||
{name: "cold above threshold", temperature: 33},
|
||||
{name: "cold at threshold", temperature: 32, want: Indicators{Cold: true}},
|
||||
{name: "cold below threshold", temperature: 31, want: Indicators{Cold: true}},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
us := weatherdata.ForecastPeriod{TemperatureF: ptr(tt.temperature)}
|
||||
metric := weatherdata.ForecastPeriod{TemperatureC: ptr((tt.temperature - 32) * 5 / 9)}
|
||||
both := metric
|
||||
both.TemperatureF = ptr(tt.temperature)
|
||||
both.TemperatureC = ptr(40)
|
||||
|
||||
for _, source := range []struct {
|
||||
name string
|
||||
period weatherdata.ForecastPeriod
|
||||
}{
|
||||
{name: "us customary", period: us},
|
||||
{name: "metric", period: metric},
|
||||
{name: "both", period: both},
|
||||
} {
|
||||
t.Run(source.name, func(t *testing.T) {
|
||||
if got := numericIndicators(source.period); got != tt.want {
|
||||
t.Fatalf("numericIndicators() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
us := weatherdata.ForecastPeriod{
|
||||
StartTime: period.Start,
|
||||
EndTime: period.End,
|
||||
TemperatureF: ptr(68),
|
||||
TemperatureFMin: ptr(50),
|
||||
TemperatureFMax: ptr(86),
|
||||
ApparentTemperatureF: ptr(69.8),
|
||||
WindSpeedMph: ptr(31.068559),
|
||||
WindGustMph: ptr(37.282271),
|
||||
}
|
||||
metric := weatherdata.ForecastPeriod{
|
||||
StartTime: period.Start,
|
||||
EndTime: period.End,
|
||||
TemperatureC: ptr(20),
|
||||
TemperatureCMin: ptr(10),
|
||||
TemperatureCMax: ptr(30),
|
||||
ApparentTemperatureC: ptr(21),
|
||||
WindSpeedKmh: ptr(50),
|
||||
WindGustKmh: ptr(60),
|
||||
}
|
||||
both := metric
|
||||
both.TemperatureF = us.TemperatureF
|
||||
both.TemperatureFMin = us.TemperatureFMin
|
||||
both.TemperatureFMax = us.TemperatureFMax
|
||||
both.ApparentTemperatureF = us.ApparentTemperatureF
|
||||
both.WindSpeedMph = us.WindSpeedMph
|
||||
both.WindGustMph = us.WindGustMph
|
||||
both.TemperatureC = ptr(-40)
|
||||
both.TemperatureCMin = ptr(-40)
|
||||
both.TemperatureCMax = ptr(-40)
|
||||
both.ApparentTemperatureC = ptr(-40)
|
||||
both.WindSpeedKmh = ptr(0)
|
||||
both.WindGustKmh = ptr(0)
|
||||
|
||||
for _, source := range []struct {
|
||||
name string
|
||||
period weatherdata.ForecastPeriod
|
||||
}{
|
||||
{name: "us customary", period: us},
|
||||
{name: "metric", period: metric},
|
||||
{name: "both", period: both},
|
||||
} {
|
||||
t.Run(source.name, func(t *testing.T) {
|
||||
summary := SummarizeDaypart("morning", period, []weatherdata.ForecastPeriod{source.period})
|
||||
assertRangeClose(t, "temperature", summary.Temperature, 50, 86)
|
||||
assertRangeClose(t, "apparent temperature", summary.ApparentTemperature, 69.8, 69.8)
|
||||
assertTimedValueClose(t, "peak wind speed", summary.PeakWindSpeed, 31.068559)
|
||||
assertTimedValueClose(t, "peak wind gust", summary.PeakWindGust, 37.282271)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoundaryTimestampsAtDaypartEdges(t *testing.T) {
|
||||
@@ -466,3 +609,17 @@ func assertRange(t *testing.T, name string, got Range, wantMin float64, wantMax
|
||||
t.Fatalf("%s = [%v,%v], want [%v,%v]", name, *got.Min, *got.Max, wantMin, wantMax)
|
||||
}
|
||||
}
|
||||
|
||||
func assertRangeClose(t *testing.T, name string, got Range, wantMin float64, wantMax float64) {
|
||||
t.Helper()
|
||||
if got.Min == nil || got.Max == nil || math.Abs(*got.Min-wantMin) > 0.000001 || math.Abs(*got.Max-wantMax) > 0.000001 {
|
||||
t.Fatalf("%s = %#v, want range [%v, %v]", name, got, wantMin, wantMax)
|
||||
}
|
||||
}
|
||||
|
||||
func assertTimedValueClose(t *testing.T, name string, got *TimedValue, want float64) {
|
||||
t.Helper()
|
||||
if got == nil || math.Abs(got.Value-want) > 0.000001 {
|
||||
t.Fatalf("%s = %#v, want %v", name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user