Files
weatherreporter/internal/generatedtext/render_context.go

753 lines
26 KiB
Go

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")
}