package generatedtext import ( "fmt" "reflect" "sort" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/facts" "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) type HourlyRenderContext struct { Report HourlyReportContext GeneratedText Hourly Modules HourlyTemplateModules } type HourlyReportContext struct { Title string LocationName string GeneratedAt time.Time GeneratedAtLabel string ValidPeriod timeutil.Period ValidPeriodLabel string Timezone string } type HourlyTemplateModules struct { CurrentConditions *briefing.CurrentConditionsModule HourlyForecast *briefing.HourlyForecastModule PrecipTiming *briefing.PrecipTimingModule AlertDigest *briefing.AlertDigestModule SPCConvectiveOutlooks *briefing.SPCConvectiveOutlooksModule AreaForecastDiscussion *briefing.AreaForecastDiscussionModule SPCConvectiveDiscussion *briefing.SPCConvectiveDiscussionModule WeatherStory *briefing.WeatherStoryModule } type TomorrowRenderContext struct { Report TomorrowReportContext GeneratedText Tomorrow Modules TomorrowTemplateModules } type DailyRenderContext struct { Report DailyReportContext GeneratedText Daily Modules DailyTemplateModules } type TodayRenderContext struct { Report TodayReportContext GeneratedText Today Modules TodayTemplateModules } type TodayReportContext struct { Title string ForecastDate time.Time ForecastDateLabel string ForecastDayName string GeneratedAt time.Time GeneratedAtLabel string ValidPeriod timeutil.Period Timezone string } type TodayTemplateModules struct { CurrentConditions *briefing.CurrentConditionsModule HourlyForecast *briefing.HourlyForecastModule DerivedDailySummary *briefing.DerivedDailySummaryModule DerivedDaypartSummaries *map[string]briefing.DerivedDaypartSummaryModule Dayparts []TodayDaypartContext HasDaypartDetails bool PrecipTiming *briefing.PrecipTimingModule AlertDigest *briefing.AlertDigestModule SPCConvectiveOutlooks *briefing.SPCConvectiveOutlooksModule AreaForecastDiscussion *briefing.AreaForecastDiscussionModule SPCConvectiveDiscussion *briefing.SPCConvectiveDiscussionModule WeatherStory *briefing.WeatherStoryModule TodayPlanning *briefing.TodayPlanningModule } type TodayDaypartContext struct { Key string Summary briefing.DerivedDaypartSummaryModule } type TomorrowReportContext struct { Title string ForecastDate time.Time ForecastDateLabel string ForecastDayName string GeneratedAt time.Time GeneratedAtLabel string ValidPeriod timeutil.Period Timezone string } type TomorrowTemplateModules struct { CurrentConditions *briefing.CurrentConditionsModule HourlyForecast *briefing.HourlyForecastModule DerivedDailySummary *briefing.DerivedDailySummaryModule DerivedDaypartSummaries *map[string]briefing.DerivedDaypartSummaryModule Dayparts []TomorrowDaypartContext PrecipTiming *briefing.PrecipTimingModule AlertDigest *briefing.AlertDigestModule SPCConvectiveOutlooks *briefing.SPCConvectiveOutlooksModule AreaForecastDiscussion *briefing.AreaForecastDiscussionModule SPCConvectiveDiscussion *briefing.SPCConvectiveDiscussionModule WeatherStory *briefing.WeatherStoryModule TomorrowPlanning *briefing.TomorrowPlanningModule } type TomorrowDaypartContext struct { Key string Summary briefing.DerivedDaypartSummaryModule } type DailyReportContext struct { Title string ForecastDate time.Time ForecastDateLabel string ForecastDayName string GeneratedAt time.Time GeneratedAtLabel string ValidPeriod timeutil.Period Timezone string } type DailyTemplateModules struct { CurrentConditions *briefing.CurrentConditionsModule HourlyForecast *briefing.HourlyForecastModule DerivedDailySummary *briefing.DerivedDailySummaryModule DerivedDaypartSummaries *map[string]briefing.DerivedDaypartSummaryModule Dayparts []DailyDaypartContext PrecipTiming *briefing.PrecipTimingModule AlertDigest *briefing.AlertDigestModule SPCConvectiveOutlooks *briefing.SPCConvectiveOutlooksModule AreaForecastDiscussion *briefing.AreaForecastDiscussionModule SPCConvectiveDiscussion *briefing.SPCConvectiveDiscussionModule WeatherStory *briefing.WeatherStoryModule OutdoorWindows *briefing.OutdoorWindowsModule DailyPlanning *briefing.DailyPlanningModule } type DailyDaypartContext struct { Key string Summary briefing.DerivedDaypartSummaryModule } type dayStyleReportContext struct { Title string ForecastDate time.Time ForecastDateLabel string ForecastDayName string GeneratedAt time.Time GeneratedAtLabel string ValidPeriod timeutil.Period Timezone string } type dayStyleTemplateModules struct { CurrentConditions *briefing.CurrentConditionsModule HourlyForecast *briefing.HourlyForecastModule DerivedDailySummary *briefing.DerivedDailySummaryModule DerivedDaypartSummaries *map[string]briefing.DerivedDaypartSummaryModule PrecipTiming *briefing.PrecipTimingModule AlertDigest *briefing.AlertDigestModule SPCConvectiveOutlooks *briefing.SPCConvectiveOutlooksModule AreaForecastDiscussion *briefing.AreaForecastDiscussionModule SPCConvectiveDiscussion *briefing.SPCConvectiveDiscussionModule WeatherStory *briefing.WeatherStoryModule } func BuildHourlyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Hourly) (HourlyRenderContext, error) { if err := validateRenderIdentity(report.Hourly, identity, snapshot); err != nil { return HourlyRenderContext{}, err } location, err := timeutil.LoadLocation(identity.Timezone) if err != nil { return HourlyRenderContext{}, fmt.Errorf("build hourly render context: %w", err) } if identity.GeneratedAt.IsZero() { return HourlyRenderContext{}, fmt.Errorf("build hourly render context: generatedAt is required") } if !identity.ValidPeriod.IsValid() { return HourlyRenderContext{}, fmt.Errorf("build hourly render context: valid period is required") } modules, err := hourlyTemplateModules(snapshot) if err != nil { return HourlyRenderContext{}, err } return HourlyRenderContext{ Report: HourlyReportContext{ Title: "Hourly Report", LocationName: locationName(identity), GeneratedAt: identity.GeneratedAt, GeneratedAtLabel: generatedAtLabel(identity.GeneratedAt, location), ValidPeriod: identity.ValidPeriod, ValidPeriodLabel: periodLabel(identity.ValidPeriod, location), Timezone: identity.Timezone, }, GeneratedText: generated, Modules: modules, }, nil } func BuildDailyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Daily, derived facts.DerivedFacts) (DailyRenderContext, error) { if err := validateRenderIdentity(report.Daily, identity, snapshot); err != nil { return DailyRenderContext{}, err } reportContext, err := buildDayStyleReportContext(identity, "daily", func(dayName string) string { return dayName + "'s Weather" }) if err != nil { return DailyRenderContext{}, err } modules, err := dailyTemplateModules(snapshot, derived) if err != nil { return DailyRenderContext{}, err } return DailyRenderContext{ Report: reportContext.dailyReportContext(), GeneratedText: generated, Modules: modules, }, nil } func BuildTodayRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Today, derived facts.DerivedFacts) (TodayRenderContext, error) { if err := validateRenderIdentity(report.Today, identity, snapshot); err != nil { return TodayRenderContext{}, err } reportContext, err := buildDayStyleReportContext(identity, "today", func(string) string { return "Today's Weather" }) if err != nil { return TodayRenderContext{}, err } modules, err := todayTemplateModules(snapshot, derived) if err != nil { return TodayRenderContext{}, err } return TodayRenderContext{ Report: reportContext.todayReportContext(), GeneratedText: generated, Modules: modules, }, nil } func BuildTomorrowRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Tomorrow, derived facts.DerivedFacts) (TomorrowRenderContext, error) { if err := validateRenderIdentity(report.Tomorrow, identity, snapshot); err != nil { return TomorrowRenderContext{}, err } reportContext, err := buildDayStyleReportContext(identity, "tomorrow", func(dayName string) string { return dayName + "'s Weather" }) if err != nil { return TomorrowRenderContext{}, err } modules, err := tomorrowTemplateModules(snapshot, derived) if err != nil { return TomorrowRenderContext{}, err } return TomorrowRenderContext{ Report: reportContext.tomorrowReportContext(), GeneratedText: generated, Modules: modules, }, nil } func validateRenderIdentity(expected report.ID, identity briefing.PreparedIdentity, snapshot module.Snapshot) error { if identity.ReportID != expected { return fmt.Errorf("render identity report %q does not match expected report %q", identity.ReportID, expected) } metadata, found, err := module.StanzaValue[briefing.MetadataModule](snapshot, string(module.Metadata)) if err != nil { return fmt.Errorf("read metadata module for report %q: %w", expected, err) } if !found || metadataMatchesIdentity(metadata, identity) { return nil } return fmt.Errorf("metadata module does not match render identity for report %q", expected) } func metadataMatchesIdentity(metadata briefing.MetadataModule, identity briefing.PreparedIdentity) bool { if metadata.RunID != identity.RunID || metadata.ReportID != identity.ReportID || metadata.Variant != identity.Variant || metadata.PromptID != identity.PromptID || !metadata.GeneratedAt.Equal(identity.GeneratedAt) || metadata.Units != identity.Units || metadata.Timezone != identity.Timezone || metadata.ValidPeriod != identity.ValidPeriod || !reflect.DeepEqual(metadata.Location, identity.Location) || len(metadata.SourceWarnings) != len(identity.SourceWarnings) { return false } for index, warning := range identity.SourceWarnings { summary := metadata.SourceWarnings[index] if summary.Source != warning.Source || summary.Code != warning.Code || summary.Severity != warning.Severity || summary.Message != warning.Message || summary.CompletenessImpact != warning.CompletenessImpact { return false } } return true } func buildDayStyleReportContext(identity briefing.PreparedIdentity, name string, title func(string) string) (dayStyleReportContext, error) { location, err := timeutil.LoadLocation(identity.Timezone) if err != nil { return dayStyleReportContext{}, fmt.Errorf("build %s render context: %w", name, err) } if identity.GeneratedAt.IsZero() { return dayStyleReportContext{}, fmt.Errorf("build %s render context: generatedAt is required", name) } if !identity.ValidPeriod.IsValid() { return dayStyleReportContext{}, fmt.Errorf("build %s render context: valid period is required", name) } forecastDate := identity.ValidPeriod.Start.In(location) forecastDayName := forecastDate.Format("Monday") return dayStyleReportContext{ Title: title(forecastDayName), ForecastDate: forecastDate, ForecastDateLabel: forecastDate.Format("Monday, January 2, 2006"), ForecastDayName: forecastDayName, GeneratedAt: identity.GeneratedAt, GeneratedAtLabel: generatedAtLabel(identity.GeneratedAt, location), ValidPeriod: identity.ValidPeriod, Timezone: identity.Timezone, }, nil } func (ctx dayStyleReportContext) dailyReportContext() DailyReportContext { return DailyReportContext{ Title: ctx.Title, ForecastDate: ctx.ForecastDate, ForecastDateLabel: ctx.ForecastDateLabel, ForecastDayName: ctx.ForecastDayName, GeneratedAt: ctx.GeneratedAt, GeneratedAtLabel: ctx.GeneratedAtLabel, ValidPeriod: ctx.ValidPeriod, Timezone: ctx.Timezone, } } func (ctx dayStyleReportContext) todayReportContext() TodayReportContext { return TodayReportContext{ Title: ctx.Title, ForecastDate: ctx.ForecastDate, ForecastDateLabel: ctx.ForecastDateLabel, ForecastDayName: ctx.ForecastDayName, GeneratedAt: ctx.GeneratedAt, GeneratedAtLabel: ctx.GeneratedAtLabel, ValidPeriod: ctx.ValidPeriod, Timezone: ctx.Timezone, } } func (ctx dayStyleReportContext) tomorrowReportContext() TomorrowReportContext { return TomorrowReportContext{ Title: ctx.Title, ForecastDate: ctx.ForecastDate, ForecastDateLabel: ctx.ForecastDateLabel, ForecastDayName: ctx.ForecastDayName, GeneratedAt: ctx.GeneratedAt, GeneratedAtLabel: ctx.GeneratedAtLabel, ValidPeriod: ctx.ValidPeriod, Timezone: ctx.Timezone, } } func dailyTemplateModules(snapshot module.Snapshot, derived facts.DerivedFacts) (DailyTemplateModules, error) { common, lookup, err := dayStyleTemplateModuleSnapshot(snapshot) if err != nil { return DailyTemplateModules{}, err } outdoor, err := lookup.outdoorWindows() if err != nil { return DailyTemplateModules{}, err } planning, err := lookup.dailyPlanning() if err != nil { return DailyTemplateModules{}, err } return DailyTemplateModules{ CurrentConditions: common.CurrentConditions, HourlyForecast: common.HourlyForecast, DerivedDailySummary: common.DerivedDailySummary, DerivedDaypartSummaries: common.DerivedDaypartSummaries, Dayparts: orderedDailyDayparts(common.DerivedDaypartSummaries, derived.DaypartSummaries), PrecipTiming: common.PrecipTiming, AlertDigest: common.AlertDigest, SPCConvectiveOutlooks: common.SPCConvectiveOutlooks, AreaForecastDiscussion: common.AreaForecastDiscussion, SPCConvectiveDiscussion: common.SPCConvectiveDiscussion, WeatherStory: common.WeatherStory, OutdoorWindows: outdoor, DailyPlanning: planning, }, nil } func todayTemplateModules(snapshot module.Snapshot, derived facts.DerivedFacts) (TodayTemplateModules, error) { common, lookup, err := dayStyleTemplateModuleSnapshot(snapshot) if err != nil { return TodayTemplateModules{}, err } planning, err := lookup.todayPlanning() if err != nil { return TodayTemplateModules{}, err } dayparts := orderedTodayDayparts(common.DerivedDaypartSummaries, derived.DaypartSummaries) return TodayTemplateModules{ CurrentConditions: common.CurrentConditions, HourlyForecast: common.HourlyForecast, DerivedDailySummary: common.DerivedDailySummary, DerivedDaypartSummaries: common.DerivedDaypartSummaries, Dayparts: dayparts, HasDaypartDetails: hasTodayDaypartDetails(dayparts), PrecipTiming: common.PrecipTiming, AlertDigest: common.AlertDigest, SPCConvectiveOutlooks: common.SPCConvectiveOutlooks, AreaForecastDiscussion: common.AreaForecastDiscussion, SPCConvectiveDiscussion: common.SPCConvectiveDiscussion, WeatherStory: common.WeatherStory, TodayPlanning: planning, }, nil } func hourlyTemplateModules(snapshot module.Snapshot) (HourlyTemplateModules, error) { lookup := newModuleSnapshotLookup(snapshot) current, err := lookup.currentConditions() if err != nil { return HourlyTemplateModules{}, err } hourly, err := lookup.hourlyForecast() if err != nil { return HourlyTemplateModules{}, err } precip, err := lookup.precipTiming() if err != nil { return HourlyTemplateModules{}, err } alerts, err := lookup.alertDigest() if err != nil { return HourlyTemplateModules{}, err } outlooks, err := lookup.spcConvectiveOutlooks() if err != nil { return HourlyTemplateModules{}, err } discussion, err := lookup.areaForecastDiscussion() if err != nil { return HourlyTemplateModules{}, err } spcDiscussion, err := lookup.spcConvectiveDiscussion() if err != nil { return HourlyTemplateModules{}, err } story, err := lookup.weatherStory() if err != nil { return HourlyTemplateModules{}, err } return HourlyTemplateModules{ CurrentConditions: current, HourlyForecast: hourly, PrecipTiming: precip, AlertDigest: alerts, SPCConvectiveOutlooks: outlooks, AreaForecastDiscussion: discussion, SPCConvectiveDiscussion: spcDiscussion, WeatherStory: story, }, nil } func tomorrowTemplateModules(snapshot module.Snapshot, derived facts.DerivedFacts) (TomorrowTemplateModules, error) { common, lookup, err := dayStyleTemplateModuleSnapshot(snapshot) if err != nil { return TomorrowTemplateModules{}, err } planning, err := lookup.tomorrowPlanning() if err != nil { return TomorrowTemplateModules{}, err } return TomorrowTemplateModules{ CurrentConditions: common.CurrentConditions, HourlyForecast: common.HourlyForecast, DerivedDailySummary: common.DerivedDailySummary, DerivedDaypartSummaries: common.DerivedDaypartSummaries, Dayparts: orderedTomorrowDayparts(common.DerivedDaypartSummaries, derived.DaypartSummaries), PrecipTiming: common.PrecipTiming, AlertDigest: common.AlertDigest, SPCConvectiveOutlooks: common.SPCConvectiveOutlooks, AreaForecastDiscussion: common.AreaForecastDiscussion, SPCConvectiveDiscussion: common.SPCConvectiveDiscussion, WeatherStory: common.WeatherStory, TomorrowPlanning: planning, }, nil } func dayStyleTemplateModuleSnapshot(snapshot module.Snapshot) (dayStyleTemplateModules, moduleSnapshotLookup, error) { lookup := newModuleSnapshotLookup(snapshot) current, err := lookup.currentConditions() if err != nil { return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err } hourly, err := lookup.hourlyForecast() if err != nil { return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err } daily, err := lookup.derivedDailySummary() if err != nil { return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err } dayparts, err := lookup.derivedDaypartSummaries() if err != nil { return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err } precip, err := lookup.precipTiming() if err != nil { return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err } alerts, err := lookup.alertDigest() if err != nil { return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err } outlooks, err := lookup.spcConvectiveOutlooks() if err != nil { return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err } discussion, err := lookup.areaForecastDiscussion() if err != nil { return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err } spcDiscussion, err := lookup.spcConvectiveDiscussion() if err != nil { return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err } story, err := lookup.weatherStory() if err != nil { return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err } return dayStyleTemplateModules{ CurrentConditions: current, HourlyForecast: hourly, DerivedDailySummary: daily, DerivedDaypartSummaries: dayparts, PrecipTiming: precip, AlertDigest: alerts, SPCConvectiveOutlooks: outlooks, AreaForecastDiscussion: discussion, SPCConvectiveDiscussion: spcDiscussion, WeatherStory: story, }, lookup, nil } type moduleSnapshotLookup struct { snapshot module.Snapshot stanzas map[string]module.Output } func newModuleSnapshotLookup(snapshot module.Snapshot) moduleSnapshotLookup { stanzas := make(map[string]module.Output, len(snapshot.Outputs)) for _, output := range snapshot.Outputs { stanzas[output.StanzaName] = output } return moduleSnapshotLookup{ snapshot: snapshot, stanzas: stanzas, } } func (lookup moduleSnapshotLookup) currentConditions() (*briefing.CurrentConditionsModule, error) { return optionalStanza[briefing.CurrentConditionsModule](lookup, module.CurrentConditions) } func (lookup moduleSnapshotLookup) hourlyForecast() (*briefing.HourlyForecastModule, error) { return optionalStanza[briefing.HourlyForecastModule](lookup, module.HourlyForecast) } func (lookup moduleSnapshotLookup) derivedDailySummary() (*briefing.DerivedDailySummaryModule, error) { return optionalStanza[briefing.DerivedDailySummaryModule](lookup, module.DerivedDailySummary) } func (lookup moduleSnapshotLookup) derivedDaypartSummaries() (*map[string]briefing.DerivedDaypartSummaryModule, error) { return optionalStanza[map[string]briefing.DerivedDaypartSummaryModule](lookup, module.DerivedDaypartSummaries) } func (lookup moduleSnapshotLookup) precipTiming() (*briefing.PrecipTimingModule, error) { return optionalStanza[briefing.PrecipTimingModule](lookup, module.PrecipTiming) } func (lookup moduleSnapshotLookup) alertDigest() (*briefing.AlertDigestModule, error) { return optionalStanza[briefing.AlertDigestModule](lookup, module.AlertDigest) } func (lookup moduleSnapshotLookup) spcConvectiveOutlooks() (*briefing.SPCConvectiveOutlooksModule, error) { return optionalStanza[briefing.SPCConvectiveOutlooksModule](lookup, module.SPCConvectiveOutlooks) } func (lookup moduleSnapshotLookup) areaForecastDiscussion() (*briefing.AreaForecastDiscussionModule, error) { return optionalStanza[briefing.AreaForecastDiscussionModule](lookup, module.AreaForecastDiscussion) } func (lookup moduleSnapshotLookup) spcConvectiveDiscussion() (*briefing.SPCConvectiveDiscussionModule, error) { return optionalStanza[briefing.SPCConvectiveDiscussionModule](lookup, module.SPCConvectiveDiscussion) } func (lookup moduleSnapshotLookup) weatherStory() (*briefing.WeatherStoryModule, error) { return optionalStanza[briefing.WeatherStoryModule](lookup, module.WeatherStory) } func (lookup moduleSnapshotLookup) outdoorWindows() (*briefing.OutdoorWindowsModule, error) { return optionalStanza[briefing.OutdoorWindowsModule](lookup, module.OutdoorWindows) } func (lookup moduleSnapshotLookup) dailyPlanning() (*briefing.DailyPlanningModule, error) { return optionalStanza[briefing.DailyPlanningModule](lookup, module.DailyPlanning) } func (lookup moduleSnapshotLookup) todayPlanning() (*briefing.TodayPlanningModule, error) { return optionalStanza[briefing.TodayPlanningModule](lookup, module.TodayPlanning) } func (lookup moduleSnapshotLookup) tomorrowPlanning() (*briefing.TomorrowPlanningModule, error) { return optionalStanza[briefing.TomorrowPlanningModule](lookup, module.TomorrowPlanning) } func optionalStanza[T any](lookup moduleSnapshotLookup, id module.ID) (*T, error) { name := string(id) output, ok := lookup.stanzas[name] if !ok || output.Value == nil { return nil, nil } value, _, err := module.StanzaValue[T](lookup.snapshot, name) if err != nil { return nil, fmt.Errorf("build render context module %q: %w", id, err) } return &value, nil } func orderedTodayDayparts(dayparts *map[string]briefing.DerivedDaypartSummaryModule, ordered []forecast.DaypartSummary) []TodayDaypartContext { orderedRows := orderedDaypartRows(dayparts, ordered) out := make([]TodayDaypartContext, 0, len(orderedRows)) for _, row := range orderedRows { out = append(out, TodayDaypartContext{Key: row.Key, Summary: row.Summary}) } return out } func hasTodayDaypartDetails(dayparts []TodayDaypartContext) bool { for _, daypart := range dayparts { if daypart.Summary.DominantConditionDisplay != "" || daypart.Summary.DominantCondition != "" { return true } } return false } func orderedDailyDayparts(dayparts *map[string]briefing.DerivedDaypartSummaryModule, ordered []forecast.DaypartSummary) []DailyDaypartContext { orderedRows := orderedDaypartRows(dayparts, ordered) out := make([]DailyDaypartContext, 0, len(orderedRows)) for _, row := range orderedRows { out = append(out, DailyDaypartContext{Key: row.Key, Summary: row.Summary}) } return out } func orderedTomorrowDayparts(dayparts *map[string]briefing.DerivedDaypartSummaryModule, ordered []forecast.DaypartSummary) []TomorrowDaypartContext { orderedRows := orderedDaypartRows(dayparts, ordered) out := make([]TomorrowDaypartContext, 0, len(orderedRows)) for _, row := range orderedRows { out = append(out, TomorrowDaypartContext{Key: row.Key, Summary: row.Summary}) } return out } type orderedDaypartRow struct { Key string Summary briefing.DerivedDaypartSummaryModule } func orderedDaypartRows(dayparts *map[string]briefing.DerivedDaypartSummaryModule, ordered []forecast.DaypartSummary) []orderedDaypartRow { if dayparts == nil || len(*dayparts) == 0 { return nil } out := make([]orderedDaypartRow, 0, len(*dayparts)) seen := map[string]struct{}{} for _, daypart := range ordered { for _, key := range daypartModuleKeyCandidates(daypart) { value, ok := (*dayparts)[key] if !ok { continue } if _, exists := seen[key]; exists { continue } seen[key] = struct{}{} out = append(out, orderedDaypartRow{Key: key, Summary: value}) break } } var remaining []string for key := range *dayparts { if _, ok := seen[key]; !ok { remaining = append(remaining, key) } } sort.Strings(remaining) for _, key := range remaining { out = append(out, orderedDaypartRow{Key: key, Summary: (*dayparts)[key]}) } return out } func daypartModuleKeyCandidates(daypart forecast.DaypartSummary) []string { key := forecast.CanonicalDaypartKey(daypart.Name) if key == "" { key = "unnamed" } if daypart.Period.Start.IsZero() { return []string{key} } return []string{key, daypart.Period.Start.Format(timeutil.DateLayout) + "_" + key} } func locationName(identity briefing.PreparedIdentity) string { if identity.Location != nil { if identity.Location.Name != "" && identity.Location.Region != "" { return identity.Location.Name + ", " + identity.Location.Region } if identity.Location.Name != "" { return identity.Location.Name } } if identity.SourceLocation != "" { return identity.SourceLocation } if identity.SourceLocationID != "" { return identity.SourceLocationID } return "Unknown location" } func periodLabel(period timeutil.Period, location *time.Location) string { return fmt.Sprintf("%s to %s", timeLabel(period.Start, location), timeLabel(period.End, location)) } func timeLabel(value time.Time, location *time.Location) string { return value.In(location).Format("2006-01-02 at 3:04 PM") } func generatedAtLabel(value time.Time, location *time.Location) string { return value.In(location).Format("Monday, January 2, 2006 at 3:04 PM") }