Normalize NWS forecast discussion preambles
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user