From f9d6d42b1b06141076a61cb9de995d569ddc78b9 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 16 Jun 2026 19:10:11 -0500 Subject: [PATCH] Updated precipitation timing language in the shared template --- docs/templates.md | 2 + internal/briefing/derived_modules_test.go | 103 ++++++++++++++++++ internal/briefing/precip_timing_module.go | 54 +++++++++ internal/forecast/derive.go | 15 +++ internal/forecast/derive_test.go | 6 + internal/generatedtext/render_context_test.go | 12 +- .../reporttemplate/reporttemplate_test.go | 14 ++- .../partials/precipitation_timing.md.tmpl | 2 +- 8 files changed, 195 insertions(+), 13 deletions(-) diff --git a/docs/templates.md b/docs/templates.md index a92a806..296dfe1 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -293,6 +293,8 @@ Common fields: | `.Modules.PrecipTiming.PrecipitationWindows[].MaxPopPercent` | *int | Highest precipitation probability inside the window. | | `.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.ThunderMentioned` | bool | Whether thunder is mentioned in the forecast text. | ### Daypart Summaries diff --git a/internal/briefing/derived_modules_test.go b/internal/briefing/derived_modules_test.go index 7a89854..2f2c7df 100644 --- a/internal/briefing/derived_modules_test.go +++ b/internal/briefing/derived_modules_test.go @@ -105,9 +105,15 @@ func TestPrecipTimingModuleHandlesRainyAndDryForecasts(t *testing.T) { if rainy.PrecipitationWindows[0].PeriodBegins != "2026-05-29 at 8:00 AM" || rainy.PrecipitationWindows[0].PeriodBeginsHourLabel != "8:00 AM" || rainy.PrecipitationWindows[0].PeriodEnds != "2026-05-29 at 9:00 AM" || rainy.PrecipitationWindows[0].PeriodEndsHourLabel != "9:00 AM" || rainy.PrecipitationWindows[0].MaxPopPercent == nil || *rainy.PrecipitationWindows[0].MaxPopPercent != 60 || rainy.PrecipitationWindows[0].MaxPopHourLabel != "8:00 AM" { t.Fatalf("first precipitation window = %#v, want 8-9 AM at 60%%", rainy.PrecipitationWindows[0]) } + if rainy.PrecipitationWindows[0].PrecipitationType != "showers" || rainy.PrecipitationWindows[0].ExpectationPhrase != "Showers likely." { + t.Fatalf("first precipitation window phrase = %#v, want showers likely", rainy.PrecipitationWindows[0]) + } if rainy.PrecipitationWindows[1].PeriodBegins != "2026-05-29 at 12:00 PM" || rainy.PrecipitationWindows[1].PeriodBeginsHourLabel != "12:00 PM" || rainy.PrecipitationWindows[1].PeriodEnds != "2026-05-29 at 2:00 PM" || rainy.PrecipitationWindows[1].PeriodEndsHourLabel != "2:00 PM" || rainy.PrecipitationWindows[1].MaxPopPercent == nil || *rainy.PrecipitationWindows[1].MaxPopPercent != 80 || rainy.PrecipitationWindows[1].MaxPopHourLabel != "12:00 PM" { t.Fatalf("second precipitation window = %#v, want noon-2 PM at 80%%", rainy.PrecipitationWindows[1]) } + if rainy.PrecipitationWindows[1].PrecipitationType != "showers and thunderstorms" || rainy.PrecipitationWindows[1].ExpectationPhrase != "Expect showers and thunderstorms." { + t.Fatalf("second precipitation window phrase = %#v, want expect showers and thunderstorms", rainy.PrecipitationWindows[1]) + } data, err := json.Marshal(output.Value) if err != nil { t.Fatalf("marshal precip timing: %v", err) @@ -115,6 +121,9 @@ func TestPrecipTimingModuleHandlesRainyAndDryForecasts(t *testing.T) { if !strings.Contains(string(data), "precipitation_windows") || !strings.Contains(string(data), "probability_threshold") || !strings.Contains(string(data), "period_begins_hour_label") || !strings.Contains(string(data), "max_pop_hour_label") { t.Fatalf("precip timing json = %s, want threshold and windows", string(data)) } + if !strings.Contains(string(data), "precipitation_type") || !strings.Contains(string(data), "expectation_phrase") { + t.Fatalf("precip timing json = %s, want precipitation type and expectation phrase", string(data)) + } if strings.Contains(string(data), `"start"`) || strings.Contains(string(data), `"end"`) { t.Fatalf("precip timing json = %s, want period_begins/period_ends instead of start/end", string(data)) } @@ -136,6 +145,100 @@ func TestPrecipTimingModuleHandlesRainyAndDryForecasts(t *testing.T) { } } +func TestPrecipTimingModuleBuildsExpectationPhrases(t *testing.T) { + now := mustParseModuleTime("2026-05-29T08:00:00-05:00") + tests := []struct { + name string + maxPop float64 + descriptions []string + wantType string + wantPhrase string + }{ + { + name: "chance lower bound", + maxPop: 40, + descriptions: []string{"Scattered showers"}, + wantType: "showers", + wantPhrase: "Chance of showers.", + }, + { + name: "chance upper bound", + maxPop: 49, + descriptions: []string{"Rain possible"}, + wantType: "rain", + wantPhrase: "Chance of rain.", + }, + { + name: "likely lower bound", + maxPop: 50, + descriptions: []string{"Drizzle"}, + wantType: "drizzle", + wantPhrase: "Drizzle likely.", + }, + { + name: "likely upper bound", + maxPop: 69, + descriptions: []string{"Freezing rain"}, + wantType: "freezing rain", + wantPhrase: "Freezing rain likely.", + }, + { + name: "expect lower bound", + maxPop: 70, + descriptions: []string{"Snow"}, + wantType: "snow", + wantPhrase: "Expect snow.", + }, + { + name: "showers and thunderstorms preferred", + maxPop: 100, + descriptions: []string{"Showers likely", "Thunderstorms possible"}, + wantType: "showers and thunderstorms", + wantPhrase: "Expect showers and thunderstorms.", + }, + { + name: "thunderstorms only", + maxPop: 80, + descriptions: []string{"Thunderstorms"}, + wantType: "thunderstorms", + wantPhrase: "Expect thunderstorms.", + }, + { + name: "unknown fallback", + maxPop: 95, + descriptions: []string{"Unsettled conditions"}, + wantType: "precipitation", + wantPhrase: "Expect precipitation.", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value := precipTimingValue(forecast.PrecipTiming{ + ProbabilityThreshold: forecast.DefaultPrecipWindowProbabilityThreshold, + PrecipitationWindows: []forecast.PrecipitationWindow{ + { + Start: now, + MaxPrecipitationProbability: forecast.TimedValue{ + Value: tt.maxPop, + Time: now, + }, + ProbabilityThreshold: forecast.DefaultPrecipWindowProbabilityThreshold, + TextDescriptions: tt.descriptions, + }, + }, + }, "America/Chicago") + if len(value.PrecipitationWindows) != 1 { + t.Fatalf("PrecipitationWindows = %#v, want one window", value.PrecipitationWindows) + } + window := value.PrecipitationWindows[0] + if window.PrecipitationType != tt.wantType || window.ExpectationPhrase != tt.wantPhrase { + t.Fatalf("window = %#v, want type %q and phrase %q", window, tt.wantType, tt.wantPhrase) + } + }) + } +} + func TestPrecipTimingModuleUsesDerivedTimingWithoutDaypartSummaries(t *testing.T) { registry := MustDefaultModuleRegistry() ctx := derivedModuleContext(report.Hourly) diff --git a/internal/briefing/precip_timing_module.go b/internal/briefing/precip_timing_module.go index c5c2be8..321d2bb 100644 --- a/internal/briefing/precip_timing_module.go +++ b/internal/briefing/precip_timing_module.go @@ -1,10 +1,19 @@ package briefing import ( + "fmt" + "strings" + "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" ) +const ( + precipTimingChanceLowerBound = 40 + precipTimingLikelyLowerBound = 50 + precipTimingExpectLowerBound = 70 +) + type PrecipTimingModule struct { MaxPopPercent *int `json:"max_pop_percent,omitempty"` MaxPopTime string `json:"max_pop_time,omitempty"` @@ -21,6 +30,8 @@ type PrecipitationWindowModule struct { MaxPopPercent *int `json:"max_pop_percent,omitempty"` MaxPopTime string `json:"max_pop_time,omitempty"` MaxPopHourLabel string `json:"max_pop_hour_label,omitempty"` + PrecipitationType string `json:"precipitation_type,omitempty"` + ExpectationPhrase string `json:"expectation_phrase,omitempty"` } func buildPrecipTimingModule(ctx ModuleContext, _ any) (*module.Output, error) { @@ -49,7 +60,50 @@ func precipTimingValue(timing forecast.PrecipTiming, timezone string) PrecipTimi item.MaxPopPercent = roundedInt(&window.MaxPrecipitationProbability.Value) item.MaxPopTime = clockLabel(window.MaxPrecipitationProbability.Time, timezone) item.MaxPopHourLabel = hourMinuteLabel(window.MaxPrecipitationProbability.Time, timezone) + item.PrecipitationType = precipitationWindowType(window.TextDescriptions) + if item.MaxPopPercent != nil { + item.ExpectationPhrase = precipitationWindowExpectationPhrase(*item.MaxPopPercent, item.PrecipitationType) + } value.PrecipitationWindows = append(value.PrecipitationWindows, item) } return value } + +func precipitationWindowType(descriptions []string) string { + combined := strings.ToLower(strings.Join(descriptions, " ")) + switch { + case (strings.Contains(combined, "thunderstorm") || strings.Contains(combined, "t-storm")) && + (strings.Contains(combined, "shower") || strings.Contains(combined, "rain")): + return "showers and thunderstorms" + case strings.Contains(combined, "freezing rain"): + return "freezing rain" + case strings.Contains(combined, "thunderstorm") || strings.Contains(combined, "t-storm"): + return "thunderstorms" + case strings.Contains(combined, "shower"): + return "showers" + case strings.Contains(combined, "snow"): + return "snow" + case strings.Contains(combined, "drizzle"): + return "drizzle" + case strings.Contains(combined, "rain"): + return "rain" + default: + return "precipitation" + } +} + +func precipitationWindowExpectationPhrase(maxPopPercent int, precipitationType string) string { + if precipitationType == "" { + precipitationType = "precipitation" + } + switch { + case maxPopPercent >= precipTimingExpectLowerBound: + return fmt.Sprintf("Expect %s.", precipitationType) + case maxPopPercent >= precipTimingLikelyLowerBound: + return fmt.Sprintf("%s likely.", sentenceCase(precipitationType)) + case maxPopPercent >= precipTimingChanceLowerBound: + return fmt.Sprintf("Chance of %s.", precipitationType) + default: + return "" + } +} diff --git a/internal/forecast/derive.go b/internal/forecast/derive.go index 9ee71d3..29110b7 100644 --- a/internal/forecast/derive.go +++ b/internal/forecast/derive.go @@ -81,6 +81,7 @@ type PrecipitationWindow struct { End *time.Time `json:"end,omitempty"` MaxPrecipitationProbability TimedValue `json:"maxPrecipitationProbability"` ProbabilityThreshold float64 `json:"probabilityThreshold"` + TextDescriptions []string `json:"textDescriptions,omitempty"` } func BuildPrecipTiming(periods []weatherdata.ForecastPeriod) PrecipTiming { @@ -114,6 +115,7 @@ func buildPrecipTimingWithThreshold(periods []weatherdata.ForecastPeriod, thresh }, ProbabilityThreshold: threshold, } + appendActiveTextDescription(active, forecastPeriod.TextDescription) activeLastEnd = forecastPeriod.EndTime if timing.FirstPrecipitation == nil { timing.FirstPrecipitation = &TimedValue{ @@ -135,6 +137,8 @@ func buildPrecipTimingWithThreshold(periods []weatherdata.ForecastPeriod, thresh if forecastPeriod.StartTime.After(activeLastEnd) { closeActive() startActive(forecastPeriod, probability) + } else { + appendActiveTextDescription(active, forecastPeriod.TextDescription) } if probability > active.MaxPrecipitationProbability.Value { active.MaxPrecipitationProbability = TimedValue{ @@ -166,6 +170,17 @@ func buildPrecipTimingWithThreshold(periods []weatherdata.ForecastPeriod, thresh return timing } +func appendActiveTextDescription(window *PrecipitationWindow, text string) { + if window == nil { + return + } + text = strings.TrimSpace(text) + if text == "" { + return + } + window.TextDescriptions = append(window.TextDescriptions, text) +} + func BuildDailySummary(bundle *weatherdata.Bundle, date time.Time, location *time.Location, dayparts []DaypartDefinition) (*DailySummary, error) { if bundle == nil { return nil, fmt.Errorf("forecast bundle is required") diff --git a/internal/forecast/derive_test.go b/internal/forecast/derive_test.go index 116dd64..5765d2e 100644 --- a/internal/forecast/derive_test.go +++ b/internal/forecast/derive_test.go @@ -245,10 +245,16 @@ func TestBuildPrecipTimingBuildsThresholdWindows(t *testing.T) { if first.MaxPrecipitationProbability.Value != 60 || first.MaxPrecipitationProbability.Time.Format(time.RFC3339) != "2026-05-29T10:00:00-05:00" { t.Fatalf("first window max = %#v, want 60 at 10 AM", first.MaxPrecipitationProbability) } + if len(first.TextDescriptions) != 2 || first.TextDescriptions[0] != "Showers" || first.TextDescriptions[1] != "Rain likely" { + t.Fatalf("first window text descriptions = %#v, want contributing hourly descriptions", first.TextDescriptions) + } second := timing.PrecipitationWindows[1] if second.Start.Format(time.RFC3339) != "2026-05-29T12:00:00-05:00" || second.End == nil || second.End.Format(time.RFC3339) != "2026-05-29T13:00:00-05:00" { t.Fatalf("second window = %#v, want noon-1 PM", second) } + if len(second.TextDescriptions) != 1 || second.TextDescriptions[0] != "Thunderstorms" { + t.Fatalf("second window text descriptions = %#v, want thunderstorm description", second.TextDescriptions) + } if !timing.ThunderMentioned { t.Fatal("ThunderMentioned = false, want true") } diff --git a/internal/generatedtext/render_context_test.go b/internal/generatedtext/render_context_test.go index d06d7e5..4b37919 100644 --- a/internal/generatedtext/render_context_test.go +++ b/internal/generatedtext/render_context_test.go @@ -219,7 +219,7 @@ func TestBuildTodayRenderContext(t *testing.T) { "- **Morning:** Partly cloudy, with temperatures in the low 60s.", "- **Afternoon:** Showers, with temperatures in the mid 70s. Chance of precipitation is 70%.", "## Precipitation Timing", - "- **3:00 PM** to **6:00 PM**: Precipitation is expected during this period. The peak precipitation chance is 70% at 3:00 PM.", + "- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM.", "The most likely rain window is from midafternoon into early evening.", "Morning conditions should stay mostly dry.", "Rain chances increase during the afternoon as deeper moisture arrives.", @@ -542,7 +542,7 @@ func TestBuildDailyRenderContext(t *testing.T) { "- **Afternoon:** Showers, with temperatures in the mid 70s. Chance of precipitation is 70%.", "- **Evening:** Mostly cloudy, with temperatures in the upper 60s.", "## Precipitation Timing", - "- **3:00 PM** to **6:00 PM**: Precipitation is expected during this period. The peak precipitation chance is 70% at 3:00 PM.", + "- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM.", "The most likely rain window is from midafternoon into early evening.", "Morning conditions should stay mostly dry.", "Rain chances increase during the afternoon as deeper moisture arrives.", @@ -663,7 +663,7 @@ func TestBuildTomorrowRenderContext(t *testing.T) { "- **Morning:** Partly cloudy, with temperatures in the low 60s.", "- **Afternoon:** Showers, with temperatures in the mid 70s. Chance of precipitation is 70%.", "## Precipitation Timing", - "- **3:00 PM** to **6:00 PM**: Precipitation is expected during this period. The peak precipitation chance is 70% at 3:00 PM.", + "- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM.", "The most likely rain window is from midafternoon into early evening.", "Morning conditions should stay mostly dry.", "Rain chances increase during the afternoon as deeper moisture arrives.", @@ -910,7 +910,7 @@ func testTodaySnapshot(t *testing.T) module.Snapshot { MaxPopPercent: intPtr(70), MaxPopTime: "3 PM", PrecipitationWindows: []briefing.PrecipitationWindowModule{ - {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM"}, + {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM", PrecipitationType: "showers", ExpectationPhrase: "Expect showers."}, }, }, }, @@ -1045,7 +1045,7 @@ func testDailySnapshot(t *testing.T) module.Snapshot { MaxPopPercent: intPtr(70), MaxPopTime: "3 PM", PrecipitationWindows: []briefing.PrecipitationWindowModule{ - {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM"}, + {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM", PrecipitationType: "showers", ExpectationPhrase: "Expect showers."}, }, }, }, @@ -1317,7 +1317,7 @@ func testTomorrowSnapshot(t *testing.T) module.Snapshot { MaxPopPercent: intPtr(70), MaxPopTime: "3 PM", PrecipitationWindows: []briefing.PrecipitationWindowModule{ - {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM"}, + {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM", PrecipitationType: "showers", ExpectationPhrase: "Expect showers."}, }, }, }, diff --git a/internal/reporttemplate/reporttemplate_test.go b/internal/reporttemplate/reporttemplate_test.go index a5f932b..4fe3481 100644 --- a/internal/reporttemplate/reporttemplate_test.go +++ b/internal/reporttemplate/reporttemplate_test.go @@ -348,7 +348,7 @@ func TestRenderTomorrow(t *testing.T) { }, PrecipTiming: &testPrecipTiming{ PrecipitationWindows: []testPrecipWindow{ - {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM"}, + {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM", ExpectationPhrase: "Expect showers."}, }, }, }, @@ -365,7 +365,7 @@ func TestRenderTomorrow(t *testing.T) { "- **Overnight:** Partly cloudy, with temperatures falling from the mid 60s to the upper 50s.", "- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.", "- **Afternoon:** Sunny, with temperatures in the upper 70s. Chance of precipitation is 70%.", - "- **3:00 PM** to **6:00 PM**: Precipitation is expected during this period. The peak precipitation chance is 70% at 3:00 PM.", + "- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM.", "A few showers may linger into early evening.", "Clouds increase after sunrise.", "Rain chances peak during the afternoon.", @@ -439,7 +439,7 @@ func TestRenderDaily(t *testing.T) { }, PrecipTiming: &testPrecipTiming{ PrecipitationWindows: []testPrecipWindow{ - {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM"}, + {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM", ExpectationPhrase: "Expect showers."}, }, }, }, @@ -455,7 +455,7 @@ func TestRenderDaily(t *testing.T) { "The selected day starts dry before showers return later in the day.", "- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.", "- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%.", - "- **3:00 PM** to **6:00 PM**: Precipitation is expected during this period. The peak precipitation chance is 70% at 3:00 PM.", + "- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM.", "A few showers may linger into early evening.", "Clouds increase after sunrise.", "Rain chances peak during the afternoon.", @@ -532,7 +532,7 @@ func TestRenderToday(t *testing.T) { }, PrecipTiming: &testPrecipTiming{ PrecipitationWindows: []testPrecipWindow{ - {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM"}, + {PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM", ExpectationPhrase: "Expect showers."}, }, }, TodayPlanning: &testTodayPlanning{ @@ -553,7 +553,7 @@ func TestRenderToday(t *testing.T) { "Currently, it is 58°F and clear. It feels like 57°F, with a relative humidity of 61% and winds from the northwest at 9 mph.", "- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.", "- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%.", - "- **3:00 PM** to **6:00 PM**: Precipitation is expected during this period. The peak precipitation chance is 70% at 3:00 PM.", + "- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM.", "A few showers may linger into early evening.", "Clouds increase after sunrise.", "Rain chances peak during the afternoon.", @@ -943,6 +943,8 @@ type testPrecipWindow struct { MaxPopPercent *int MaxPopTime string MaxPopHourLabel string + PrecipitationType string + ExpectationPhrase string } type testAlertDigest struct { diff --git a/internal/reporttemplate/templates/partials/precipitation_timing.md.tmpl b/internal/reporttemplate/templates/partials/precipitation_timing.md.tmpl index 8ebd292..af82446 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 }}: 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 }} +- **{{ 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 }} {{ end }}{{ with $.GeneratedText.PrecipitationTiming }} {{ . }} {{ end }}