package generatedtext import ( "fmt" "sort" "strings" "time" "unicode" "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/timeutil" ) type HourlyRenderContext struct { Report HourlyReportContext GeneratedText Hourly Modules HourlyTemplateModules Collected facts.CollectedFacts Derived facts.DerivedFacts } type HourlyReportContext struct { Title string LocationName string GeneratedAt time.Time GeneratedAtLabel string ValidPeriod timeutil.Period ValidPeriodLabel string Timezone string } type HourlyTemplateModules struct { Metadata *briefing.MetadataModule 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 Collected facts.CollectedFacts Derived facts.DerivedFacts } type DailyRenderContext struct { Report DailyReportContext GeneratedText Daily Modules DailyTemplateModules Collected facts.CollectedFacts Derived facts.DerivedFacts } type TodayRenderContext struct { Report TodayReportContext GeneratedText Today Modules TodayTemplateModules Collected facts.CollectedFacts Derived facts.DerivedFacts } 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 { Metadata *briefing.MetadataModule CurrentConditions *briefing.CurrentConditionsModule HourlyForecast *briefing.HourlyForecastModule DerivedDailySummary *briefing.DerivedDailySummaryModule DerivedDaypartSummaries *map[string]briefing.DerivedDaypartSummaryModule Dayparts []TodayDaypartContext 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 { Metadata *briefing.MetadataModule 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 { Metadata *briefing.MetadataModule 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 { Metadata *briefing.MetadataModule 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(metadata briefing.Metadata, snapshot module.Snapshot, generated Hourly, collected facts.CollectedFacts, derived facts.DerivedFacts) (HourlyRenderContext, error) { location, err := timeutil.LoadLocation(metadata.Timezone) if err != nil { return HourlyRenderContext{}, fmt.Errorf("build hourly render context: %w", err) } if metadata.GeneratedAt.IsZero() { return HourlyRenderContext{}, fmt.Errorf("build hourly render context: generatedAt is required") } if !metadata.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(metadata), GeneratedAt: metadata.GeneratedAt, GeneratedAtLabel: generatedAtLabel(metadata.GeneratedAt, location), ValidPeriod: metadata.ValidPeriod, ValidPeriodLabel: periodLabel(metadata.ValidPeriod, location), Timezone: metadata.Timezone, }, GeneratedText: generated, Modules: modules, Collected: collected, Derived: derived, }, nil } func BuildDailyRenderContext(metadata briefing.Metadata, snapshot module.Snapshot, generated Daily, collected facts.CollectedFacts, derived facts.DerivedFacts) (DailyRenderContext, error) { reportContext, err := buildDayStyleReportContext(metadata, "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, Collected: collected, Derived: derived, }, nil } func BuildTodayRenderContext(metadata briefing.Metadata, snapshot module.Snapshot, generated Today, collected facts.CollectedFacts, derived facts.DerivedFacts) (TodayRenderContext, error) { reportContext, err := buildDayStyleReportContext(metadata, "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, Collected: collected, Derived: derived, }, nil } func BuildTomorrowRenderContext(metadata briefing.Metadata, snapshot module.Snapshot, generated Tomorrow, collected facts.CollectedFacts, derived facts.DerivedFacts) (TomorrowRenderContext, error) { reportContext, err := buildDayStyleReportContext(metadata, "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, Collected: collected, Derived: derived, }, nil } func buildDayStyleReportContext(metadata briefing.Metadata, name string, title func(string) string) (dayStyleReportContext, error) { location, err := timeutil.LoadLocation(metadata.Timezone) if err != nil { return dayStyleReportContext{}, fmt.Errorf("build %s render context: %w", name, err) } if metadata.GeneratedAt.IsZero() { return dayStyleReportContext{}, fmt.Errorf("build %s render context: generatedAt is required", name) } if !metadata.ValidPeriod.IsValid() { return dayStyleReportContext{}, fmt.Errorf("build %s render context: valid period is required", name) } forecastDate := metadata.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: metadata.GeneratedAt, GeneratedAtLabel: generatedAtLabel(metadata.GeneratedAt, location), ValidPeriod: metadata.ValidPeriod, Timezone: metadata.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{ Metadata: common.Metadata, 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 } return TodayTemplateModules{ Metadata: common.Metadata, CurrentConditions: common.CurrentConditions, HourlyForecast: common.HourlyForecast, DerivedDailySummary: common.DerivedDailySummary, DerivedDaypartSummaries: common.DerivedDaypartSummaries, Dayparts: orderedTodayDayparts(common.DerivedDaypartSummaries, derived.DaypartSummaries), 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) metadata, err := lookup.metadata() if err != nil { return HourlyTemplateModules{}, err } 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{ Metadata: metadata, 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{ Metadata: common.Metadata, 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) metadata, err := lookup.metadata() if err != nil { return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err } 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{ Metadata: metadata, 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) metadata() (*briefing.MetadataModule, error) { return optionalStanza[briefing.MetadataModule](lookup, module.Metadata) } 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 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 := normalizedDaypartKey(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 normalizedDaypartKey(value string) string { lower := strings.ToLower(strings.TrimSpace(value)) var out strings.Builder lastUnderscore := false for _, r := range lower { if unicode.IsLetter(r) || unicode.IsDigit(r) { out.WriteRune(r) lastUnderscore = false continue } if !lastUnderscore { out.WriteByte('_') lastUnderscore = true } } return strings.Trim(out.String(), "_") } func locationName(metadata briefing.Metadata) string { if metadata.Location != nil { if metadata.Location.Name != "" && metadata.Location.Region != "" { return metadata.Location.Name + ", " + metadata.Location.Region } if metadata.Location.Name != "" { return metadata.Location.Name } } if metadata.SourceLocation != "" { return metadata.SourceLocation } if metadata.SourceLocationID != "" { return metadata.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") }