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

@@ -550,6 +550,7 @@ func isForecastDiscussionHorizontalWhitespace(b byte) bool {
func parseForecastDiscussionSectionBlocks(lines []string) []forecastDiscussionSectionBlock {
var blocks []forecastDiscussionSectionBlock
var active *forecastDiscussionSectionBlock
embeddedHeadings := false
finish := func() {
if active == nil {
@@ -567,6 +568,7 @@ func parseForecastDiscussionSectionBlocks(lines []string) []forecastDiscussionSe
}
if line == "&&" || strings.Contains(line, "WATCHES/WARNINGS/ADVISORIES") {
finish()
embeddedHeadings = false
continue
}
@@ -574,8 +576,17 @@ func parseForecastDiscussionSectionBlocks(lines []string) []forecastDiscussionSe
if ok {
finish()
active = &forecastDiscussionSectionBlock{heading: heading}
embeddedHeadings = isForecastDiscussionEmbeddedSectionWrapper(heading.section)
continue
}
if embeddedHeadings {
heading, ok = parseForecastDiscussionEmbeddedSectionHeading(raw)
if ok {
finish()
active = &forecastDiscussionSectionBlock{heading: heading}
continue
}
}
if active != nil {
active.body = append(active.body, raw)
}
@@ -585,6 +596,18 @@ func parseForecastDiscussionSectionBlocks(lines []string) []forecastDiscussionSe
return blocks
}
func isForecastDiscussionEmbeddedSectionWrapper(section string) bool {
return section == "PREV DISCUSSION"
}
func parseForecastDiscussionEmbeddedSectionHeading(line string) (forecastDiscussionSectionHeading, bool) {
line = strings.TrimSpace(line)
if line == "" || line[0] == '.' {
return forecastDiscussionSectionHeading{}, false
}
return parseForecastDiscussionSectionHeading("." + line)
}
func parseForecastDiscussionKeyMessages(body []string) []string {
body = removeForecastDiscussionPresentationMarkers(body)
body = trimBlankLines(body)
@@ -716,7 +739,32 @@ func isForecastDiscussionKeyMessageMetadataLine(line string) bool {
return true
}
}
return false
return isForecastDiscussionKeyMessageAsOfLine(line)
}
func isForecastDiscussionKeyMessageAsOfLine(line string) bool {
const label = "As of"
if !hasForecastDiscussionASCIIPrefix(line, label) || len(line) == len(label) || !isForecastDiscussionHorizontalWhitespace(line[len(label)]) {
return false
}
remainder := strings.TrimSpace(line[len(label):])
if !strings.HasSuffix(remainder, "...") {
return false
}
fields := strings.Fields(strings.TrimSpace(strings.TrimSuffix(remainder, "...")))
if len(fields) != 3 {
return false
}
if _, _, err := parseForecastDiscussionClock(fields[0], fields[1]); err != nil {
return false
}
switch strings.ToLower(fields[2]) {
case "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday":
return true
default:
return false
}
}
func stripForecastDiscussionKeyMessageMarker(line string) (string, bool) {
@@ -740,11 +788,23 @@ func stripForecastDiscussionKeyMessageMarker(line string) (string, bool) {
}
func stripForecastDiscussionKeyMessageNumericMarker(line string) (string, bool) {
digitStart := 0
digitEnd := 0
parenthesized := len(line) > 0 && line[0] == '('
if parenthesized {
digitStart = 1
digitEnd = 1
}
for digitEnd < len(line) && line[digitEnd] >= '0' && line[digitEnd] <= '9' {
digitEnd++
}
if digitEnd == 0 || digitEnd == len(line) || (line[digitEnd] != ')' && line[digitEnd] != '.') {
if digitEnd == digitStart || digitEnd == len(line) {
return "", false
}
if parenthesized && line[digitEnd] != ')' {
return "", false
}
if !parenthesized && line[digitEnd] != ')' && line[digitEnd] != '.' {
return "", false
}
@@ -752,7 +812,7 @@ func stripForecastDiscussionKeyMessageNumericMarker(line string) (string, bool)
if markerEnd < len(line) && !isForecastDiscussionHorizontalWhitespace(line[markerEnd]) {
return "", false
}
value, err := strconv.ParseUint(line[:digitEnd], 10, 0)
value, err := strconv.ParseUint(line[digitStart:digitEnd], 10, 0)
if err != nil || value == 0 {
return "", false
}