Simplify and rationalize the hourly report template
This commit is contained in:
@@ -10,10 +10,10 @@ import (
|
||||
)
|
||||
|
||||
type Hourly struct {
|
||||
Summary string `json:"summary"`
|
||||
Timing string `json:"timing"`
|
||||
Impacts string `json:"impacts"`
|
||||
Confidence string `json:"confidence,omitempty"`
|
||||
Summary string `json:"summary"`
|
||||
ForecastDiscussion string `json:"forecast_discussion"`
|
||||
PrecipitationTiming string `json:"precipitation_timing,omitempty"`
|
||||
Confidence string `json:"confidence,omitempty"`
|
||||
}
|
||||
|
||||
func ValidateHourly(data []byte) (Hourly, []byte, error) {
|
||||
@@ -34,17 +34,14 @@ func ValidateHourly(data []byte) (Hourly, []byte, error) {
|
||||
}
|
||||
|
||||
value.Summary = strings.TrimSpace(value.Summary)
|
||||
value.Timing = strings.TrimSpace(value.Timing)
|
||||
value.Impacts = strings.TrimSpace(value.Impacts)
|
||||
value.ForecastDiscussion = strings.TrimSpace(value.ForecastDiscussion)
|
||||
value.PrecipitationTiming = strings.TrimSpace(value.PrecipitationTiming)
|
||||
value.Confidence = strings.TrimSpace(value.Confidence)
|
||||
if value.Summary == "" {
|
||||
return Hourly{}, nil, fmt.Errorf("hourly generated text summary is required")
|
||||
}
|
||||
if value.Timing == "" {
|
||||
return Hourly{}, nil, fmt.Errorf("hourly generated text timing is required")
|
||||
}
|
||||
if value.Impacts == "" {
|
||||
return Hourly{}, nil, fmt.Errorf("hourly generated text impacts is required")
|
||||
if value.ForecastDiscussion == "" {
|
||||
return Hourly{}, nil, fmt.Errorf("hourly generated text forecast discussion is required")
|
||||
}
|
||||
|
||||
normalized, err := json.Marshal(value)
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
|
||||
func TestValidateHourlyNormalizesJSON(t *testing.T) {
|
||||
value, normalized, err := ValidateHourly([]byte(`{
|
||||
"timing": " main window late morning ",
|
||||
"summary": " Storm chances increase. ",
|
||||
"confidence": " Medium ",
|
||||
"impacts": " Brief downpours. "
|
||||
"forecast_discussion": " A front will keep the region unsettled. ",
|
||||
"precipitation_timing": " Showers are most likely early this afternoon. ",
|
||||
"confidence": " Medium "
|
||||
}`))
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateHourly() error = %v", err)
|
||||
@@ -18,7 +18,13 @@ func TestValidateHourlyNormalizesJSON(t *testing.T) {
|
||||
if value.Summary != "Storm chances increase." {
|
||||
t.Fatalf("Summary = %q, want trimmed summary", value.Summary)
|
||||
}
|
||||
want := `{"summary":"Storm chances increase.","timing":"main window late morning","impacts":"Brief downpours.","confidence":"Medium"}`
|
||||
if value.ForecastDiscussion != "A front will keep the region unsettled." {
|
||||
t.Fatalf("ForecastDiscussion = %q, want trimmed discussion", value.ForecastDiscussion)
|
||||
}
|
||||
if value.PrecipitationTiming != "Showers are most likely early this afternoon." {
|
||||
t.Fatalf("PrecipitationTiming = %q, want trimmed precipitation timing", value.PrecipitationTiming)
|
||||
}
|
||||
want := `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled.","precipitation_timing":"Showers are most likely early this afternoon.","confidence":"Medium"}`
|
||||
if string(normalized) != want {
|
||||
t.Fatalf("normalized = %s, want %s", normalized, want)
|
||||
}
|
||||
@@ -27,14 +33,14 @@ func TestValidateHourlyNormalizesJSON(t *testing.T) {
|
||||
func TestValidateHourlyOmitsEmptyConfidence(t *testing.T) {
|
||||
_, normalized, err := ValidateHourly([]byte(`{
|
||||
"summary": "Storm chances increase.",
|
||||
"timing": "Late morning.",
|
||||
"impacts": "Brief downpours.",
|
||||
"forecast_discussion": "A front will keep the region unsettled.",
|
||||
"precipitation_timing": " ",
|
||||
"confidence": " "
|
||||
}`))
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateHourly() error = %v", err)
|
||||
}
|
||||
want := `{"summary":"Storm chances increase.","timing":"Late morning.","impacts":"Brief downpours."}`
|
||||
want := `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled."}`
|
||||
if string(normalized) != want {
|
||||
t.Fatalf("normalized = %s, want %s", normalized, want)
|
||||
}
|
||||
@@ -53,22 +59,32 @@ func TestValidateHourlyRejectsInvalidInput(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "unknown field",
|
||||
in: `{"summary":"Storm chances increase.","timing":"Late morning.","impacts":"Brief downpours.","extra":"value"}`,
|
||||
in: `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled.","extra":"value"}`,
|
||||
want: `unknown field "extra"`,
|
||||
},
|
||||
{
|
||||
name: "missing summary",
|
||||
in: `{"timing":"Late morning.","impacts":"Brief downpours."}`,
|
||||
in: `{"forecast_discussion":"A front will keep the region unsettled."}`,
|
||||
want: "summary is required",
|
||||
},
|
||||
{
|
||||
name: "blank timing",
|
||||
in: `{"summary":"Storm chances increase.","timing":" ","impacts":"Brief downpours."}`,
|
||||
want: "timing is required",
|
||||
name: "blank forecast discussion",
|
||||
in: `{"summary":"Storm chances increase.","forecast_discussion":" "}`,
|
||||
want: "forecast discussion is required",
|
||||
},
|
||||
{
|
||||
name: "old timing field rejected",
|
||||
in: `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled.","timing":"Late morning."}`,
|
||||
want: `unknown field "timing"`,
|
||||
},
|
||||
{
|
||||
name: "old impacts field rejected",
|
||||
in: `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled.","impacts":"Brief downpours."}`,
|
||||
want: `unknown field "impacts"`,
|
||||
},
|
||||
{
|
||||
name: "multiple values",
|
||||
in: `{"summary":"Storm chances increase.","timing":"Late morning.","impacts":"Brief downpours."} {}`,
|
||||
in: `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled."} {}`,
|
||||
want: "multiple JSON values",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func BuildHourlyRenderContext(metadata briefing.Metadata, snapshot module.Snapsh
|
||||
Title: "Hourly Report",
|
||||
LocationName: locationName(metadata),
|
||||
GeneratedAt: metadata.GeneratedAt,
|
||||
GeneratedAtLabel: timeLabel(metadata.GeneratedAt, location),
|
||||
GeneratedAtLabel: generatedAtLabel(metadata.GeneratedAt, location),
|
||||
ValidPeriod: metadata.ValidPeriod,
|
||||
ValidPeriodLabel: periodLabel(metadata.ValidPeriod, location),
|
||||
Timezone: metadata.Timezone,
|
||||
@@ -159,3 +159,7 @@ 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 generatedAtLabel(value time.Time, location *time.Location) string {
|
||||
return value.In(location).Format("Monday, January 2, 2006 at 3:04 PM")
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ func TestBuildHourlyRenderContext(t *testing.T) {
|
||||
metadata := testMetadata()
|
||||
snapshot := testSnapshot(t)
|
||||
generated := Hourly{
|
||||
Summary: "Storm chances increase through late morning.",
|
||||
Timing: "The main window is 10 AM to noon.",
|
||||
Impacts: "Brief downpours may slow travel.",
|
||||
Confidence: "Medium confidence in timing.",
|
||||
Summary: "Storm chances increase through late morning.",
|
||||
ForecastDiscussion: "A front will keep the region unsettled.",
|
||||
PrecipitationTiming: "A cold front is moving into the region.",
|
||||
Confidence: "Medium confidence in timing.",
|
||||
}
|
||||
collected := testCollected()
|
||||
derived := testDerived()
|
||||
@@ -38,6 +38,9 @@ func TestBuildHourlyRenderContext(t *testing.T) {
|
||||
if ctx.Report.ValidPeriodLabel != "2026-05-29 at 8:30 AM to 2026-05-29 at 2:30 PM" {
|
||||
t.Fatalf("Report.ValidPeriodLabel = %q, want friendly period", ctx.Report.ValidPeriodLabel)
|
||||
}
|
||||
if ctx.Report.GeneratedAtLabel != "Friday, May 29, 2026 at 8:30 AM" {
|
||||
t.Fatalf("Report.GeneratedAtLabel = %q, want friendly generated-at label", ctx.Report.GeneratedAtLabel)
|
||||
}
|
||||
if ctx.Modules.CurrentConditions == nil || ctx.Modules.CurrentConditions.ConditionText != "Partly cloudy" || ctx.Modules.CurrentConditions.TemperatureF == nil || *ctx.Modules.CurrentConditions.TemperatureF != 74 {
|
||||
t.Fatalf("Modules.CurrentConditions = %#v, want structured current conditions", ctx.Modules.CurrentConditions)
|
||||
}
|
||||
@@ -79,10 +82,12 @@ func TestBuildHourlyRenderContext(t *testing.T) {
|
||||
text := string(rendered)
|
||||
for _, want := range []string{
|
||||
"# Hourly Report",
|
||||
"**Updated:** Friday, May 29, 2026 at 8:30 AM",
|
||||
"Storm chances increase through late morning.",
|
||||
"- 2026-05-29 at 10:00 AM: Showers; 75 F; 70% precipitation; wind S 10 mph, gusts 18 mph",
|
||||
"- Flood Watch: Flood Watch until early afternoon (Moderate)",
|
||||
"Morning storms - Morning storms remain the main story.",
|
||||
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%.",
|
||||
"- **Flood Watch**: Flood Watch until early afternoon",
|
||||
"A cold front is moving into the region.",
|
||||
"A front will keep the region unsettled.",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
||||
@@ -98,9 +103,8 @@ func TestBuildHourlyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
||||
t.Fatalf("NewSnapshot() error = %v", err)
|
||||
}
|
||||
ctx, err := BuildHourlyRenderContext(testMetadata(), snapshot, Hourly{
|
||||
Summary: "Storm chances increase.",
|
||||
Timing: "Late morning.",
|
||||
Impacts: "Brief downpours.",
|
||||
Summary: "Storm chances increase.",
|
||||
ForecastDiscussion: "A front will keep the region unsettled.",
|
||||
}, testCollected(), facts.DerivedFacts{})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildHourlyRenderContext() error = %v", err)
|
||||
@@ -146,11 +150,13 @@ func testSnapshot(t *testing.T) module.Snapshot {
|
||||
StanzaName: string(module.CurrentConditions),
|
||||
Value: briefing.CurrentConditionsModule{
|
||||
ConditionText: "Partly cloudy",
|
||||
ConditionTextLower: "partly cloudy",
|
||||
TemperatureF: floatPtr(74),
|
||||
ApparentTemperatureF: floatPtr(76),
|
||||
RelativeHumidityPercent: floatPtr(71),
|
||||
WindSpeedMph: floatPtr(8),
|
||||
WindDirection: "S",
|
||||
WindDirectionText: "south",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -159,18 +165,24 @@ func testSnapshot(t *testing.T) module.Snapshot {
|
||||
Value: briefing.HourlyForecastModule{
|
||||
Periods: []briefing.HourlyForecastPeriod{
|
||||
{
|
||||
HourLabel: "9:00 AM",
|
||||
PeriodBegins: "2026-05-29 at 9:00 AM",
|
||||
TextDescription: "Cloudy",
|
||||
TextDescriptionLower: "cloudy",
|
||||
TemperatureF: floatPtr(74),
|
||||
ProbabilityOfPrecipitationPercent: floatPtr(30),
|
||||
MentionPrecipitation: true,
|
||||
WindSpeedMph: floatPtr(8),
|
||||
WindDirection: "S",
|
||||
},
|
||||
{
|
||||
HourLabel: "10:00 AM",
|
||||
PeriodBegins: "2026-05-29 at 10:00 AM",
|
||||
TextDescription: "Showers",
|
||||
TextDescriptionLower: "showers",
|
||||
TemperatureF: floatPtr(75),
|
||||
ProbabilityOfPrecipitationPercent: floatPtr(70),
|
||||
MentionPrecipitation: true,
|
||||
WindSpeedMph: floatPtr(10),
|
||||
WindGustMph: floatPtr(18),
|
||||
WindDirection: "S",
|
||||
@@ -187,10 +199,13 @@ func testSnapshot(t *testing.T) module.Snapshot {
|
||||
ProbabilityThreshold: 50,
|
||||
PrecipitationWindows: []briefing.PrecipitationWindowModule{
|
||||
{
|
||||
PeriodBegins: "2026-05-29 at 10:00 AM",
|
||||
PeriodEnds: "2026-05-29 at 12:00 PM",
|
||||
MaxPopPercent: intPtr(70),
|
||||
MaxPopTime: "10 AM",
|
||||
PeriodBegins: "2026-05-29 at 10:00 AM",
|
||||
PeriodBeginsHourLabel: "10:00 AM",
|
||||
PeriodEnds: "2026-05-29 at 12:00 PM",
|
||||
PeriodEndsHourLabel: "12:00 PM",
|
||||
MaxPopPercent: intPtr(70),
|
||||
MaxPopTime: "10 AM",
|
||||
MaxPopHourLabel: "10:00 AM",
|
||||
},
|
||||
},
|
||||
ThunderMentioned: true,
|
||||
|
||||
Reference in New Issue
Block a user