Made forecast-period conditionCode optional
All checks were successful
ci/woodpecker/manual/build-image Pipeline was successful
All checks were successful
ci/woodpecker/manual/build-image Pipeline was successful
This commit is contained in:
@@ -23,10 +23,11 @@ import (
|
||||
// builders by raw schema.
|
||||
//
|
||||
// Caveats / policy:
|
||||
// 1. NWS forecast periods do not include METAR presentWeather phenomena, so ConditionCode
|
||||
// is inferred from period.shortForecast (with a conservative icon-based fallback).
|
||||
// 2. Temperature is converted to °C when NWS supplies °F.
|
||||
// 3. WindSpeed is parsed from strings like "9 mph" / "10 to 15 mph" and converted to km/h.
|
||||
// 1. Hourly NWS forecast periods do not include METAR presentWeather phenomena, so
|
||||
// ConditionCode is inferred from period.shortForecast (with a conservative icon fallback).
|
||||
// 2. Narrative NWS periods intentionally leave ConditionCode unset.
|
||||
// 3. Temperature is converted to °C when NWS supplies °F.
|
||||
// 4. WindSpeed is parsed from strings like "9 mph" / "10 to 15 mph" and converted to km/h.
|
||||
type ForecastNormalizer struct{}
|
||||
|
||||
func (ForecastNormalizer) Match(e event.Event) bool {
|
||||
@@ -224,6 +225,7 @@ func mapHourlyForecastPeriod(idx int, p nwsHourlyForecastPeriod) (model.WeatherF
|
||||
// Infer WMO from shortForecast (and fall back to icon token).
|
||||
providerDesc := strings.TrimSpace(p.ShortForecast)
|
||||
wmo := wmoFromNWSForecast(providerDesc, p.Icon, tempC)
|
||||
wmoPtr := wmoCodePtr(wmo)
|
||||
|
||||
return model.WeatherForecastPeriod{
|
||||
StartTime: start,
|
||||
@@ -232,7 +234,7 @@ func mapHourlyForecastPeriod(idx int, p nwsHourlyForecastPeriod) (model.WeatherF
|
||||
Name: strings.TrimSpace(p.Name),
|
||||
IsDay: isDay,
|
||||
|
||||
ConditionCode: wmo,
|
||||
ConditionCode: wmoPtr,
|
||||
|
||||
// For forecasts, keep provider short forecast text as the human-facing description.
|
||||
TextDescription: providerDesc,
|
||||
@@ -264,9 +266,7 @@ func mapNarrativeForecastPeriod(idx int, p nwsNarrativeForecastPeriod) (model.We
|
||||
|
||||
tempC := tempCFromNWS(p.Temperature, p.TemperatureUnit)
|
||||
|
||||
// Infer WMO from shortForecast (and fall back to icon token).
|
||||
shortForecast := strings.TrimSpace(p.ShortForecast)
|
||||
wmo := wmoFromNWSForecast(shortForecast, p.Icon, tempC)
|
||||
|
||||
textDescription := strings.TrimSpace(p.DetailedForecast)
|
||||
if textDescription == "" {
|
||||
@@ -280,7 +280,7 @@ func mapNarrativeForecastPeriod(idx int, p nwsNarrativeForecastPeriod) (model.We
|
||||
Name: strings.TrimSpace(p.Name),
|
||||
IsDay: isDay,
|
||||
|
||||
ConditionCode: wmo,
|
||||
ConditionCode: nil,
|
||||
|
||||
TextDescription: textDescription,
|
||||
|
||||
@@ -292,3 +292,8 @@ func mapNarrativeForecastPeriod(idx int, p nwsNarrativeForecastPeriod) (model.We
|
||||
ProbabilityOfPrecipitationPercent: p.ProbabilityOfPrecipitation.Value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func wmoCodePtr(code model.WMOCode) *model.WMOCode {
|
||||
out := code
|
||||
return &out
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ func TestBuildHourlyForecastUsesShortForecastAsTextDescription(t *testing.T) {
|
||||
if got, want := run.Periods[0].TextDescription, "Mostly Cloudy"; got != want {
|
||||
t.Fatalf("TextDescription = %q, want %q", got, want)
|
||||
}
|
||||
if run.Periods[0].ConditionCode == nil {
|
||||
t.Fatalf("ConditionCode is nil, want inferred hourly WMO code")
|
||||
}
|
||||
|
||||
wantIssued := time.Date(2026, 3, 16, 18, 0, 0, 0, time.UTC)
|
||||
if !run.IssuedAt.Equal(wantIssued) {
|
||||
@@ -261,6 +264,9 @@ func TestBuildNarrativeForecastMapsExpectedFields(t *testing.T) {
|
||||
if p.ProbabilityOfPrecipitationPercent == nil || *p.ProbabilityOfPrecipitationPercent != 20 {
|
||||
t.Fatalf("ProbabilityOfPrecipitationPercent = %v, want 20", p.ProbabilityOfPrecipitationPercent)
|
||||
}
|
||||
if p.ConditionCode != nil {
|
||||
t.Fatalf("ConditionCode = %v, want nil for narrative period", p.ConditionCode)
|
||||
}
|
||||
|
||||
wantIssued := time.Date(2026, 3, 27, 15, 17, 1, 0, time.UTC)
|
||||
if !run.IssuedAt.Equal(wantIssued) {
|
||||
|
||||
@@ -98,6 +98,7 @@ func buildForecast(parsed omForecastResponse, fallbackIssued time.Time) (model.W
|
||||
}
|
||||
|
||||
wmo := wmoAt(parsed.Hourly.WeatherCode, i)
|
||||
wmoPtr := wmoCodePtr(wmo)
|
||||
canonicalText := standards.WMOText(wmo, isDay)
|
||||
|
||||
period := model.WeatherForecastPeriod{
|
||||
@@ -107,7 +108,7 @@ func buildForecast(parsed omForecastResponse, fallbackIssued time.Time) (model.W
|
||||
Name: "",
|
||||
IsDay: isDay,
|
||||
|
||||
ConditionCode: wmo,
|
||||
ConditionCode: wmoPtr,
|
||||
TextDescription: canonicalText,
|
||||
}
|
||||
|
||||
@@ -237,3 +238,8 @@ func wmoAt(vals []*int, idx int) model.WMOCode {
|
||||
}
|
||||
return model.WMOUnknown
|
||||
}
|
||||
|
||||
func wmoCodePtr(code model.WMOCode) *model.WMOCode {
|
||||
out := code
|
||||
return &out
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ func TestBuildForecastUsesCanonicalTextDescription(t *testing.T) {
|
||||
if got := run.Periods[0].TextDescription; got != expectedText {
|
||||
t.Fatalf("TextDescription = %q, want %q", got, expectedText)
|
||||
}
|
||||
if run.Periods[0].ConditionCode == nil {
|
||||
t.Fatalf("ConditionCode is nil, want mapped WMO code")
|
||||
}
|
||||
|
||||
wantIssued := time.Date(2026, 3, 16, 19, 0, 0, 0, time.UTC)
|
||||
if !run.IssuedAt.Equal(wantIssued) {
|
||||
|
||||
Reference in New Issue
Block a user