Normalize NWS forecast discussion preambles

This commit is contained in:
2026-08-02 22:58:48 +00:00
parent 0a3e52d0e5
commit 882059014c
2 changed files with 242 additions and 9 deletions

View File

@@ -379,6 +379,180 @@ func TestParseForecastDiscussionTextKeepsFirstMappedSections(t *testing.T) {
}
}
func TestParseForecastDiscussionTextSectionKeepsHeadingQualifiers(t *testing.T) {
tests := []struct {
name string
heading string
wantQualifier string
}{
{
name: "legacy heading",
heading: ".SHORT TERM... (Tonight)",
wantQualifier: "(Tonight)",
},
{
name: "slash-qualified heading",
heading: ".SHORT TERM /Tonight/...",
wantQualifier: "Tonight",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, tt.heading),
body: []string{"Section prose."},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Qualifier != tt.wantQualifier {
t.Fatalf("Qualifier = %q, want %q", section.Qualifier, tt.wantQualifier)
}
if section.Text != "Section prose." {
t.Fatalf("Text = %q", section.Text)
}
})
}
}
func TestParseForecastDiscussionTextSectionReadsNextLineQualifierAndIssuedAt(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
body: []string{
"(Tonight)",
"Issued at 219 PM CDT Sat Mar 28 2026",
"Section prose.",
},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Qualifier != "(Tonight)" {
t.Fatalf("Qualifier = %q", section.Qualifier)
}
wantIssuedAt := time.Date(2026, 3, 28, 19, 19, 0, 0, time.UTC)
if section.IssuedAt == nil || !section.IssuedAt.Equal(wantIssuedAt) {
t.Fatalf("IssuedAt = %v, want %s", section.IssuedAt, wantIssuedAt.Format(time.RFC3339))
}
if section.Text != "Section prose." {
t.Fatalf("Text = %q", section.Text)
}
}
func TestParseForecastDiscussionTextSectionRecognizesUppercaseIssuedAt(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
body: []string{
"ISSUED AT 219 PM CDT Sat Mar 28 2026",
"Section prose.",
},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
wantIssuedAt := time.Date(2026, 3, 28, 19, 19, 0, 0, time.UTC)
if section.IssuedAt == nil || !section.IssuedAt.Equal(wantIssuedAt) {
t.Fatalf("IssuedAt = %v, want %s", section.IssuedAt, wantIssuedAt.Format(time.RFC3339))
}
}
func TestParseForecastDiscussionTextSectionKeepsHeadingQualifierBeforeParentheticalProse(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM... (From Heading)"),
body: []string{"(Parenthetical prose.)"},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Qualifier != "(From Heading)" {
t.Fatalf("Qualifier = %q", section.Qualifier)
}
if section.Text != "(Parenthetical prose.)" {
t.Fatalf("Text = %q", section.Text)
}
}
func TestParseForecastDiscussionTextSectionHandlesEmptyAndInvalidPreambles(t *testing.T) {
t.Run("empty section", func(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Qualifier != "" || section.IssuedAt != nil || section.Text != "" {
t.Fatalf("section = %#v, want empty section", section)
}
})
t.Run("invalid issue time", func(t *testing.T) {
_, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
body: []string{"ISSUED AT not a timestamp"},
})
if err == nil {
t.Fatalf("parseForecastDiscussionTextSection() error = nil")
}
if !strings.Contains(err.Error(), "parse section issuedAt") {
t.Fatalf("error = %q, want section issue-time context", err)
}
})
}
func TestParseForecastDiscussionTextSectionRemovesPresentationMarkers(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
body: []string{
"-- Changed Discussion --",
"(Tonight)",
"Issued at 219 PM CDT Sat Mar 28 2026",
"Section prose.",
"-- END CHANGED DISCUSSION --",
},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Qualifier != "(Tonight)" || section.Text != "Section prose." {
t.Fatalf("section = %#v", section)
}
if strings.Contains(section.Text, "Changed Discussion") {
t.Fatalf("Text contains presentation marker: %q", section.Text)
}
}
func TestParseForecastDiscussionKeyMessagesRemovesMarkersAndLeadingMetadata(t *testing.T) {
got := parseForecastDiscussionKeyMessages([]string{
"-- changed discussion --",
"Updated at 300 PM CDT Sat Mar 28 2026",
"- First message.",
"Updated at after the first message.",
"-- End Changed Discussion --",
"- Second message.",
})
want := []string{
"First message. Updated at after the first message.",
"Second message.",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("KeyMessages = %#v, want %#v", got, want)
}
}
func TestParseForecastDiscussionTextSectionKeepsArbitraryDashedProse(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
body: []string{"-- Changed Discussion -- extra", "Section prose."},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Text != "-- Changed Discussion -- extra Section prose." {
t.Fatalf("Text = %q", section.Text)
}
}
func TestParseForecastDiscussionHTMLParsesExpectedFields(t *testing.T) {
raw := loadForecastDiscussionSampleHTML(t)
@@ -563,3 +737,13 @@ func loadMixedFormatForecastDiscussionSampleHTML(t *testing.T) string {
return raw
}
func mustParseForecastDiscussionSectionHeading(t *testing.T, line string) forecastDiscussionSectionHeading {
t.Helper()
heading, ok := parseForecastDiscussionSectionHeading(line)
if !ok {
t.Fatalf("parseForecastDiscussionSectionHeading(%q) did not recognize heading", line)
}
return heading
}