package risk import "math" // RiskLevel represents the severity of heat risk. type RiskLevel int const ( Low RiskLevel = iota Moderate High Extreme ) func (r RiskLevel) String() string { switch r { case Low: return "low" case Moderate: return "moderate" case High: return "high" case Extreme: return "extreme" default: return "unknown" } } // HourlyData holds weather data for a single hour. type HourlyData struct { Hour int TempC float64 ApparentC float64 HumidityPct float64 IsDay bool } // RiskWindow represents a contiguous block of hours with elevated heat risk. type RiskWindow struct { StartHour int EndHour int PeakTempC float64 Level RiskLevel Reason string } // DayRisk holds the overall risk assessment for a day. type DayRisk struct { Level RiskLevel PeakTempC float64 MinNightTempC float64 PoorNightCool bool Windows []RiskWindow } // isNightHour returns true for hours 21-23 and 0-6. func isNightHour(hour int) bool { return hour >= 21 || hour <= 6 } // riskLevelForTemp returns the risk level based on temperature and thresholds. func riskLevelForTemp(tempC float64, th Thresholds) RiskLevel { switch { case tempC >= th.ExtremeDayC: return Extreme case tempC >= th.VeryHotDayC: return High case tempC >= th.HotDayC: return Moderate default: return Low } } // AnalyzeDay analyzes 24 hourly data points and returns the overall day risk. func AnalyzeDay(hours []HourlyData, th Thresholds) DayRisk { if len(hours) == 0 { return DayRisk{Level: Low, MinNightTempC: math.Inf(1)} } result := DayRisk{ Level: Low, MinNightTempC: math.Inf(1), } // Find peak temp and min night temp for _, h := range hours { if h.TempC > result.PeakTempC { result.PeakTempC = h.TempC } if isNightHour(h.Hour) { if h.TempC < result.MinNightTempC { result.MinNightTempC = h.TempC } if h.TempC >= th.PoorNightCoolingC { result.PoorNightCool = true } } } // If no night hours were seen, set MinNightTempC to 0 if math.IsInf(result.MinNightTempC, 1) { result.MinNightTempC = 0 } // Find contiguous risk windows (hours where temp >= HotDayC) var currentWindow *RiskWindow for _, h := range hours { level := riskLevelForTemp(h.TempC, th) if level >= Moderate { if currentWindow == nil { currentWindow = &RiskWindow{ StartHour: h.Hour, EndHour: h.Hour, PeakTempC: h.TempC, Level: level, } } else { currentWindow.EndHour = h.Hour if h.TempC > currentWindow.PeakTempC { currentWindow.PeakTempC = h.TempC } if level > currentWindow.Level { currentWindow.Level = level } } } else { if currentWindow != nil { currentWindow.Reason = reasonForLevel(currentWindow.Level) result.Windows = append(result.Windows, *currentWindow) currentWindow = nil } } } if currentWindow != nil { currentWindow.Reason = reasonForLevel(currentWindow.Level) result.Windows = append(result.Windows, *currentWindow) } // Overall level = max of all windows for _, w := range result.Windows { if w.Level > result.Level { result.Level = w.Level } } // Poor night cooling elevates by one level (capped at Extreme) if result.PoorNightCool && result.Level > Low && result.Level < Extreme { result.Level++ } return result } func reasonForLevel(level RiskLevel) string { switch level { case Moderate: return "hot daytime temperatures" case High: return "very hot daytime temperatures" case Extreme: return "extreme heat" default: return "" } }