Refactor the template variable framework
This commit is contained in:
@@ -2,44 +2,45 @@ package generatedtext
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
)
|
||||
|
||||
type HourlyRenderContext struct {
|
||||
ReportTitle string
|
||||
LocationName string
|
||||
ValidPeriod string
|
||||
GeneratedAt string
|
||||
GeneratedText Hourly
|
||||
CurrentConditions string
|
||||
HourlyForecast []HourlyForecastRow
|
||||
PrecipitationTiming string
|
||||
Alerts []string
|
||||
SPCOutlooks []string
|
||||
ForecastDiscussion ForecastDiscussion
|
||||
SPCDiscussions []string
|
||||
WeatherStory string
|
||||
Report HourlyReportContext
|
||||
GeneratedText Hourly
|
||||
Modules HourlyTemplateModules
|
||||
Collected facts.CollectedFacts
|
||||
Derived facts.DerivedFacts
|
||||
}
|
||||
|
||||
type HourlyForecastRow struct {
|
||||
Time string
|
||||
Summary string
|
||||
Temperature string
|
||||
Precipitation string
|
||||
Wind string
|
||||
type HourlyReportContext struct {
|
||||
Title string
|
||||
LocationName string
|
||||
GeneratedAt time.Time
|
||||
GeneratedAtLabel string
|
||||
ValidPeriod timeutil.Period
|
||||
ValidPeriodLabel string
|
||||
Timezone string
|
||||
}
|
||||
|
||||
type ForecastDiscussion struct {
|
||||
KeyMessages []string
|
||||
ShortTerm 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
|
||||
}
|
||||
|
||||
func BuildHourlyRenderContext(metadata briefing.Metadata, snapshot module.Snapshot, generated Hourly) (HourlyRenderContext, error) {
|
||||
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)
|
||||
@@ -50,80 +51,87 @@ func BuildHourlyRenderContext(metadata briefing.Metadata, snapshot module.Snapsh
|
||||
if !metadata.ValidPeriod.IsValid() {
|
||||
return HourlyRenderContext{}, fmt.Errorf("build hourly render context: valid period is required")
|
||||
}
|
||||
|
||||
current, err := requiredStanza[briefing.CurrentConditionsModule](snapshot, string(module.CurrentConditions))
|
||||
modules, err := hourlyTemplateModules(snapshot)
|
||||
if err != nil {
|
||||
return HourlyRenderContext{}, err
|
||||
}
|
||||
hourly, err := requiredStanza[briefing.HourlyForecastModule](snapshot, string(module.HourlyForecast))
|
||||
if err != nil {
|
||||
return HourlyRenderContext{}, err
|
||||
}
|
||||
precip, err := requiredStanza[briefing.PrecipTimingModule](snapshot, string(module.PrecipTiming))
|
||||
if err != nil {
|
||||
return HourlyRenderContext{}, err
|
||||
}
|
||||
alerts, err := requiredStanza[briefing.AlertDigestModule](snapshot, string(module.AlertDigest))
|
||||
if err != nil {
|
||||
return HourlyRenderContext{}, err
|
||||
}
|
||||
outlooks, err := requiredStanza[briefing.SPCConvectiveOutlooksModule](snapshot, string(module.SPCConvectiveOutlooks))
|
||||
if err != nil {
|
||||
return HourlyRenderContext{}, err
|
||||
}
|
||||
discussion, err := optionalStanza[briefing.AreaForecastDiscussionModule](snapshot, string(module.AreaForecastDiscussion))
|
||||
if err != nil {
|
||||
return HourlyRenderContext{}, err
|
||||
}
|
||||
spcDiscussion, err := optionalStanza[briefing.SPCConvectiveDiscussionModule](snapshot, string(module.SPCConvectiveDiscussion))
|
||||
if err != nil {
|
||||
return HourlyRenderContext{}, err
|
||||
}
|
||||
story, err := optionalStanza[briefing.WeatherStoryModule](snapshot, string(module.WeatherStory))
|
||||
if err != nil {
|
||||
return HourlyRenderContext{}, err
|
||||
}
|
||||
|
||||
return HourlyRenderContext{
|
||||
ReportTitle: "Hourly Report",
|
||||
LocationName: locationName(metadata),
|
||||
ValidPeriod: periodLabel(metadata.ValidPeriod, location),
|
||||
GeneratedAt: timeLabel(metadata.GeneratedAt, location),
|
||||
GeneratedText: generated,
|
||||
CurrentConditions: currentConditionsLabel(current),
|
||||
HourlyForecast: hourlyForecastRows(hourly),
|
||||
PrecipitationTiming: precipitationTimingLabel(precip),
|
||||
Alerts: alertLabels(alerts),
|
||||
SPCOutlooks: outlookLabels(outlooks),
|
||||
ForecastDiscussion: ForecastDiscussion{
|
||||
KeyMessages: append([]string(nil), discussion.KeyMessages...),
|
||||
ShortTerm: discussion.ShortTerm,
|
||||
Report: HourlyReportContext{
|
||||
Title: "Hourly Report",
|
||||
LocationName: locationName(metadata),
|
||||
GeneratedAt: metadata.GeneratedAt,
|
||||
GeneratedAtLabel: timeLabel(metadata.GeneratedAt, location),
|
||||
ValidPeriod: metadata.ValidPeriod,
|
||||
ValidPeriodLabel: periodLabel(metadata.ValidPeriod, location),
|
||||
Timezone: metadata.Timezone,
|
||||
},
|
||||
SPCDiscussions: spcDiscussionLabels(spcDiscussion),
|
||||
WeatherStory: weatherStoryLabel(story),
|
||||
GeneratedText: generated,
|
||||
Modules: modules,
|
||||
Collected: collected,
|
||||
Derived: derived,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func requiredStanza[T any](snapshot module.Snapshot, name string) (T, error) {
|
||||
value, ok, err := module.StanzaValue[T](snapshot, name)
|
||||
func hourlyTemplateModules(snapshot module.Snapshot) (HourlyTemplateModules, error) {
|
||||
metadata, err := optionalStanza[briefing.MetadataModule](snapshot, string(module.Metadata))
|
||||
if err != nil {
|
||||
var zero T
|
||||
return zero, fmt.Errorf("build hourly render context: %w", err)
|
||||
return HourlyTemplateModules{}, err
|
||||
}
|
||||
if !ok {
|
||||
var zero T
|
||||
return zero, fmt.Errorf("build hourly render context requires stanza %q", name)
|
||||
current, err := optionalStanza[briefing.CurrentConditionsModule](snapshot, string(module.CurrentConditions))
|
||||
if err != nil {
|
||||
return HourlyTemplateModules{}, err
|
||||
}
|
||||
return value, nil
|
||||
hourly, err := optionalStanza[briefing.HourlyForecastModule](snapshot, string(module.HourlyForecast))
|
||||
if err != nil {
|
||||
return HourlyTemplateModules{}, err
|
||||
}
|
||||
precip, err := optionalStanza[briefing.PrecipTimingModule](snapshot, string(module.PrecipTiming))
|
||||
if err != nil {
|
||||
return HourlyTemplateModules{}, err
|
||||
}
|
||||
alerts, err := optionalStanza[briefing.AlertDigestModule](snapshot, string(module.AlertDigest))
|
||||
if err != nil {
|
||||
return HourlyTemplateModules{}, err
|
||||
}
|
||||
outlooks, err := optionalStanza[briefing.SPCConvectiveOutlooksModule](snapshot, string(module.SPCConvectiveOutlooks))
|
||||
if err != nil {
|
||||
return HourlyTemplateModules{}, err
|
||||
}
|
||||
discussion, err := optionalStanza[briefing.AreaForecastDiscussionModule](snapshot, string(module.AreaForecastDiscussion))
|
||||
if err != nil {
|
||||
return HourlyTemplateModules{}, err
|
||||
}
|
||||
spcDiscussion, err := optionalStanza[briefing.SPCConvectiveDiscussionModule](snapshot, string(module.SPCConvectiveDiscussion))
|
||||
if err != nil {
|
||||
return HourlyTemplateModules{}, err
|
||||
}
|
||||
story, err := optionalStanza[briefing.WeatherStoryModule](snapshot, string(module.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 optionalStanza[T any](snapshot module.Snapshot, name string) (T, error) {
|
||||
func optionalStanza[T any](snapshot module.Snapshot, name string) (*T, error) {
|
||||
output, ok := snapshot.LookupStanza(name)
|
||||
if !ok || output.Value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
value, _, err := module.StanzaValue[T](snapshot, name)
|
||||
if err != nil {
|
||||
var zero T
|
||||
return zero, fmt.Errorf("build hourly render context: %w", err)
|
||||
return nil, fmt.Errorf("build hourly render context: %w", err)
|
||||
}
|
||||
return value, nil
|
||||
return &value, nil
|
||||
}
|
||||
|
||||
func locationName(metadata briefing.Metadata) string {
|
||||
@@ -151,210 +159,3 @@ func periodLabel(period timeutil.Period, location *time.Location) string {
|
||||
func timeLabel(value time.Time, location *time.Location) string {
|
||||
return value.In(location).Format("2006-01-02 at 3:04 PM")
|
||||
}
|
||||
|
||||
func currentConditionsLabel(current briefing.CurrentConditionsModule) string {
|
||||
parts := []string{}
|
||||
if current.ConditionText != "" {
|
||||
parts = append(parts, current.ConditionText)
|
||||
}
|
||||
if temperature := temperatureLabel(current.TemperatureF, current.TemperatureC); temperature != "" {
|
||||
parts = append(parts, temperature)
|
||||
}
|
||||
if apparent := temperatureLabel(current.ApparentTemperatureF, current.ApparentTemperatureC); apparent != "" {
|
||||
parts = append(parts, "feels like "+apparent)
|
||||
}
|
||||
if current.RelativeHumidityPercent != nil {
|
||||
parts = append(parts, fmt.Sprintf("humidity %d%%", rounded(*current.RelativeHumidityPercent)))
|
||||
}
|
||||
if wind := windLabel(current.WindDirection, current.WindSpeedMph, current.WindSpeedKmh, nil, nil); wind != "" {
|
||||
parts = append(parts, wind)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "No current conditions available."
|
||||
}
|
||||
return strings.Join(parts, "; ") + "."
|
||||
}
|
||||
|
||||
func hourlyForecastRows(hourly briefing.HourlyForecastModule) []HourlyForecastRow {
|
||||
rows := make([]HourlyForecastRow, 0, len(hourly.Periods))
|
||||
for _, period := range hourly.Periods {
|
||||
summary := period.TextDescription
|
||||
if summary == "" {
|
||||
summary = period.Name
|
||||
}
|
||||
rows = append(rows, HourlyForecastRow{
|
||||
Time: firstNonEmpty(period.PeriodBegins, period.Name),
|
||||
Summary: summary,
|
||||
Temperature: temperatureLabel(period.TemperatureF, period.TemperatureC),
|
||||
Precipitation: precipitationLabel(period.ProbabilityOfPrecipitationPercent),
|
||||
Wind: windLabel(period.WindDirection, period.WindSpeedMph, period.WindSpeedKmh, period.WindGustMph, period.WindGustKmh),
|
||||
})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func precipitationTimingLabel(timing briefing.PrecipTimingModule) string {
|
||||
parts := []string{}
|
||||
if timing.MaxPopPercent != nil {
|
||||
max := fmt.Sprintf("Peak precipitation probability %d%%", *timing.MaxPopPercent)
|
||||
if timing.MaxPopTime != "" {
|
||||
max += " at " + timing.MaxPopTime
|
||||
}
|
||||
parts = append(parts, max)
|
||||
}
|
||||
for _, window := range timing.PrecipitationWindows {
|
||||
label := window.PeriodBegins
|
||||
if window.PeriodEnds != "" {
|
||||
label += " to " + window.PeriodEnds
|
||||
}
|
||||
if window.MaxPopPercent != nil {
|
||||
label += fmt.Sprintf(" (max %d%%", *window.MaxPopPercent)
|
||||
if window.MaxPopTime != "" {
|
||||
label += " at " + window.MaxPopTime
|
||||
}
|
||||
label += ")"
|
||||
}
|
||||
parts = append(parts, label)
|
||||
}
|
||||
if timing.ThunderMentioned {
|
||||
parts = append(parts, "Thunder is mentioned in the forecast.")
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "No precipitation timing signal above threshold."
|
||||
}
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
func alertLabels(alerts briefing.AlertDigestModule) []string {
|
||||
if alerts.Missing {
|
||||
return []string{"Alert source missing."}
|
||||
}
|
||||
out := make([]string, 0, len(alerts.Relevant))
|
||||
for _, alert := range alerts.Relevant {
|
||||
main := firstNonEmpty(alert.Event, alert.Headline)
|
||||
if main == "" {
|
||||
continue
|
||||
}
|
||||
if alert.Headline != "" && alert.Headline != main {
|
||||
main += ": " + alert.Headline
|
||||
}
|
||||
if alert.Severity != "" {
|
||||
main += " (" + alert.Severity + ")"
|
||||
}
|
||||
out = append(out, main)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func outlookLabels(outlooks briefing.SPCConvectiveOutlooksModule) []string {
|
||||
out := make([]string, 0, len(outlooks.Outlooks))
|
||||
for _, outlook := range outlooks.Outlooks {
|
||||
label := firstNonEmpty(outlook.LabelText, outlook.Label, outlook.OutlookType)
|
||||
if label == "" {
|
||||
continue
|
||||
}
|
||||
if outlook.PeriodBegins != "" {
|
||||
label += " from " + outlook.PeriodBegins
|
||||
if outlook.PeriodEnds != "" {
|
||||
label += " to " + outlook.PeriodEnds
|
||||
}
|
||||
}
|
||||
out = append(out, label)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func spcDiscussionLabels(discussion briefing.SPCConvectiveDiscussionModule) []string {
|
||||
out := make([]string, 0, len(discussion.Discussions))
|
||||
for _, record := range discussion.Discussions {
|
||||
label := firstNonEmpty(record.Headline, record.Summary, record.Discussion)
|
||||
if label == "" {
|
||||
continue
|
||||
}
|
||||
if record.Summary != "" && record.Summary != label {
|
||||
label += ": " + record.Summary
|
||||
}
|
||||
out = append(out, label)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func weatherStoryLabel(story briefing.WeatherStoryModule) string {
|
||||
if !story.Available {
|
||||
return "No weather story available."
|
||||
}
|
||||
parts := []string{}
|
||||
if story.Title != "" {
|
||||
parts = append(parts, story.Title)
|
||||
}
|
||||
if story.Description != "" {
|
||||
parts = append(parts, story.Description)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "Weather story is available."
|
||||
}
|
||||
return strings.Join(parts, " - ")
|
||||
}
|
||||
|
||||
func temperatureLabel(fahrenheit *float64, celsius *float64) string {
|
||||
if fahrenheit != nil {
|
||||
return fmt.Sprintf("%d F", rounded(*fahrenheit))
|
||||
}
|
||||
if celsius != nil {
|
||||
return fmt.Sprintf("%d C", rounded(*celsius))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func precipitationLabel(percent *float64) string {
|
||||
if percent == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%d%% precipitation", rounded(*percent))
|
||||
}
|
||||
|
||||
func windLabel(direction string, mph *float64, kmh *float64, gustMph *float64, gustKmh *float64) string {
|
||||
speed := ""
|
||||
if mph != nil {
|
||||
speed = fmt.Sprintf("%d mph", rounded(*mph))
|
||||
} else if kmh != nil {
|
||||
speed = fmt.Sprintf("%d km/h", rounded(*kmh))
|
||||
}
|
||||
if direction != "" && speed != "" {
|
||||
speed = direction + " " + speed
|
||||
} else if direction != "" {
|
||||
speed = direction + " wind"
|
||||
}
|
||||
gust := ""
|
||||
if gustMph != nil {
|
||||
gust = fmt.Sprintf("gusts %d mph", rounded(*gustMph))
|
||||
} else if gustKmh != nil {
|
||||
gust = fmt.Sprintf("gusts %d km/h", rounded(*gustKmh))
|
||||
}
|
||||
switch {
|
||||
case speed != "" && gust != "":
|
||||
return "wind " + speed + ", " + gust
|
||||
case speed != "":
|
||||
return "wind " + speed
|
||||
case gust != "":
|
||||
return "wind " + gust
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func rounded(value float64) int {
|
||||
if value < 0 {
|
||||
return int(value - 0.5)
|
||||
}
|
||||
return int(value + 0.5)
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user