taf/taf.go

387 lines
11 KiB
Go

package taf
import (
"io"
"io/fs"
"math/big"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/alecthomas/participle/v2"
"go.elara.ws/taf/airports"
"go.elara.ws/taf/internal/parser"
"go.elara.ws/taf/units"
)
// DecodeString decodes a TAF string and returns a Forecast.
// This is equivalent to Decode(strings.NewReader(s)).
func DecodeString(s string) (*Forecast, error) {
return Decode(strings.NewReader(s))
}
// DecodeFile decodes a TAF string and returns a Forecast.
// This is equivalent to opening a file and passing it
// to Decode().
func DecodeFile(path string) (*Forecast, error) {
fl, err := os.Open(path)
if err != nil {
return nil, err
}
defer fl.Close()
return Decode(fl)
}
// Decode decodes the data in a reader using default options and
// returns a Forecast
func Decode(r io.Reader) (*Forecast, error) {
return DecodeWithOptions(r, Options{})
}
// Options contains options for the decoder
type Options struct {
// If this is set, all distance units in the forecast
// will be converted to the given unit
DistanceUnit units.Distance
// If this is set, all speed units in the forecast will
// be converted to the given unit
SpeedUnit units.Speed
// The Year field is used to calculate the full date that this
// report was published. If it's unset, the current year will be used.
Year int
// The Month field is used to calculate the full date that this
// report was published. If it's unset, the current month will be used.
Month time.Month
}
// DecodeWithOptions decodes the data in a reader and returns a Forecast
func DecodeWithOptions(r io.Reader, opts Options) (*Forecast, error) {
filename := "unknown"
switch r := r.(type) {
case *os.File:
filename = r.Name()
case fs.File:
fi, err := r.Stat()
if err == nil {
filename = fi.Name()
}
case *strings.Reader:
filename = "string"
}
if opts.Year == 0 {
opts.Year = time.Now().Year()
}
if opts.Month == 0 {
opts.Month = time.Now().Month()
}
ast, err := parser.Parser.Parse(filename, r)
if err != nil {
return nil, err
}
setProb := 0
fc := &Forecast{}
out := reflect.ValueOf(fc).Elem()
if ast.Type != nil {
fc.ReportType = convertReportType(*ast.Type)
}
for _, item := range ast.Items {
switch {
case item.ID != nil:
fc.Identifier = *item.ID
if a, ok := airports.Airports[fc.Identifier]; ok {
fc.Airport = a
}
case item.Time != nil:
t, err := parseTime(*item.Time, opts.Month, opts.Year)
if err != nil {
return nil, participle.Errorf(item.Pos, "time: %s", err)
}
setField(out, "PublishTime", t)
// The Time item always comes with a Valid as well because
// of the way it's parsed into the AST
vp, err := parseValid(item.Valid, opts.Month, opts.Year)
if err != nil {
return nil, participle.Errorf(item.Pos, "time: %s", err)
}
setField(out, "Valid", vp)
case item.Weather != nil:
appendField(out, "Weather", Weather{
Modifier: convertModifier(item.Weather.Modifier),
Descriptor: convertDescriptor(item.Weather.Descriptor),
Precipitation: convertPrecipitation(item.Weather.Precipitation),
Obscuration: convertObscuration(item.Weather.Obscuration),
Phenomenon: convertPhenomenon(item.Weather.Other),
})
case item.Vicinity != nil:
appendField(out, "Weather", Weather{
Vicinity: true,
Descriptor: convertDescriptor(item.Vicinity.Descriptor),
Precipitation: convertPrecipitation(item.Vicinity.Precipitation),
})
case item.SkyCondition != nil:
var altitude int
if item.SkyCondition.Altitude != "" {
altitude, err = strconv.Atoi(item.SkyCondition.Altitude)
if err != nil {
return nil, participle.Errorf(item.SkyCondition.Pos, "sky: %s", err)
}
}
appendField(out, "SkyCondition", SkyCondition{
Altitude: altitude * 100, // Scale factor for altitude is 100
Type: convertSkyConditionType(item.SkyCondition.Type),
CloudType: convertCloudType(item.SkyCondition.CloudType),
})
case item.Temperature != nil:
vt, err := parseValidTime(item.Temperature.Time, opts.Month, opts.Year)
if err != nil {
return nil, participle.Errorf(item.Temperature.Pos, "temp: %s", err)
}
val, err := strconv.Atoi(item.Temperature.Value)
if err != nil {
return nil, participle.Errorf(item.Temperature.Pos, "temp: %s", err)
}
appendField(out, "Temperature", Temperature{
Type: convertTemperatureType(item.Temperature.Type),
Time: vt,
Value: val,
})
case item.Visibility != nil:
// This value may have a space at the end if there's no unit
item.Visibility.Value = strings.TrimSpace(item.Visibility.Value)
// Create a new rational number
ratNum := new(big.Rat)
// If there's a space, this is a mixed number, split it at the space
if before, after, ok := strings.Cut(item.Visibility.Value, " "); ok {
// Set the rational number to the fraction of the mixed number
ratNum, ok = ratNum.SetString(after)
if !ok {
return nil, participle.Errorf(item.Visibility.Pos, "visibility: invalid fraction %q", after)
}
// Create a new rational number and set it to the whole part of
// the mixed number
add, ok := new(big.Rat).SetString(before)
if !ok {
return nil, participle.Errorf(item.Visibility.Pos, "visibility: invalid whole number %q", before)
}
// Add the whole part to the fractional part
ratNum = ratNum.Add(ratNum, add)
} else {
// There's no space, so this is just a fraction or a whole number.
// Just set the rational number to the whole string.
ratNum, ok = ratNum.SetString(before)
if !ok {
return nil, participle.Errorf(item.Visibility.Pos, "visibility: invalid fraction %q", after)
}
}
// If there's no unit, set the unit to meters
if item.Visibility.Unit == "" {
item.Visibility.Unit = "M"
}
unit, ok := units.ParseDistance(item.Visibility.Unit)
if !ok {
return nil, participle.Errorf(item.Visibility.Pos, "visibility: invalid unit %q", item.Visibility.Unit)
}
val, _ := ratNum.Float64()
if opts.DistanceUnit != "" {
val = unit.Convert(opts.DistanceUnit, val)
unit = opts.DistanceUnit
}
setField(out, "Visibility", Visibility{
Plus: item.Visibility.Plus,
Value: val,
Unit: unit,
})
case item.WindSpeed != nil:
var direction int
// If the wind speed is variable, there's no direction to worry about
if !item.WindSpeed.Variable {
// The length of the value must be at least 5 (3 characters for direction and 2 for speed)
if len(item.WindSpeed.Value) < 5 {
return nil, participle.Errorf(item.WindSpeed.Pos, "wind: invalid length (%d)", len(item.WindSpeed.Value))
}
// First three characters are the direction
direction, err = strconv.Atoi(item.WindSpeed.Value[:3])
if err != nil {
return nil, participle.Errorf(item.WindSpeed.Pos, "wind: %s", err)
}
// Set the value to the last two characters so it can be processed
// as just a speed.
item.WindSpeed.Value = item.WindSpeed.Value[3:]
// The direction is in degrees so it may not go above 360 or below 0
if direction > 360 || direction < 0 {
return nil, participle.Errorf(item.WindSpeed.Pos, "wind: invalid direction (%d)", direction)
}
}
// If there was a direction, it was removed above, so now we can just
// get the speed by parsing the string
speed, err := strconv.Atoi(item.WindSpeed.Value)
if err != nil {
return nil, participle.Errorf(item.WindSpeed.Pos, "wind: %s", err)
}
var gusts int
if item.WindSpeed.Gusts != "" {
gusts, err = strconv.Atoi(item.WindSpeed.Gusts)
if err != nil {
return nil, participle.Errorf(item.WindSpeed.Pos, "wind: %s", err)
}
}
var windshear int
if item.WindSpeed.WindShear != "" {
windshear, err = strconv.Atoi(item.WindSpeed.WindShear)
if err != nil {
return nil, participle.Errorf(item.WindSpeed.Pos, "wind: %s", err)
}
}
unit, ok := units.ParseSpeed(item.WindSpeed.Unit)
if !ok {
return nil, participle.Errorf(item.WindSpeed.Pos, "wind: invalid unit %q", item.Visibility.Unit)
}
if opts.SpeedUnit != "" {
speed = unit.Convert(opts.SpeedUnit, speed)
if gusts != 0 {
gusts = unit.Convert(opts.SpeedUnit, gusts)
}
unit = opts.SpeedUnit
}
setField(out, "Wind", Wind{
Gusts: gusts,
Speed: speed,
WindShear: windshear * 100, // Scale factor for altitude is 100
Direction: Direction{
Variable: item.WindSpeed.Variable,
Value: direction,
},
Unit: unit,
})
case item.Flag != nil:
switch {
case item.Flag.CAVOK:
appendField(out, "Flags", CeilingAndVisibilityOK)
}
case item.Change != nil:
ch := &Change{
Type: convertChangeType(item.Change.Type),
}
// if setProb is set, add the probability within it to the change,
// then reset the variable.
if setProb != 0 {
ch.Probability = setProb
setProb = 0
}
// FM changes don't have a valid pair, they only come with a single time string
if ch.Type == From {
t, err := parseTime(item.Change.Time, opts.Month, opts.Year)
if err != nil {
return nil, participle.Errorf(item.Change.Pos, "changes: %s", err)
}
ch.Valid = ValidPair{From: t}
} else {
vp, err := parseValid(item.Change.Valid, opts.Month, opts.Year)
if err != nil {
return nil, participle.Errorf(item.Change.Pos, "changes: %s", err)
}
ch.Valid = vp
}
fc.Changes = append(fc.Changes, ch)
// Set out to the change value so that future mutations
// happen to the change rather than the root forecast.
out = reflect.ValueOf(ch).Elem()
case item.Probability != nil:
prob, err := strconv.Atoi(item.Probability.Value)
if err != nil {
return nil, participle.Errorf(item.Probability.Pos, "prob: %s", err)
}
// If the time is empty, this probability belongs to the
// next change.
if item.Probability.Valid.Start == "" {
// Set the setProb variable. This will let the decoder know to add it to the next change.
setProb = prob
} else {
pr := &Probability{Value: prob}
pr.Valid, err = parseValid(&item.Probability.Valid, opts.Month, opts.Year)
if err != nil {
return nil, participle.Errorf(item.Probability.Pos, "prob: %s", err)
}
fc.Probabilities = append(fc.Probabilities, pr)
// Set out to the probability value so that future mutations
// happen to the probability rather than the root forecast.
out = reflect.ValueOf(pr).Elem()
}
case item.Remark != nil:
fc.Remark = strings.TrimSpace(strings.TrimPrefix(*item.Remark, "RMK"))
}
}
return fc, nil
}
// setField sets a field of a struct to a value.
//
// This is used to allow mutations to happen on either
// the root forecast or a change or probability. It makes it
// easier to handle the different types.
func setField(rv reflect.Value, name string, to any) {
rv.FieldByName(name).Set(reflect.ValueOf(to))
}
// appendField appends a value to a slice in the field of a struct.
//
// This is used to allow mutations to happen on either
// the root forecast or a change or probability. It makes it
// easier to handle the different types.
func appendField(rv reflect.Value, name string, items ...any) {
f := rv.FieldByName(name)
f.Set(reflect.Append(f, anyToValues(items)...))
}
// anyToValues converts a slice of any type to a slice
// of reflect values.
func anyToValues(items []any) []reflect.Value {
out := make([]reflect.Value, len(items))
for i, item := range items {
out[i] = reflect.ValueOf(item)
}
return out
}