From b36e198bfe6b146b21a0254cba2f1e96ca6768f1 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 16 Jun 2026 19:20:26 -0500 Subject: [PATCH] Update the shared precipitation timing template --- docs/internal/reporttemplate.md | 6 +-- docs/templates.md | 4 +- .../reporttemplate/reporttemplate_test.go | 46 ++++++++++++++++++- .../reporttemplate/templates/hourly.md.tmpl | 9 +--- .../partials/precipitation_timing.md.tmpl | 2 +- 5 files changed, 51 insertions(+), 16 deletions(-) diff --git a/docs/internal/reporttemplate.md b/docs/internal/reporttemplate.md index 6c17a89..437bb27 100644 --- a/docs/internal/reporttemplate.md +++ b/docs/internal/reporttemplate.md @@ -76,9 +76,9 @@ fail rendering instead of producing incomplete Markdown. Daily and Tomorrow call the shared `daypart_forecast` partial. Today calls `today_daypart_forecast` so it can omit elapsed or missing dayparts. Daily, -Today, and Tomorrow call the shared `precipitation_timing` partial. Partial -files are parsed with each top-level template at render time and receive the -same typed render context as the caller. +Today, Tomorrow, and Hourly call the shared `precipitation_timing` partial. +Partial files are parsed with each top-level template at render time and +receive the same typed render context as the caller. ## Schema Contract diff --git a/docs/templates.md b/docs/templates.md index 296dfe1..7cdb1c8 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -18,7 +18,7 @@ Shared named partials live under `internal/reporttemplate/templates/partials/`: - `daypart_forecast.md.tmpl`, used by Daily and Tomorrow - `today_daypart_forecast.md.tmpl`, used by Today -- `precipitation_timing.md.tmpl`, used by Daily, Today, and Tomorrow +- `precipitation_timing.md.tmpl`, used by Daily, Today, Tomorrow, and Hourly Templates are rendered from structured contexts such as `DailyRenderContext`, `TodayRenderContext`, `TomorrowRenderContext`, and `HourlyRenderContext`. @@ -294,7 +294,7 @@ Common fields: | `.Modules.PrecipTiming.PrecipitationWindows[].MaxPopTime` | string | Friendly local time for the window maximum. | | `.Modules.PrecipTiming.PrecipitationWindows[].MaxPopHourLabel` | string | Friendly hour label for the window maximum. | | `.Modules.PrecipTiming.PrecipitationWindows[].PrecipitationType` | string | Conservatively inferred precipitation type, such as `showers and thunderstorms`. | -| `.Modules.PrecipTiming.PrecipitationWindows[].ExpectationPhrase` | string | Probability-based sentence used by day-style templates. | +| `.Modules.PrecipTiming.PrecipitationWindows[].ExpectationPhrase` | string | Probability-based sentence used by precipitation timing templates. | | `.Modules.PrecipTiming.ThunderMentioned` | bool | Whether thunder is mentioned in the forecast text. | ### Daypart Summaries diff --git a/internal/reporttemplate/reporttemplate_test.go b/internal/reporttemplate/reporttemplate_test.go index 4fe3481..8baa11b 100644 --- a/internal/reporttemplate/reporttemplate_test.go +++ b/internal/reporttemplate/reporttemplate_test.go @@ -236,7 +236,7 @@ func TestRenderHourly(t *testing.T) { MaxPopPercent: intPtr(70), MaxPopTime: "10 AM", PrecipitationWindows: []testPrecipWindow{ - {PeriodBegins: "10 AM", PeriodBeginsHourLabel: "10:00 AM", PeriodEnds: "12 PM", PeriodEndsHourLabel: "12:00 PM", MaxPopPercent: intPtr(70), MaxPopTime: "10 AM", MaxPopHourLabel: "10:00 AM"}, + {PeriodBegins: "10 AM", PeriodBeginsHourLabel: "10:00 AM", PeriodEnds: "12 PM", PeriodEndsHourLabel: "12:00 PM", MaxPopPercent: intPtr(70), MaxPopTime: "10 AM", MaxPopHourLabel: "10:00 AM", ExpectationPhrase: "Expect showers."}, }, }, AlertDigest: &testAlertDigest{ @@ -272,7 +272,7 @@ func TestRenderHourly(t *testing.T) { "- **Flood Watch**: Flood Watch until 2:30 PM", "- **9:00 AM:** 74°F and cloudy.", "- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%.", - "- **10:00 AM** to **12:00 PM**: Precipitation is expected during this period. The peak precipitation chance is 70% at 10:00 AM.", + "- **10:00 AM** to **12:00 PM**: Expect showers. The peak precipitation chance is 70% at 10:00 AM.", "A cold front is moving into the region.", "A front will keep the region unsettled.", } { @@ -732,6 +732,48 @@ func TestRenderHourlyOmitsConditionalSectionsForClearWeather(t *testing.T) { } } +func TestRenderHourlyUsesSharedOpenEndedPrecipitationTiming(t *testing.T) { + rendered, err := Render("hourly", testRenderContext{ + Report: testReportContext{ + Title: "Hourly Report", + GeneratedAtLabel: "Saturday, June 14, 2026 at 9:14 AM", + }, + GeneratedText: testGeneratedText{ + Summary: "Rain chances increase through midday.", + ForecastDiscussion: "Showers may continue beyond the report period.", + PrecipitationTiming: "Plan for wet roads through the end of the period.", + }, + Modules: testModules{ + CurrentConditions: &testCurrentConditions{}, + HourlyForecast: &testHourlyForecast{}, + AlertDigest: &testAlertDigest{}, + PrecipTiming: &testPrecipTiming{ + PrecipitationWindows: []testPrecipWindow{ + { + PeriodBeginsHourLabel: "8:00 AM", + MaxPopPercent: intPtr(60), + MaxPopHourLabel: "10:00 AM", + ExpectationPhrase: "Showers likely.", + }, + }, + }, + }, + }) + if err != nil { + t.Fatalf("Render() error = %v", err) + } + text := string(rendered) + for _, want := range []string{ + "## Precipitation Timing", + "- **Starting at 8:00 AM**: Showers likely. The peak precipitation chance is 60% at 10:00 AM.", + "Plan for wet roads through the end of the period.", + } { + if !strings.Contains(text, want) { + t.Fatalf("rendered template missing %q:\n%s", want, text) + } + } +} + func TestUnknownAssetsReturnActionableErrors(t *testing.T) { if _, err := Template("missing"); err == nil || !strings.Contains(err.Error(), `unknown report template "missing"`) { t.Fatalf("Template() error = %v, want unknown template", err) diff --git a/internal/reporttemplate/templates/hourly.md.tmpl b/internal/reporttemplate/templates/hourly.md.tmpl index 1513994..988d25a 100644 --- a/internal/reporttemplate/templates/hourly.md.tmpl +++ b/internal/reporttemplate/templates/hourly.md.tmpl @@ -26,14 +26,7 @@ Current conditions are unavailable. - No hourly forecast rows are available. {{ end }} -{{ with .Modules.PrecipTiming }}{{ with .PrecipitationWindows }} -## Precipitation Timing -{{ range . }}{{ $window := . }} -- **{{ if .PeriodBeginsHourLabel }}{{ .PeriodBeginsHourLabel }}{{ else }}{{ .PeriodBegins }}{{ end }}**{{ with .PeriodEndsHourLabel }} to **{{ . }}**{{ else }}{{ with .PeriodEnds }} to **{{ . }}**{{ end }}{{ end }}: Precipitation is expected during this period.{{ with .MaxPopPercent }} The peak precipitation chance is {{ . }}%{{ with $window.MaxPopHourLabel }} at {{ . }}{{ else }}{{ with $window.MaxPopTime }} at {{ . }}{{ end }}{{ end }}.{{ end }} -{{ end }}{{ with $.GeneratedText.PrecipitationTiming }} -{{ . }} -{{ end }} -{{ end }}{{ end }} +{{ template "precipitation_timing" . }} ## Forecast Discussion {{ .GeneratedText.ForecastDiscussion }} diff --git a/internal/reporttemplate/templates/partials/precipitation_timing.md.tmpl b/internal/reporttemplate/templates/partials/precipitation_timing.md.tmpl index af82446..f636a93 100644 --- a/internal/reporttemplate/templates/partials/precipitation_timing.md.tmpl +++ b/internal/reporttemplate/templates/partials/precipitation_timing.md.tmpl @@ -1,7 +1,7 @@ {{ define "precipitation_timing" }}{{ with .Modules.PrecipTiming }}{{ with .PrecipitationWindows }} ## Precipitation Timing {{ range . }}{{ $window := . }} -- **{{ if .PeriodBeginsHourLabel }}{{ .PeriodBeginsHourLabel }}{{ else }}{{ .PeriodBegins }}{{ end }}**{{ with .PeriodEndsHourLabel }} to **{{ . }}**{{ else }}{{ with .PeriodEnds }} to **{{ . }}**{{ end }}{{ end }}: {{ with .ExpectationPhrase }}{{ . }}{{ else }}Chance of precipitation.{{ end }}{{ with .MaxPopPercent }} The peak precipitation chance is {{ . }}%{{ with $window.MaxPopHourLabel }} at {{ . }}{{ else }}{{ with $window.MaxPopTime }} at {{ . }}{{ end }}{{ end }}.{{ end }} +- **{{ if or .PeriodEndsHourLabel .PeriodEnds }}{{ if .PeriodBeginsHourLabel }}{{ .PeriodBeginsHourLabel }}{{ else }}{{ .PeriodBegins }}{{ end }}{{ else }}Starting at {{ if .PeriodBeginsHourLabel }}{{ .PeriodBeginsHourLabel }}{{ else }}{{ .PeriodBegins }}{{ end }}{{ end }}**{{ with .PeriodEndsHourLabel }} to **{{ . }}**{{ else }}{{ with .PeriodEnds }} to **{{ . }}**{{ end }}{{ end }}: {{ with .ExpectationPhrase }}{{ . }}{{ else }}Chance of precipitation.{{ end }}{{ with .MaxPopPercent }} The peak precipitation chance is {{ . }}%{{ with $window.MaxPopHourLabel }} at {{ . }}{{ else }}{{ with $window.MaxPopTime }} at {{ . }}{{ end }}{{ end }}.{{ end }} {{ end }}{{ with $.GeneratedText.PrecipitationTiming }} {{ . }} {{ end }}