Support parenthesized NWS discussion headings

This commit is contained in:
2026-08-03 00:13:58 +00:00
parent a62cb87b78
commit b8c6708439
4 changed files with 526 additions and 296 deletions

View File

@@ -426,6 +426,11 @@ func parseForecastDiscussionSectionHeading(line string) (forecastDiscussionSecti
if strings.HasSuffix(line, "/...") {
return parseForecastDiscussionSlashQualifiedHeading(line)
}
if strings.HasSuffix(line, "...") {
if heading, ok := parseForecastDiscussionParenthesizedTerminalHeading(line); ok {
return heading, true
}
}
return parseForecastDiscussionEllipsisHeading(line)
}
@@ -453,6 +458,38 @@ func parseForecastDiscussionSlashQualifiedHeading(line string) (forecastDiscussi
return forecastDiscussionSectionHeading{section: section, qualifier: qualifier}, true
}
func parseForecastDiscussionParenthesizedTerminalHeading(line string) (forecastDiscussionSectionHeading, bool) {
if len(line) < 4 || line[0] != '.' || !strings.HasSuffix(line, "...") {
return forecastDiscussionSectionHeading{}, false
}
content := strings.TrimRight(line[1:len(line)-3], " \t")
if !strings.HasSuffix(content, ")") {
return forecastDiscussionSectionHeading{}, false
}
separator := -1
for i := 1; i < len(content); i++ {
if content[i] == '(' && isForecastDiscussionHorizontalWhitespace(content[i-1]) {
separator = i
break
}
}
if separator < 0 {
return forecastDiscussionSectionHeading{}, false
}
section, ok := normalizeForecastDiscussionSectionIdentity(content[:separator])
if !ok {
return forecastDiscussionSectionHeading{}, false
}
qualifier := content[separator:]
if len(qualifier) <= 2 || strings.TrimSpace(qualifier[1:len(qualifier)-1]) == "" {
return forecastDiscussionSectionHeading{}, false
}
return forecastDiscussionSectionHeading{section: section, qualifier: qualifier}, true
}
func parseForecastDiscussionEllipsisHeading(line string) (forecastDiscussionSectionHeading, bool) {
content := line[1:]
delimiter := strings.Index(content, "...")

View File

@@ -129,6 +129,34 @@ func TestParseForecastDiscussionSectionHeading(t *testing.T) {
wantQualifier: "(Tonight)",
wantOK: true,
},
{
name: "parenthesized terminal discussion",
line: ".DISCUSSION (Today through Thursday)...",
wantSection: "DISCUSSION",
wantQualifier: "(Today through Thursday)",
wantOK: true,
},
{
name: "parenthesized terminal mapped section",
line: ".SHORT TERM (Tonight)...",
wantSection: "SHORT TERM",
wantQualifier: "(Tonight)",
wantOK: true,
},
{
name: "parenthesized terminal normalized identity whitespace",
line: " .SHORT\tTERM (Tonight)... ",
wantSection: "SHORT TERM",
wantQualifier: "(Tonight)",
wantOK: true,
},
{
name: "parenthesized terminal qualifier punctuation and nesting",
line: ".DISCUSSION (Today (and Thursday), 70% chance!)...",
wantSection: "DISCUSSION",
wantQualifier: "(Today (and Thursday), 70% chance!)",
wantOK: true,
},
{
name: "lowercase prose",
line: ".This is ordinary prose...",
@@ -194,6 +222,41 @@ func TestParseForecastDiscussionSectionHeading(t *testing.T) {
line: ".Short Term... (Tonight)",
wantOK: false,
},
{
name: "parenthesized terminal missing separator whitespace",
line: ".DISCUSSION(Today)...",
wantOK: false,
},
{
name: "parenthesized terminal empty qualifier",
line: ".DISCUSSION ( )...",
wantOK: false,
},
{
name: "parenthesized terminal missing final parenthesis",
line: ".DISCUSSION (Today...",
wantOK: false,
},
{
name: "parenthesized terminal misplaced parentheses",
line: ".DISCUSSION Today)...",
wantOK: false,
},
{
name: "parenthesized terminal trailing text",
line: ".DISCUSSION (Today)... extra",
wantOK: false,
},
{
name: "parenthesized terminal unsupported identity punctuation",
line: ".DISCUSSION: (Today)...",
wantOK: false,
},
{
name: "parenthesized terminal lowercase identity",
line: ".discussion (Today)...",
wantOK: false,
},
}
for _, tt := range tests {
@@ -310,6 +373,28 @@ func TestParseForecastDiscussionSectionBlocksUsesGenericHeadingsAsBoundaries(t *
}
}
func TestParseForecastDiscussionSectionBlocksUsesParenthesizedTerminalHeadingAsBoundary(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
".SHORT TERM... (Tonight)",
"Short-term prose.",
".DISCUSSION (Today through Thursday)...",
"Discussion prose.",
})
want := []forecastDiscussionSectionBlock{
{
heading: forecastDiscussionSectionHeading{section: "SHORT TERM", qualifier: "(Tonight)"},
body: []string{"Short-term prose."},
},
{
heading: forecastDiscussionSectionHeading{section: "DISCUSSION", qualifier: "(Today through Thursday)"},
body: []string{"Discussion 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)",