Scan NWS forecast discussion blocks once

This commit is contained in:
2026-08-02 22:56:03 +00:00
parent 2b19a121fa
commit 0a3e52d0e5
2 changed files with 176 additions and 76 deletions

View File

@@ -236,41 +236,82 @@ func TestForecastDiscussionSectionRoleHeadingsParse(t *testing.T) {
}
}
func TestExtractForecastDiscussionSectionStopsAtSlashQualifiedHeading(t *testing.T) {
block, ok := extractForecastDiscussionSection([]string{
".LONG TERM /Monday through Next Saturday/...",
"Long-term prose.",
".AVIATION /18Z TAFS/...",
"Aviation prose.",
}, forecastDiscussionSectionForRole(forecastDiscussionSectionRoleLongTerm))
if !ok {
t.Fatalf("extractForecastDiscussionSection() found no LONG TERM block")
}
if block.heading.qualifier != "Monday through Next Saturday" {
t.Fatalf("qualifier = %q, want normalized slash qualifier", block.heading.qualifier)
}
wantBody := []string{"Long-term prose."}
if !reflect.DeepEqual(block.body, wantBody) {
t.Fatalf("body = %#v, want %#v", block.body, wantBody)
}
}
func TestExtractForecastDiscussionSectionAllowsEmptyBodyBeforeHeading(t *testing.T) {
block, ok := extractForecastDiscussionSection([]string{
func TestParseForecastDiscussionSectionBlocksRetainsOrderAndEmptyBodies(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
".SHORT TERM... (Tonight)",
".LONG TERM... (Tomorrow)",
"Long-term prose.",
}, forecastDiscussionSectionForRole(forecastDiscussionSectionRoleShortTerm))
if !ok {
t.Fatalf("extractForecastDiscussionSection() found no SHORT TERM block")
})
want := []forecastDiscussionSectionBlock{
{heading: forecastDiscussionSectionHeading{section: "SHORT TERM", qualifier: "(Tonight)"}},
{
heading: forecastDiscussionSectionHeading{section: "LONG TERM", qualifier: "(Tomorrow)"},
body: []string{"Long-term prose."},
},
}
if len(block.body) != 0 {
t.Fatalf("body = %#v, want empty body", block.body)
if !reflect.DeepEqual(got, want) {
t.Fatalf("blocks = %#v, want %#v", got, want)
}
}
func TestExtractForecastDiscussionSectionKeepsMalformedHeadingLikeLines(t *testing.T) {
block, ok := extractForecastDiscussionSection([]string{
func TestParseForecastDiscussionSectionBlocksStopsAtTerminators(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
"Preamble is ignored.",
".KEY MESSAGES...",
"- First message.",
"&&",
".SHORT TERM...",
"Short-term prose.",
".LSX WATCHES/WARNINGS/ADVISORIES...",
"Watch text is ignored.",
"$$",
"WFO LSX",
})
want := []forecastDiscussionSectionBlock{
{
heading: forecastDiscussionSectionHeading{section: "KEY MESSAGES"},
body: []string{"- First message."},
},
{
heading: forecastDiscussionSectionHeading{section: "SHORT TERM"},
body: []string{"Short-term prose."},
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("blocks = %#v, want %#v", got, want)
}
}
func TestParseForecastDiscussionSectionBlocksUsesGenericHeadingsAsBoundaries(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
".SHORT TERM...",
"Short-term prose.",
".SYNOPSIS...",
"Synopsis prose.",
".LONG TERM /Tomorrow/...",
"Long-term prose.",
})
want := []forecastDiscussionSectionBlock{
{
heading: forecastDiscussionSectionHeading{section: "SHORT TERM"},
body: []string{"Short-term prose."},
},
{
heading: forecastDiscussionSectionHeading{section: "SYNOPSIS"},
body: []string{"Synopsis prose."},
},
{
heading: forecastDiscussionSectionHeading{section: "LONG TERM", qualifier: "Tomorrow"},
body: []string{"Long-term prose."},
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("blocks = %#v, want %#v", got, want)
}
}
func TestParseForecastDiscussionSectionBlocksKeepsMalformedHeadingLikeLines(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
".SHORT TERM... (Tonight)",
".short term... Lowercase prose stays in the body.",
".LONG TERM /Tomorrow/..",
@@ -278,18 +319,63 @@ func TestExtractForecastDiscussionSectionKeepsMalformedHeadingLikeLines(t *testi
"Expected short-term prose.",
".LONG TERM... (Tomorrow)",
"Long-term prose.",
}, forecastDiscussionSectionForRole(forecastDiscussionSectionRoleShortTerm))
if !ok {
t.Fatalf("extractForecastDiscussionSection() found no SHORT TERM block")
})
want := []forecastDiscussionSectionBlock{
{
heading: forecastDiscussionSectionHeading{section: "SHORT TERM", qualifier: "(Tonight)"},
body: []string{
".short term... Lowercase prose stays in the body.",
".LONG TERM /Tomorrow/..",
".LONG TERM /Tomorrow/... more",
"Expected short-term prose.",
},
},
{
heading: forecastDiscussionSectionHeading{section: "LONG TERM", qualifier: "(Tomorrow)"},
body: []string{"Long-term prose."},
},
}
wantBody := []string{
".short term... Lowercase prose stays in the body.",
".LONG TERM /Tomorrow/..",
".LONG TERM /Tomorrow/... more",
"Expected short-term prose.",
if !reflect.DeepEqual(got, want) {
t.Fatalf("blocks = %#v, want %#v", got, want)
}
if !reflect.DeepEqual(block.body, wantBody) {
t.Fatalf("body = %#v, want %#v", block.body, wantBody)
}
func TestParseForecastDiscussionTextKeepsFirstMappedSections(t *testing.T) {
text := strings.Join([]string{
"AFDLSX",
"National Weather Service Saint Louis MO",
"224 PM CDT Sat Mar 28 2026",
"",
".KEY MESSAGES...",
"&&",
".KEY MESSAGES...",
"- Second key message.",
"&&",
".SHORT TERM... (First short term)",
"First short-term prose.",
"&&",
".SHORT TERM... (Second short term)",
"Second short-term prose.",
"&&",
".LONG TERM... (First long term)",
"First long-term prose.",
"&&",
".LONG TERM... (Second long term)",
"Second long-term prose.",
}, "\n")
got, err := ParseForecastDiscussionText(text)
if err != nil {
t.Fatalf("ParseForecastDiscussionText() error = %v", err)
}
if len(got.KeyMessages) != 0 {
t.Fatalf("KeyMessages = %#v, want empty first block", got.KeyMessages)
}
if got.ShortTerm == nil || got.ShortTerm.Qualifier != "(First short term)" || got.ShortTerm.Text != "First short-term prose." {
t.Fatalf("ShortTerm = %#v, want first section", got.ShortTerm)
}
if got.LongTerm == nil || got.LongTerm.Qualifier != "(First long term)" || got.LongTerm.Text != "First long-term prose." {
t.Fatalf("LongTerm = %#v, want first section", got.LongTerm)
}
}