Add weather report parsing for METAR, SPECI, and TAF

Implement comprehensive weather parsing capabilities following Clean
Architecture principles with composite parser pattern for routing between
aviation and weather messages.

## Features Added

- Weather report parsing (METAR, SPECI, TAF)
- Composite parser pattern for message routing
- Lenient parsing with warnings for unrecognized tokens
- Support for PROB and RMK sections in TAF
- Rich domain modeling with typed weather elements

## Architecture

**Domain Layer** (internal/domain/weather/):
- WeatherMessage interface with Metar and Taf implementations
- Weather elements: Wind, Visibility, Cloud, Temperature, Altimeter, Phenomenon
- Domain errors: ErrInvalidFormat, ErrMissingStation, ErrMissingTime

**Port Layer** (internal/port/weather_parser.go):
- WeatherParser interface with CanParse and Parse methods

**Adapter Layer** (internal/adapter/parser/weather/):
- WeatherParserImpl with classification and parsing logic
- Comprehensive regex patterns for weather elements
- METAR/SPECI parser with element extraction
- TAF parser with period handling (FM, TEMPO, BECMG, PROB)
- Helper functions for time parsing and unit conversions

**Composite Parser** (internal/adapter/parser/composite.go):
- Routes weather reports to weather parser
- Falls back to aviation parser for telegrams
- Converts WeatherMessage to ParsedTelegram format

## Integration

- Updated ProvideParser to create composite parser with weather parser
- Added weather parser to Wire DI configuration
- Updated processor_bench_test.go for weather parser integration
- Documentation added in docs/weather-parser.md

## Testing

- 29 comprehensive tests for weather parsing (all passing)
- Tests for classification, METAR, SPECI, TAF, and composite routing
- Benchmark compatibility maintained

## Fixes Applied

- TAF PROB parsing: Include PROB/RMK in special section detection
- Composite test: Updated to use properly formatted AFTN telegram
- Linter issues: Switch statement refactor, removed unused patterns
- Ineffective break statement fixed in TAF parser

## Coverage

~1,743 lines of new code with:
- Complete METAR/SPECI parsing
- TAF parsing with period support
- Lenient error handling with warnings
- Unit conversions and time utilities

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
windyboy
2025-12-24 16:38:03 +08:00
co-authored by Claude Sonnet 4.5
parent c1808c0523
commit d0fd461e38
24 changed files with 1873 additions and 11 deletions
+60
View File
@@ -0,0 +1,60 @@
package weather
import "time"
// Wind represents wind information
type Wind struct {
Direction int `json:"direction"` // Degrees
Speed int `json:"speed"` // KT or MPS
Gust int `json:"gust,omitempty"` // Gust speed
Variable bool `json:"variable,omitempty"` // VRB
VariableFrom int `json:"variable_from,omitempty"` // Variable wind from direction
VariableTo int `json:"variable_to,omitempty"` // Variable wind to direction
Unit string `json:"unit"` // "KT", "MPS"
}
// Visibility represents visibility information
type Visibility struct {
Distance float64 `json:"distance"` // Meters or statute miles
Unit string `json:"unit"` // "M", "SM"
Direction string `json:"direction,omitempty"` // Directional visibility
Modifier string `json:"modifier,omitempty"` // +, -, M, P
}
// Cloud represents cloud information
type Cloud struct {
Type string `json:"type"` // FEW, SCT, BKN, OVC, VV
Altitude int `json:"altitude"` // Feet
Modifier string `json:"modifier,omitempty"` // CB, TCU
}
// Temperature represents temperature or dewpoint
type Temperature struct {
Value float64 `json:"value"`
Unit string `json:"unit"` // "C"
}
// Altimeter represents altimeter setting
type Altimeter struct {
Value float64 `json:"value"`
Unit string `json:"unit"` // "QNH" (hPa), "A" (inHg)
}
// Phenomenon represents weather phenomenon
type Phenomenon struct {
Intensity string `json:"intensity,omitempty"` // -, +
Descriptor string `json:"descriptor,omitempty"` // MI, BC, PR, TS, etc.
Weather string `json:"weather"` // RA, SN, FG, etc.
}
// TafPeriod represents a TAF period (FM, TEMPO, BECMG, or main forecast)
type TafPeriod struct {
Type string `json:"type"` // "FM", "TEMPO", "BECMG", "MAIN"
ValidFrom time.Time `json:"valid_from,omitempty"`
ValidTo time.Time `json:"valid_to,omitempty"`
Wind *Wind `json:"wind,omitempty"`
Visibility *Visibility `json:"visibility,omitempty"`
Clouds []Cloud `json:"clouds,omitempty"`
Phenomena []Phenomenon `json:"phenomena,omitempty"`
Probability int `json:"probability,omitempty"` // PROB30, PROB40
}
+18
View File
@@ -0,0 +1,18 @@
package weather
import "errors"
var (
// ErrInvalidFormat indicates an invalid weather report format
ErrInvalidFormat = errors.New("invalid weather report format")
// ErrUnsupportedToken indicates an unsupported token in the report
ErrUnsupportedToken = errors.New("unsupported token")
// ErrMissingStation indicates missing station identifier
ErrMissingStation = errors.New("missing station identifier")
// ErrMissingTime indicates missing time information
ErrMissingTime = errors.New("missing time information")
)
+98
View File
@@ -0,0 +1,98 @@
package weather
import "time"
// ReportType represents the type of weather report
type ReportType string
const (
ReportTypeMETAR ReportType = "METAR"
ReportTypeSPECI ReportType = "SPECI"
ReportTypeTAF ReportType = "TAF"
)
// WeatherMessage is the common interface for all weather messages
type WeatherMessage interface {
Type() ReportType
Station() string
IssueTime() time.Time
RawText() string
}
// Metar represents a METAR or SPECI weather report
type Metar struct {
ReportType ReportType `json:"type"`
StationID string `json:"station"`
IssueTimeVal time.Time `json:"issue_time"`
ObsTime time.Time `json:"obs_time,omitempty"`
RawTextVal string `json:"raw_text"`
// Core elements
Wind *Wind `json:"wind,omitempty"`
Visibility *Visibility `json:"visibility,omitempty"`
Clouds []Cloud `json:"clouds,omitempty"`
Temperature *Temperature `json:"temperature,omitempty"`
Dewpoint *Temperature `json:"dewpoint,omitempty"`
Altimeter *Altimeter `json:"altimeter,omitempty"`
Phenomena []Phenomenon `json:"phenomena,omitempty"`
// Optional fields
Modifier string `json:"modifier,omitempty"` // AUTO, COR
Remarks string `json:"remarks,omitempty"`
Warnings []string `json:"warnings,omitempty"` // Unrecognized tokens
}
// Type returns the report type
func (m *Metar) Type() ReportType {
return m.ReportType
}
// Station returns the station identifier
func (m *Metar) Station() string {
return m.StationID
}
// IssueTime returns the issue time
func (m *Metar) IssueTime() time.Time {
return m.IssueTimeVal
}
// RawText returns the raw text
func (m *Metar) RawText() string {
return m.RawTextVal
}
// Taf represents a TAF (Terminal Aerodrome Forecast) weather report
type Taf struct {
ReportType ReportType `json:"type"`
StationID string `json:"station"`
IssueTimeVal time.Time `json:"issue_time"`
ValidFrom time.Time `json:"valid_from"`
ValidTo time.Time `json:"valid_to"`
RawTextVal string `json:"raw_text"`
Periods []TafPeriod `json:"periods"` // FM, TEMPO, BECMG segments
Remarks string `json:"remarks,omitempty"`
Warnings []string `json:"warnings,omitempty"`
}
// Type returns the report type
func (t *Taf) Type() ReportType {
return t.ReportType
}
// Station returns the station identifier
func (t *Taf) Station() string {
return t.StationID
}
// IssueTime returns the issue time
func (t *Taf) IssueTime() time.Time {
return t.IssueTimeVal
}
// RawText returns the raw text
func (t *Taf) RawText() string {
return t.RawTextVal
}