Implement derived forecast modules

This commit is contained in:
2026-06-09 20:43:53 +00:00
parent 9501dad1dc
commit eefb0681dc
8 changed files with 662 additions and 1 deletions

View File

@@ -65,6 +65,38 @@ type AlertOverlap struct {
Description string `json:"description,omitempty"`
}
type PrecipTiming struct {
MaxPrecipitationProbability *TimedValue `json:"maxPrecipitationProbability,omitempty"`
FirstPrecipitation *TimedValue `json:"firstPrecipitation,omitempty"`
LastPrecipitation *TimedValue `json:"lastPrecipitation,omitempty"`
ThunderMentioned bool `json:"thunderMentioned,omitempty"`
}
func BuildPrecipTiming(periods []weatherdata.ForecastPeriod) PrecipTiming {
var timing PrecipTiming
for _, forecastPeriod := range periods {
setMaxTimedValue(&timing.MaxPrecipitationProbability, forecastPeriod.ProbabilityOfPrecipitationPercent, forecastPeriod.StartTime)
if forecastPeriod.ProbabilityOfPrecipitationPercent != nil && *forecastPeriod.ProbabilityOfPrecipitationPercent > 0 {
value := TimedValue{
Value: *forecastPeriod.ProbabilityOfPrecipitationPercent,
Time: forecastPeriod.StartTime,
}
if timing.FirstPrecipitation == nil || value.Time.Before(timing.FirstPrecipitation.Time) {
copied := value
timing.FirstPrecipitation = &copied
}
if timing.LastPrecipitation == nil || value.Time.After(timing.LastPrecipitation.Time) {
copied := value
timing.LastPrecipitation = &copied
}
}
if mentionsThunder(forecastPeriod.TextDescription) {
timing.ThunderMentioned = true
}
}
return timing
}
func BuildDailySummary(bundle *weatherdata.Bundle, date time.Time, location *time.Location, dayparts []DaypartDefinition) (*DailySummary, error) {
if bundle == nil {
return nil, fmt.Errorf("forecast bundle is required")
@@ -298,6 +330,10 @@ func indicatorsForText(text string) Indicators {
}
}
func mentionsThunder(text string) bool {
return strings.Contains(strings.ToLower(text), "thunder")
}
func numericIndicators(period weatherdata.ForecastPeriod) Indicators {
windGust := firstValue(period.WindGustMph, period.WindGustKmh)
windSpeed := firstValue(period.WindSpeedMph, period.WindSpeedKmh)

View File

@@ -210,6 +210,47 @@ func TestAlertOverlap(t *testing.T) {
}
}
func TestBuildPrecipTimingTracksRainAndThunder(t *testing.T) {
location := time.FixedZone("Test", -5*60*60)
periods := []weatherdata.ForecastPeriod{
hour(location, "2026-05-29T08:00:00-05:00", "2026-05-29T09:00:00-05:00", "Cloudy", 70, nil, ptr(0), nil, nil),
hour(location, "2026-05-29T09:00:00-05:00", "2026-05-29T10:00:00-05:00", "Showers", 70, nil, ptr(30), nil, nil),
hour(location, "2026-05-29T12:00:00-05:00", "2026-05-29T13:00:00-05:00", "Thunderstorms", 70, nil, ptr(80), nil, nil),
hour(location, "2026-05-29T18:00:00-05:00", "2026-05-29T19:00:00-05:00", "Dry", 70, nil, ptr(0), nil, nil),
}
timing := BuildPrecipTiming(periods)
if timing.FirstPrecipitation == nil || timing.FirstPrecipitation.Time.Format(time.RFC3339) != "2026-05-29T09:00:00-05:00" {
t.Fatalf("FirstPrecipitation = %#v, want 9 AM shower", timing.FirstPrecipitation)
}
if timing.LastPrecipitation == nil || timing.LastPrecipitation.Time.Format(time.RFC3339) != "2026-05-29T12:00:00-05:00" {
t.Fatalf("LastPrecipitation = %#v, want noon thunderstorm", timing.LastPrecipitation)
}
if timing.MaxPrecipitationProbability == nil || timing.MaxPrecipitationProbability.Value != 80 {
t.Fatalf("MaxPrecipitationProbability = %#v, want 80", timing.MaxPrecipitationProbability)
}
if !timing.ThunderMentioned {
t.Fatal("ThunderMentioned = false, want true")
}
}
func TestBuildPrecipTimingHandlesDryForecast(t *testing.T) {
location := time.FixedZone("Test", -5*60*60)
periods := []weatherdata.ForecastPeriod{
hour(location, "2026-05-29T08:00:00-05:00", "2026-05-29T09:00:00-05:00", "Sunny", 70, nil, ptr(0), nil, nil),
}
timing := BuildPrecipTiming(periods)
if timing.FirstPrecipitation != nil || timing.LastPrecipitation != nil || timing.ThunderMentioned {
t.Fatalf("dry timing = %#v, want no precip timing and no thunder", timing)
}
if timing.MaxPrecipitationProbability == nil || timing.MaxPrecipitationProbability.Value != 0 {
t.Fatalf("dry max precip = %#v, want checked zero chance", timing.MaxPrecipitationProbability)
}
}
func TestThresholdHelpers(t *testing.T) {
if !DifferenceAtLeast(50, 56, 5) {
t.Fatal("DifferenceAtLeast = false, want true")