Add Today planning module scaffold

This commit is contained in:
2026-06-15 14:37:08 +00:00
parent 7b4c73d1e6
commit 4e704e4f51
11 changed files with 261 additions and 8 deletions

View File

@@ -28,6 +28,13 @@ type TomorrowPlanning struct {
OvernightChangeWatch []string
}
type TodayPlanning struct {
MorningReadiness []string
CommuteSchoolWorkdayConcerns []string
OutdoorPlanning []string
LateDayChangeWatch []string
}
func buildOutdoorWindows(dayparts []forecast.DaypartSummary) OutdoorWindows {
var best *OutdoorWindow
var worst *OutdoorWindow
@@ -48,6 +55,49 @@ func buildOutdoorWindows(dayparts []forecast.DaypartSummary) OutdoorWindows {
return OutdoorWindows{Best: best, Worst: worst}
}
func buildTodayPlanning(summary *forecast.DailySummary) *TodayPlanning {
planning := &TodayPlanning{}
morning := daypartNamed(summary.Dayparts, "morning")
if morning != nil {
planning.MorningReadiness = append(planning.MorningReadiness, readinessNotes(*morning)...)
}
if len(planning.MorningReadiness) == 0 {
planning.MorningReadiness = append(planning.MorningReadiness, "Morning weather looks routine based on the available hourly forecast.")
}
for _, daypart := range summary.Dayparts {
if daypart.Name == "overnight" || daypart.Name == "evening" {
continue
}
planning.CommuteSchoolWorkdayConcerns = appendUnique(planning.CommuteSchoolWorkdayConcerns, concernNotes(daypart)...)
}
for _, alert := range summary.AlertOverlaps {
if alert.Event != "" {
planning.CommuteSchoolWorkdayConcerns = appendUnique(planning.CommuteSchoolWorkdayConcerns, "Active alert to plan around: "+alert.Event+".")
}
}
if len(planning.CommuteSchoolWorkdayConcerns) == 0 {
planning.CommuteSchoolWorkdayConcerns = append(planning.CommuteSchoolWorkdayConcerns, "No major commute, school, or workday weather concerns stand out in the available forecast.")
}
planning.OutdoorPlanning = append(planning.OutdoorPlanning, outdoorPlanningNotes(summary.Dayparts)...)
if len(planning.OutdoorPlanning) == 0 {
planning.OutdoorPlanning = append(planning.OutdoorPlanning, "No standout outdoor weather constraints are evident in the available forecast.")
}
for _, name := range []string{"afternoon", "evening"} {
daypart := daypartNamed(summary.Dayparts, name)
if daypart != nil {
planning.LateDayChangeWatch = appendUnique(planning.LateDayChangeWatch, lateDayWatchNotes(*daypart)...)
}
}
if len(planning.LateDayChangeWatch) == 0 {
planning.LateDayChangeWatch = append(planning.LateDayChangeWatch, "Watch for forecast timing or intensity adjustments later today.")
}
return planning
}
func buildTomorrowPlanning(summary *forecast.DailySummary) *TomorrowPlanning {
planning := &TomorrowPlanning{}
morning := daypartNamed(summary.Dayparts, "morning")
@@ -84,6 +134,18 @@ func buildTomorrowPlanning(summary *forecast.DailySummary) *TomorrowPlanning {
return planning
}
func outdoorPlanningNotes(dayparts []forecast.DaypartSummary) []string {
windows := buildOutdoorWindows(dayparts)
var notes []string
if windows.Best != nil {
notes = append(notes, fmt.Sprintf("Best outdoor window: %s (%s).", titleWord(windows.Best.Daypart), strings.Join(windows.Best.Reasons, ", ")))
}
if windows.Worst != nil && (windows.Best == nil || windows.Worst.Daypart != windows.Best.Daypart) {
notes = append(notes, fmt.Sprintf("Toughest outdoor window: %s (%s).", titleWord(windows.Worst.Daypart), strings.Join(windows.Worst.Reasons, ", ")))
}
return appendUnique(nil, notes...)
}
func readinessNotes(daypart forecast.DaypartSummary) []string {
notes := []string{}
if daypart.MaxPrecipitationProbability != nil && daypart.MaxPrecipitationProbability.Value >= 50 {
@@ -104,6 +166,27 @@ func readinessNotes(daypart forecast.DaypartSummary) []string {
return appendUnique(nil, notes...)
}
func lateDayWatchNotes(daypart forecast.DaypartSummary) []string {
notes := []string{}
prefix := titleWord(daypart.Name)
if prefix == "" {
prefix = "Late-day"
}
if daypart.MaxPrecipitationProbability != nil && daypart.MaxPrecipitationProbability.Value >= 30 {
notes = append(notes, fmt.Sprintf("%s precipitation timing may shift; current peak is near %.0f%%.", prefix, daypart.MaxPrecipitationProbability.Value))
}
if daypart.PeakWindGust != nil && daypart.PeakWindGust.Value >= 30 {
notes = append(notes, fmt.Sprintf("%s gusts may reach %.0f mph.", prefix, daypart.PeakWindGust.Value))
}
if daypart.Indicators.Snow || daypart.Indicators.Ice {
notes = append(notes, prefix+" wintry weather could affect late-day travel.")
}
if len(daypart.AlertOverlaps) > 0 {
notes = append(notes, prefix+" alert timing could affect late-day plans.")
}
return appendUnique(nil, notes...)
}
func concernNotes(daypart forecast.DaypartSummary) []string {
notes := []string{}
prefix := titleWord(daypart.Name)