refactor weather parser

This commit is contained in:
windyboy
2025-12-24 17:05:51 +08:00
parent d0fd461e38
commit dbde6524b3
5 changed files with 330 additions and 358 deletions
@@ -0,0 +1,96 @@
package weather
import (
"caatsm/internal/domain/weather"
"fmt"
"strconv"
)
func parseWindAndVisibility(tokens []string) (int, *weather.Wind, *weather.Visibility) {
pos := 0
var wind *weather.Wind
var visibility *weather.Visibility
if pos < len(tokens) {
if parsed := parseWind(tokens[pos]); parsed != nil {
wind = parsed
pos++
if pos < len(tokens) {
if match := variableWindPattern.FindStringSubmatch(tokens[pos]); match != nil {
from, _ := strconv.Atoi(match[1])
to, _ := strconv.Atoi(match[2])
wind.Variable = true
wind.VariableFrom = from
wind.VariableTo = to
pos++
}
}
}
}
if pos < len(tokens) {
if parsed := parseVisibility(tokens[pos]); parsed != nil {
visibility = parsed
pos++
}
}
return pos, wind, visibility
}
func parsePhenomena(tokens []string) (int, []weather.Phenomenon) {
pos := 0
var phenomena []weather.Phenomenon
for pos < len(tokens) {
if match := phenomenonPattern.FindStringSubmatch(tokens[pos]); match != nil {
phenomena = append(phenomena, weather.Phenomenon{
Intensity: match[1],
Descriptor: match[2],
Weather: match[3],
})
pos++
continue
}
break
}
return pos, phenomena
}
func parseClouds(tokens []string) (int, []weather.Cloud) {
pos := 0
var clouds []weather.Cloud
if pos < len(tokens) {
if skyClearPattern.MatchString(tokens[pos]) {
return 1, clouds
}
}
for pos < len(tokens) {
if match := cloudPattern.FindStringSubmatch(tokens[pos]); match != nil {
alt, _ := strconv.Atoi(match[2])
clouds = append(clouds, weather.Cloud{
Type: match[1],
Altitude: alt * 100,
Modifier: match[3],
})
pos++
continue
}
break
}
return pos, clouds
}
func appendPeriodWarnings(warnings *[]string, tokens []string) {
if warnings == nil {
return
}
for _, token := range tokens {
*warnings = append(*warnings, fmt.Sprintf("unrecognized token in period: %s", token))
}
}
-89
View File
@@ -14,92 +14,3 @@ func Tokenize(raw string) []string {
tokens := strings.Fields(raw)
return tokens
}
// Section represents a special section in the report (RMK, TEMPO, BECMG, FM)
type Section struct {
Type string // "RMK", "TEMPO", "BECMG", "FM"
StartIdx int // Starting token index
EndIdx int // Ending token index (exclusive)
Tokens []string // Tokens in this section
}
// FindSpecialSections identifies special sections in the tokenized report
func FindSpecialSections(tokens []string) []Section {
var sections []Section
var currentSection *Section
for i, token := range tokens {
upperToken := strings.ToUpper(token)
switch {
case strings.HasPrefix(upperToken, "RMK"):
if currentSection != nil {
currentSection.EndIdx = i
sections = append(sections, *currentSection)
}
currentSection = &Section{
Type: "RMK",
StartIdx: i,
Tokens: []string{token},
}
case strings.HasPrefix(upperToken, "TEMPO"):
if currentSection != nil && currentSection.Type != "RMK" {
currentSection.EndIdx = i
sections = append(sections, *currentSection)
}
currentSection = &Section{
Type: "TEMPO",
StartIdx: i,
Tokens: []string{token},
}
case strings.HasPrefix(upperToken, "BECMG"):
if currentSection != nil && currentSection.Type != "RMK" {
currentSection.EndIdx = i
sections = append(sections, *currentSection)
}
currentSection = &Section{
Type: "BECMG",
StartIdx: i,
Tokens: []string{token},
}
case strings.HasPrefix(upperToken, "FM"):
if currentSection != nil && currentSection.Type != "RMK" {
currentSection.EndIdx = i
sections = append(sections, *currentSection)
}
currentSection = &Section{
Type: "FM",
StartIdx: i,
Tokens: []string{token},
}
default:
if currentSection != nil {
currentSection.Tokens = append(currentSection.Tokens, token)
}
}
}
// Close the last section
if currentSection != nil {
currentSection.EndIdx = len(tokens)
sections = append(sections, *currentSection)
}
return sections
}
// ExtractSectionTokens extracts tokens for a specific section type
func ExtractSectionTokens(tokens []string, sectionType string) []string {
sections := FindSpecialSections(tokens)
for _, section := range sections {
if section.Type == sectionType {
return section.Tokens
}
}
return nil
}
+55 -117
View File
@@ -14,87 +14,9 @@ func parseMetar(raw string, reportType string) (*weather.Metar, error) {
return nil, weather.ErrInvalidFormat
}
metar := &weather.Metar{
ReportType: weather.ReportType(reportType),
RawTextVal: raw,
Warnings: []string{},
Clouds: []weather.Cloud{},
Phenomena: []weather.Phenomenon{},
}
// Track current position in tokens
pos := 0
// Skip report type (METAR/SPECI) - first token
if pos >= len(tokens) {
return nil, weather.ErrMissingStation
}
pos++
// Parse station (second token)
if pos >= len(tokens) {
return nil, weather.ErrMissingStation
}
metar.StationID = tokens[pos]
pos++
// Parse issue time (DDHHmmZ) - third token
if pos >= len(tokens) {
return nil, weather.ErrMissingTime
}
issueTime, err := ParseTime(tokens[pos])
metar, pos, err := parseMetarHeader(tokens, raw, reportType)
if err != nil {
return nil, fmt.Errorf("failed to parse issue time: %w", err)
}
metar.IssueTimeVal = issueTime
metar.ObsTime = issueTime
pos++
// Check for modifier (AUTO, COR)
if pos < len(tokens) {
if match := modifierPattern.FindStringSubmatch(tokens[pos]); match != nil {
metar.Modifier = match[1]
pos++
}
}
// Parse wind
if pos < len(tokens) {
if wind := parseWind(tokens[pos]); wind != nil {
metar.Wind = wind
pos++
// Check for variable wind (e.g., 180V240)
if pos < len(tokens) {
if match := variableWindPattern.FindStringSubmatch(tokens[pos]); match != nil {
from, _ := strconv.Atoi(match[1])
to, _ := strconv.Atoi(match[2])
metar.Wind.Variable = true
metar.Wind.VariableFrom = from
metar.Wind.VariableTo = to
pos++
}
}
}
}
// Parse visibility
if pos < len(tokens) {
if vis := parseVisibility(tokens[pos]); vis != nil {
metar.Visibility = vis
pos++
// Check for directional visibility (e.g., 2000NE)
if pos < len(tokens) {
if match := directionalVisibilityPattern.FindStringSubmatch(tokens[pos]); match != nil {
dist, _ := strconv.ParseFloat(match[1], 64)
metar.Visibility.Distance = dist
metar.Visibility.Direction = match[2]
metar.Visibility.Unit = "M"
pos++
}
}
}
return nil, err
}
// Parse runway visual range (RVR) - skip for now, add to warnings
@@ -103,43 +25,13 @@ func parseMetar(raw string, reportType string) (*weather.Metar, error) {
pos++
}
// Parse weather phenomena
for pos < len(tokens) {
if match := phenomenonPattern.FindStringSubmatch(tokens[pos]); match != nil {
phenom := weather.Phenomenon{
Intensity: match[1],
Descriptor: match[2],
Weather: match[3],
}
metar.Phenomena = append(metar.Phenomena, phenom)
pos++
} else {
break
}
}
phenomConsumed, phenomena := parsePhenomena(tokens[pos:])
metar.Phenomena = append(metar.Phenomena, phenomena...)
pos += phenomConsumed
// Parse clouds
for pos < len(tokens) {
// Check for special cloud codes first
if match := skyClearPattern.FindStringSubmatch(tokens[pos]); match != nil {
// SKC, CLR, NSC - no clouds
pos++
break
}
if match := cloudPattern.FindStringSubmatch(tokens[pos]); match != nil {
alt, _ := strconv.Atoi(match[2])
cloud := weather.Cloud{
Type: match[1],
Altitude: alt * 100, // Convert to feet
Modifier: match[3],
}
metar.Clouds = append(metar.Clouds, cloud)
pos++
} else {
break
}
}
cloudConsumed, clouds := parseClouds(tokens[pos:])
metar.Clouds = append(metar.Clouds, clouds...)
pos += cloudConsumed
// Parse temperature/dewpoint
if pos < len(tokens) {
@@ -213,6 +105,53 @@ func parseMetar(raw string, reportType string) (*weather.Metar, error) {
return metar, nil
}
func parseMetarHeader(tokens []string, raw string, reportType string) (*weather.Metar, int, error) {
metar := &weather.Metar{
ReportType: weather.ReportType(reportType),
RawTextVal: raw,
Warnings: []string{},
Clouds: []weather.Cloud{},
Phenomena: []weather.Phenomenon{},
}
pos := 0
if pos >= len(tokens) {
return nil, 0, weather.ErrMissingStation
}
pos++
if pos >= len(tokens) {
return nil, 0, weather.ErrMissingStation
}
metar.StationID = tokens[pos]
pos++
if pos >= len(tokens) {
return nil, 0, weather.ErrMissingTime
}
issueTime, err := ParseTime(tokens[pos])
if err != nil {
return nil, 0, fmt.Errorf("failed to parse issue time: %w", err)
}
metar.IssueTimeVal = issueTime
metar.ObsTime = issueTime
pos++
if pos < len(tokens) {
if match := modifierPattern.FindStringSubmatch(tokens[pos]); match != nil {
metar.Modifier = match[1]
pos++
}
}
consumed, wind, visibility := parseWindAndVisibility(tokens[pos:])
metar.Wind = wind
metar.Visibility = visibility
pos += consumed
return metar, pos, nil
}
// parseWind parses wind information
func parseWind(token string) *weather.Wind {
match := windPattern.FindStringSubmatch(token)
@@ -302,4 +241,3 @@ func parseVisibility(token string) *weather.Visibility {
return vis
}
+101 -150
View File
@@ -15,56 +15,16 @@ func parseTaf(raw string) (*weather.Taf, error) {
return nil, weather.ErrInvalidFormat
}
taf := &weather.Taf{
ReportType: weather.ReportTypeTAF,
RawTextVal: raw,
Warnings: []string{},
Periods: []weather.TafPeriod{},
}
pos := 0
// Skip TAF - first token
if pos >= len(tokens) {
return nil, weather.ErrMissingStation
}
pos++
// Parse station (second token)
if pos >= len(tokens) {
return nil, weather.ErrMissingStation
}
taf.StationID = tokens[pos]
pos++
// Parse issue time (DDHHmmZ) - third token
if pos >= len(tokens) {
return nil, weather.ErrMissingTime
}
issueTime, err := ParseTime(tokens[pos])
taf, pos, err := parseTafHeader(tokens, raw)
if err != nil {
return nil, fmt.Errorf("failed to parse issue time: %w", err)
return nil, err
}
taf.IssueTimeVal = issueTime
pos++
// Parse validity period (DDHH/DDHH)
if pos >= len(tokens) {
return nil, fmt.Errorf("missing validity period")
}
validFrom, validTo, err := ParseTAFValidity(tokens[pos], taf.IssueTimeVal)
if err != nil {
return nil, fmt.Errorf("failed to parse validity period: %w", err)
}
taf.ValidFrom = validFrom
taf.ValidTo = validTo
pos++
// Parse main forecast period (before any FM/TEMPO/BECMG)
mainPeriod := weather.TafPeriod{
Type: "MAIN",
ValidFrom: validFrom,
ValidTo: validTo,
ValidFrom: taf.ValidFrom,
ValidTo: taf.ValidTo,
}
// Find first special section
@@ -83,8 +43,23 @@ func parseTaf(raw string) (*weather.Taf, error) {
// Parse main period tokens
if firstSpecialIdx > pos {
if firstSpecialIdx < len(tokens) {
upperToken := strings.ToUpper(tokens[firstSpecialIdx])
if strings.HasPrefix(upperToken, "FM") {
if match := tafFMPattern.FindStringSubmatch(tokens[firstSpecialIdx]); match != nil {
day, _ := strconv.Atoi(match[1])
hour, _ := strconv.Atoi(match[2])
min, _ := strconv.Atoi(match[3])
fmStart := resolveDayTime(taf.ValidFrom, day, hour, min)
if fmStart.Before(mainPeriod.ValidTo) {
mainPeriod.ValidTo = fmStart
}
}
}
}
mainTokens := tokens[pos:firstSpecialIdx]
parsePeriodElements(mainTokens, &mainPeriod, taf)
parsePeriodElements(mainTokens, &mainPeriod, &taf.Warnings)
taf.Periods = append(taf.Periods, mainPeriod)
pos = firstSpecialIdx
}
@@ -102,7 +77,7 @@ func parseTaf(raw string) (*weather.Taf, error) {
pos++
case strings.HasPrefix(upperToken, "FM"):
period, newPos, err := parseFMPeriod(tokens, pos, taf.IssueTimeVal)
period, newPos, err := parseFMPeriod(tokens, pos, taf.ValidFrom, taf.ValidTo, &taf.Warnings)
if err != nil {
taf.Warnings = append(taf.Warnings, fmt.Sprintf("failed to parse FM period: %v", err))
pos++
@@ -116,7 +91,7 @@ func parseTaf(raw string) (*weather.Taf, error) {
pos = newPos
case strings.HasPrefix(upperToken, "TEMPO"):
period, newPos, err := parseTEMPOPeriod(tokens, pos, taf.IssueTimeVal)
period, newPos, err := parseTEMPOPeriod(tokens, pos, taf.ValidFrom, &taf.Warnings)
if err != nil {
taf.Warnings = append(taf.Warnings, fmt.Sprintf("failed to parse TEMPO period: %v", err))
pos++
@@ -130,7 +105,7 @@ func parseTaf(raw string) (*weather.Taf, error) {
pos = newPos
case strings.HasPrefix(upperToken, "BECMG"):
period, newPos, err := parseBECMGPeriod(tokens, pos, taf.IssueTimeVal)
period, newPos, err := parseBECMGPeriod(tokens, pos, taf.ValidFrom, &taf.Warnings)
if err != nil {
taf.Warnings = append(taf.Warnings, fmt.Sprintf("failed to parse BECMG period: %v", err))
pos++
@@ -159,8 +134,60 @@ func parseTaf(raw string) (*weather.Taf, error) {
return taf, nil
}
func parseTafHeader(tokens []string, raw string) (*weather.Taf, int, error) {
taf := &weather.Taf{
ReportType: weather.ReportTypeTAF,
RawTextVal: raw,
Warnings: []string{},
Periods: []weather.TafPeriod{},
}
pos := 0
if pos >= len(tokens) {
return nil, 0, weather.ErrMissingStation
}
pos++
if pos >= len(tokens) {
return nil, 0, weather.ErrMissingStation
}
taf.StationID = tokens[pos]
pos++
if pos >= len(tokens) {
return nil, 0, weather.ErrMissingTime
}
issueTime, err := ParseTime(tokens[pos])
if err != nil {
return nil, 0, fmt.Errorf("failed to parse issue time: %w", err)
}
taf.IssueTimeVal = issueTime
pos++
if pos >= len(tokens) {
return nil, 0, fmt.Errorf("missing validity period")
}
validFrom, validTo, err := ParseTAFValidity(tokens[pos], taf.IssueTimeVal)
if err != nil {
return nil, 0, fmt.Errorf("failed to parse validity period: %w", err)
}
taf.ValidFrom = validFrom
taf.ValidTo = validTo
pos++
return taf, pos, nil
}
func resolveDayTime(base time.Time, day, hour, min int) time.Time {
t := time.Date(base.Year(), base.Month(), day, hour, min, 0, 0, time.UTC)
if day < base.Day() {
t = t.AddDate(0, 1, 0)
}
return t
}
// parseFMPeriod parses an FM (from) period
func parseFMPeriod(tokens []string, startPos int, issueTimeVal time.Time) (weather.TafPeriod, int, error) {
func parseFMPeriod(tokens []string, startPos int, validityFrom, validityTo time.Time, warnings *[]string) (weather.TafPeriod, int, error) {
period := weather.TafPeriod{
Type: "FM",
}
@@ -174,9 +201,8 @@ func parseFMPeriod(tokens []string, startPos int, issueTimeVal time.Time) (weath
hour, _ := strconv.Atoi(match[2])
min, _ := strconv.Atoi(match[3])
year := issueTimeVal.Year()
month := issueTimeVal.Month()
period.ValidFrom = time.Date(year, month, day, hour, min, 0, 0, time.UTC)
period.ValidFrom = resolveDayTime(validityFrom, day, hour, min)
period.ValidTo = validityTo
// Find end of this period (next FM, TEMPO, BECMG, or end)
endPos := len(tokens)
@@ -193,7 +219,7 @@ func parseFMPeriod(tokens []string, startPos int, issueTimeVal time.Time) (weath
// Parse period elements
periodTokens := tokens[startPos+1 : endPos]
parsePeriodElements(periodTokens, &period, nil)
parsePeriodElements(periodTokens, &period, warnings)
// Set valid_to to start of next period or end of validity
if endPos < len(tokens) {
@@ -204,7 +230,10 @@ func parseFMPeriod(tokens []string, startPos int, issueTimeVal time.Time) (weath
nextDay, _ := strconv.Atoi(match[1])
nextHour, _ := strconv.Atoi(match[2])
nextMin, _ := strconv.Atoi(match[3])
period.ValidTo = time.Date(year, month, nextDay, nextHour, nextMin, 0, 0, time.UTC)
nextStart := resolveDayTime(period.ValidFrom, nextDay, nextHour, nextMin)
if nextStart.Before(period.ValidTo) {
period.ValidTo = nextStart
}
}
}
}
@@ -213,7 +242,7 @@ func parseFMPeriod(tokens []string, startPos int, issueTimeVal time.Time) (weath
}
// parseTEMPOPeriod parses a TEMPO (temporary) period
func parseTEMPOPeriod(tokens []string, startPos int, issueTimeVal time.Time) (weather.TafPeriod, int, error) {
func parseTEMPOPeriod(tokens []string, startPos int, validityFrom time.Time, warnings *[]string) (weather.TafPeriod, int, error) {
period := weather.TafPeriod{
Type: "TEMPO",
}
@@ -228,16 +257,8 @@ func parseTEMPOPeriod(tokens []string, startPos int, issueTimeVal time.Time) (we
toDay, _ := strconv.Atoi(match[3])
toHour, _ := strconv.Atoi(match[4])
year := issueTimeVal.Year()
month := issueTimeVal.Month()
period.ValidFrom = time.Date(year, month, fromDay, fromHour, 0, 0, 0, time.UTC)
period.ValidTo = time.Date(year, month, toDay, toHour, 0, 0, 0, time.UTC)
// If toDay < fromDay, assume next month
if toDay < fromDay {
period.ValidTo = period.ValidTo.AddDate(0, 1, 0)
}
period.ValidFrom = resolveDayTime(validityFrom, fromDay, fromHour, 0)
period.ValidTo = resolveDayTime(period.ValidFrom, toDay, toHour, 0)
// Find end of this period
endPos := startPos + 1
@@ -254,13 +275,13 @@ func parseTEMPOPeriod(tokens []string, startPos int, issueTimeVal time.Time) (we
// Parse period elements
periodTokens := tokens[startPos+1 : endPos]
parsePeriodElements(periodTokens, &period, nil)
parsePeriodElements(periodTokens, &period, warnings)
return period, endPos, nil
}
// parseBECMGPeriod parses a BECMG (becoming) period
func parseBECMGPeriod(tokens []string, startPos int, issueTimeVal time.Time) (weather.TafPeriod, int, error) {
func parseBECMGPeriod(tokens []string, startPos int, validityFrom time.Time, warnings *[]string) (weather.TafPeriod, int, error) {
period := weather.TafPeriod{
Type: "BECMG",
}
@@ -275,16 +296,8 @@ func parseBECMGPeriod(tokens []string, startPos int, issueTimeVal time.Time) (we
toDay, _ := strconv.Atoi(match[3])
toHour, _ := strconv.Atoi(match[4])
year := issueTimeVal.Year()
month := issueTimeVal.Month()
period.ValidFrom = time.Date(year, month, fromDay, fromHour, 0, 0, 0, time.UTC)
period.ValidTo = time.Date(year, month, toDay, toHour, 0, 0, 0, time.UTC)
// If toDay < fromDay, assume next month
if toDay < fromDay {
period.ValidTo = period.ValidTo.AddDate(0, 1, 0)
}
period.ValidFrom = resolveDayTime(validityFrom, fromDay, fromHour, 0)
period.ValidTo = resolveDayTime(period.ValidFrom, toDay, toHour, 0)
// Find end of this period
endPos := startPos + 1
@@ -301,86 +314,24 @@ func parseBECMGPeriod(tokens []string, startPos int, issueTimeVal time.Time) (we
// Parse period elements
periodTokens := tokens[startPos+1 : endPos]
parsePeriodElements(periodTokens, &period, nil)
parsePeriodElements(periodTokens, &period, warnings)
return period, endPos, nil
}
// parsePeriodElements parses common elements (wind, visibility, clouds, phenomena) for a TAF period
func parsePeriodElements(tokens []string, period *weather.TafPeriod, taf *weather.Taf) {
pos := 0
// Parse wind
if pos < len(tokens) {
if wind := parseWind(tokens[pos]); wind != nil {
func parsePeriodElements(tokens []string, period *weather.TafPeriod, warnings *[]string) {
pos, wind, visibility := parseWindAndVisibility(tokens)
period.Wind = wind
pos++
period.Visibility = visibility
// Check for variable wind
if pos < len(tokens) {
if match := variableWindPattern.FindStringSubmatch(tokens[pos]); match != nil {
from, _ := strconv.Atoi(match[1])
to, _ := strconv.Atoi(match[2])
period.Wind.Variable = true
period.Wind.VariableFrom = from
period.Wind.VariableTo = to
pos++
}
}
}
}
phenomConsumed, phenomena := parsePhenomena(tokens[pos:])
period.Phenomena = append(period.Phenomena, phenomena...)
pos += phenomConsumed
// Parse visibility
if pos < len(tokens) {
if vis := parseVisibility(tokens[pos]); vis != nil {
period.Visibility = vis
pos++
}
}
cloudConsumed, clouds := parseClouds(tokens[pos:])
period.Clouds = append(period.Clouds, clouds...)
pos += cloudConsumed
// Parse weather phenomena
for pos < len(tokens) {
if match := phenomenonPattern.FindStringSubmatch(tokens[pos]); match != nil {
phenom := weather.Phenomenon{
Intensity: match[1],
Descriptor: match[2],
Weather: match[3],
appendPeriodWarnings(warnings, tokens[pos:])
}
period.Phenomena = append(period.Phenomena, phenom)
pos++
} else {
break
}
}
// Parse clouds
for pos < len(tokens) {
if match := skyClearPattern.FindStringSubmatch(tokens[pos]); match != nil {
// SKC, CLR, NSC - no clouds
pos++
break
}
if match := cloudPattern.FindStringSubmatch(tokens[pos]); match != nil {
alt, _ := strconv.Atoi(match[2])
cloud := weather.Cloud{
Type: match[1],
Altitude: alt * 100, // Convert to feet
Modifier: match[3],
}
period.Clouds = append(period.Clouds, cloud)
pos++
} else {
break
}
}
// Collect any remaining unrecognized tokens as warnings
if taf != nil {
for pos < len(tokens) {
taf.Warnings = append(taf.Warnings, fmt.Sprintf("unrecognized token in period: %s", tokens[pos]))
pos++
}
}
}
@@ -1,6 +1,7 @@
package weather
import (
domainweather "caatsm/internal/domain/weather"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -24,6 +25,52 @@ var _ = Describe("TAF Parser", func() {
Expect(taf.Periods[1].Type).To(Equal("FM"))
})
It("should set final FM validTo to the TAF validity end", func() {
raw := "TAF KJFK 251200Z 2512/2612 35012KT 10SM FEW020 FM251800 36015KT 10SM SCT030="
taf, err := parseTaf(raw)
Expect(err).ToNot(HaveOccurred())
var fmPeriod *domainweather.TafPeriod
for i := range taf.Periods {
if taf.Periods[i].Type == "FM" {
fmPeriod = &taf.Periods[i]
break
}
}
Expect(fmPeriod).ToNot(BeNil())
Expect(fmPeriod.ValidTo).To(BeTemporally("==", taf.ValidTo))
})
It("should truncate main period at the first FM", func() {
raw := "TAF KJFK 251200Z 2512/2612 35012KT 10SM FEW020 FM251800 36015KT 10SM SCT030="
taf, err := parseTaf(raw)
Expect(err).ToNot(HaveOccurred())
var fmPeriod *domainweather.TafPeriod
for i := range taf.Periods {
if taf.Periods[i].Type == "FM" {
fmPeriod = &taf.Periods[i]
break
}
}
Expect(fmPeriod).ToNot(BeNil())
Expect(taf.Periods[0].Type).To(Equal("MAIN"))
Expect(taf.Periods[0].ValidTo).To(BeTemporally("==", fmPeriod.ValidFrom))
})
It("should roll FM into next month when day precedes validity start", func() {
raw := "TAF KJFK 301200Z 3012/0112 35012KT 10SM FEW020 FM010600 36015KT 10SM SCT030="
taf, err := parseTaf(raw)
Expect(err).ToNot(HaveOccurred())
var fmPeriod *domainweather.TafPeriod
for i := range taf.Periods {
if taf.Periods[i].Type == "FM" {
fmPeriod = &taf.Periods[i]
break
}
}
Expect(fmPeriod).ToNot(BeNil())
Expect(fmPeriod.ValidFrom).To(BeTemporally(">", taf.ValidFrom))
})
It("should parse TAF with TEMPO period", func() {
raw := "TAF KJFK 251200Z 2512/2612 35012KT 10SM FEW020 TEMPO2512/2515 27015G25KT 5SM -RA="
taf, err := parseTaf(raw)
@@ -57,6 +104,36 @@ var _ = Describe("TAF Parser", func() {
Expect(found).To(BeTrue())
})
It("should roll TEMPO into next month when day precedes validity start", func() {
raw := "TAF KJFK 301200Z 3012/0112 35012KT 10SM FEW020 TEMPO0102/0106 5SM -RA="
taf, err := parseTaf(raw)
Expect(err).ToNot(HaveOccurred())
var tempoPeriod *domainweather.TafPeriod
for i := range taf.Periods {
if taf.Periods[i].Type == "TEMPO" {
tempoPeriod = &taf.Periods[i]
break
}
}
Expect(tempoPeriod).ToNot(BeNil())
Expect(tempoPeriod.ValidFrom).To(BeTemporally(">", taf.ValidFrom))
})
It("should roll BECMG into next month when day precedes validity start", func() {
raw := "TAF KJFK 301200Z 3012/0112 35012KT 10SM FEW020 BECMG0102/0106 36015KT="
taf, err := parseTaf(raw)
Expect(err).ToNot(HaveOccurred())
var becmgPeriod *domainweather.TafPeriod
for i := range taf.Periods {
if taf.Periods[i].Type == "BECMG" {
becmgPeriod = &taf.Periods[i]
break
}
}
Expect(becmgPeriod).ToNot(BeNil())
Expect(becmgPeriod.ValidFrom).To(BeTemporally(">", taf.ValidFrom))
})
It("should parse TAF with PROB", func() {
raw := "TAF KJFK 251200Z 2512/2612 35012KT 10SM FEW020 PROB30 TEMPO2512/2515 27015G25KT 5SM -RA="
taf, err := parseTaf(raw)
@@ -95,4 +172,3 @@ var _ = Describe("TAF Parser", func() {
})
})
})