Handle additional NWS forecast discussion formats
All checks were successful
ci/woodpecker/manual/build-image Pipeline was successful

This commit is contained in:
2026-08-03 01:14:47 +00:00
parent 456a46e01b
commit 9b88be4dd2
9 changed files with 374 additions and 544 deletions

View File

@@ -391,6 +391,58 @@ func TestParseForecastDiscussionSectionBlocksUsesGenericHeadingsAsBoundaries(t *
}
}
func TestParseForecastDiscussionSectionBlocksRecognizesEmbeddedHeadingsInPreviousDiscussion(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
".PREV DISCUSSION... /Issued 319 PM PDT Sun Aug 2 2026/",
"KEY MESSAGES...",
"* First embedded message.",
"DISCUSSION...",
"Discussion prose.",
"&&",
})
want := []forecastDiscussionSectionBlock{
{
heading: forecastDiscussionSectionHeading{
section: "PREV DISCUSSION",
qualifier: "/Issued 319 PM PDT Sun Aug 2 2026/",
},
},
{
heading: forecastDiscussionSectionHeading{section: "KEY MESSAGES"},
body: []string{"* First embedded message."},
},
{
heading: forecastDiscussionSectionHeading{section: "DISCUSSION"},
body: []string{"Discussion prose."},
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("blocks = %#v, want %#v", got, want)
}
}
func TestParseForecastDiscussionSectionBlocksKeepsUndottedHeadingLikeLinesOutsideWrappers(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
".SHORT TERM...",
"Short-term prose.",
"KEY MESSAGES...",
"Still short-term prose.",
})
want := []forecastDiscussionSectionBlock{
{
heading: forecastDiscussionSectionHeading{section: "SHORT TERM"},
body: []string{
"Short-term prose.",
"KEY MESSAGES...",
"Still short-term prose.",
},
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("blocks = %#v, want %#v", got, want)
}
}
func TestParseForecastDiscussionSectionBlocksUsesParenthesizedTerminalHeadingAsBoundary(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
".SHORT TERM... (Tonight)",
@@ -756,6 +808,16 @@ func TestIsForecastDiscussionKeyMessageMetadataLine(t *testing.T) {
line: "uPdAtEd At 300 PM CDT Sat Mar 28 2026",
want: true,
},
{
name: "as of weekday preamble",
line: "As of 1235 PM Sunday...",
want: true,
},
{
name: "as of ASCII case insensitive",
line: "aS oF 1235 pm SuNdAy...",
want: true,
},
{
name: "label without whitespace boundary",
line: "Updated at300 PM CDT Sat Mar 28 2026",
@@ -766,6 +828,26 @@ func TestIsForecastDiscussionKeyMessageMetadataLine(t *testing.T) {
line: "Updated at not a timestamp",
want: false,
},
{
name: "as of invalid clock",
line: "As of 2535 PM Sunday...",
want: false,
},
{
name: "as of invalid weekday",
line: "As of 1235 PM Someday...",
want: false,
},
{
name: "as of without terminal ellipsis",
line: "As of 1235 PM Sunday",
want: false,
},
{
name: "as of prefix collision",
line: "As often happens, storms weaken overnight.",
want: false,
},
{
name: "nonmetadata prose",
line: "Updated atmospheric conditions remain unsettled.",
@@ -794,13 +876,18 @@ func TestStripForecastDiscussionKeyMessageMarker(t *testing.T) {
{name: "asterisk", line: "* First message.", want: "First message.", ok: true},
{name: "numeric parenthesis", line: "1) First message.", want: "First message.", ok: true},
{name: "numeric dot", line: "2. Second message.", want: "Second message.", ok: true},
{name: "parenthesized numeric", line: "(1) First message.", want: "First message.", ok: true},
{name: "composite hyphen", line: "- 1. First message.", want: "First message.", ok: true},
{name: "composite asterisk", line: "* 1) First message.", want: "First message.", ok: true},
{name: "composite parenthesized numeric", line: "- (1) First message.", want: "First message.", ok: true},
{name: "composite asterisk parenthesized numeric", line: "* (1) First message.", want: "First message.", ok: true},
{name: "multi digit numeric", line: "12. Twelfth message.", want: "Twelfth message.", ok: true},
{name: "hyphen only", line: "-", want: "", ok: true},
{name: "numeric marker only", line: "1)", want: "", ok: true},
{name: "zero numeric marker", line: "0) Not a marker.", want: "", ok: false},
{name: "zero parenthesized numeric marker", line: "(0) Not a marker.", want: "", ok: false},
{name: "overflow numeric marker", line: "999999999999999999999999999999. Too big.", want: "", ok: false},
{name: "parenthesized numeric marker without boundary", line: "(1)Not a marker.", want: "", ok: false},
{name: "alphanumeric numeric prefix", line: "1x Not a marker.", want: "", ok: false},
{name: "numeric marker without boundary", line: "1)Not a marker.", want: "", ok: false},
{name: "numeric marker with unsupported punctuation", line: "1, Not a marker.", want: "", ok: false},
@@ -877,6 +964,14 @@ func TestParseForecastDiscussionKeyMessagesClassifiesListsAndMetadata(t *testing
},
want: []string{"First message."},
},
{
name: "leading as of metadata",
body: []string{
"As of 1235 PM Sunday...",
"- First message.",
},
want: []string{"First message."},
},
{
name: "updated atmospheric prose remains content",
body: []string{
@@ -901,6 +996,14 @@ func TestParseForecastDiscussionKeyMessagesClassifiesListsAndMetadata(t *testing
},
want: []string{"First message. Updated at 300 PM CDT Sat Mar 28 2026"},
},
{
name: "later as of metadata remains content",
body: []string{
"- First message.",
"As of 1235 PM Sunday...",
},
want: []string{"First message. As of 1235 PM Sunday..."},
},
}
for _, tt := range tests {
@@ -1089,7 +1192,7 @@ func TestParseForecastDiscussionHTMLSupportsMixedHeadingFormats(t *testing.T) {
}
}
func TestParseForecastDiscussionHTMLParsesNumberedAndKeyPointsFixtures(t *testing.T) {
func TestParseForecastDiscussionHTMLParsesCrossOfficeKeyMessageFixtures(t *testing.T) {
tests := []struct {
name string
filename string
@@ -1127,6 +1230,48 @@ func TestParseForecastDiscussionHTMLParsesNumberedAndKeyPointsFixtures(t *testin
"*", "DISCUSSION", "(Today through Thursday)", "Discussion details must not be included with key points.",
},
},
{
name: "as of preamble",
filename: "forecast_discussion_rah_as_of_sample.html",
officeID: "RAH",
officeName: "National Weather Service Raleigh NC",
issuedAt: time.Date(2026, 8, 2, 16, 35, 0, 0, time.UTC),
messages: []string{
"Scattered storms may produce locally heavy rain this afternoon.",
"Drier weather arrives Monday.",
},
unwanted: []string{
"As of", "1)", "2)", "DISCUSSION", "Discussion details remain boundary-only content.",
},
},
{
name: "parenthesized numeric markers",
filename: "forecast_discussion_lwx_parenthesized_number_sample.html",
officeID: "LWX",
officeName: "National Weather Service Baltimore MD/Washington DC",
issuedAt: time.Date(2026, 8, 2, 18, 0, 0, 0, time.UTC),
messages: []string{
"Thunderstorms remain possible near the Blue Ridge this evening.",
"Seasonably warm conditions continue Monday.",
},
unwanted: []string{
"(1)", "(2)", "AVIATION", "Aviation details remain boundary-only content.",
},
},
{
name: "embedded key messages in previous discussion",
filename: "forecast_discussion_mfr_prev_discussion_sample.html",
officeID: "MFR",
officeName: "National Weather Service Medford OR",
issuedAt: time.Date(2026, 8, 2, 22, 19, 0, 0, time.UTC),
messages: []string{
"Heat returns to inland valleys Monday.",
"Gusty afternoon winds develop east of the Cascades.",
},
unwanted: []string{
"PREV DISCUSSION", "Issued 319 PM", "*", "DISCUSSION", "Discussion details must not be included with key messages.",
},
},
}
for _, tt := range tests {