Remove dormant forecast comparison policy
This commit is contained in:
@@ -65,12 +65,6 @@ dayparts:
|
|||||||
start: "17:00"
|
start: "17:00"
|
||||||
end: "24:00"
|
end: "24:00"
|
||||||
|
|
||||||
recent_change:
|
|
||||||
temperature_degrees: 5
|
|
||||||
precip_probability_points: 20
|
|
||||||
wind_gust_miles_per_hour: 10
|
|
||||||
precip_timing_shift_minutes: 120
|
|
||||||
|
|
||||||
reports:
|
reports:
|
||||||
daily:
|
daily:
|
||||||
distributor:
|
distributor:
|
||||||
|
|||||||
@@ -1,313 +0,0 @@
|
|||||||
// Package changes compares structured module snapshots.
|
|
||||||
package changes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Thresholds struct {
|
|
||||||
TemperatureDegrees float64
|
|
||||||
PrecipProbabilityPoints int
|
|
||||||
WindGustMilesPerHour int
|
|
||||||
PrecipTimingShiftMinutes int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Change struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Previous string `json:"previous,omitempty"`
|
|
||||||
Current string `json:"current,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func CompareDaily(previous module.Snapshot, current module.Snapshot, thresholds Thresholds) ([]Change, error) {
|
|
||||||
previousSummary, err := requiredStanza[dailySummaryStanza](previous, "derived_daily_summary")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("previous daily summary: %w", err)
|
|
||||||
}
|
|
||||||
currentSummary, err := requiredStanza[dailySummaryStanza](current, "derived_daily_summary")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("current daily summary: %w", err)
|
|
||||||
}
|
|
||||||
previousDayparts, err := requiredStanza[map[string]daypartSummaryStanza](previous, "derived_daypart_summaries")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("previous daypart summaries: %w", err)
|
|
||||||
}
|
|
||||||
currentDayparts, err := requiredStanza[map[string]daypartSummaryStanza](current, "derived_daypart_summaries")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("current daypart summaries: %w", err)
|
|
||||||
}
|
|
||||||
previousAlerts, _, err := module.StanzaValue[alertDigestStanza](previous, "alert_digest")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
currentAlerts, _, err := module.StanzaValue[alertDigestStanza](current, "alert_digest")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
previousTiming, previousHasTiming, err := module.StanzaValue[precipTimingStanza](previous, "precip_timing")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
currentTiming, currentHasTiming, err := module.StanzaValue[precipTimingStanza](current, "precip_timing")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var changes []Change
|
|
||||||
changes = append(changes, compareTemperatureValues("Low", previousSummary.LowTempF, currentSummary.LowTempF, thresholds.TemperatureDegrees)...)
|
|
||||||
changes = append(changes, compareTemperatureValues("High", previousSummary.HighTempF, currentSummary.HighTempF, thresholds.TemperatureDegrees)...)
|
|
||||||
changes = append(changes, comparePrecipitationValues(previousSummary.DailyPrecipitationProbability, currentSummary.DailyPrecipitationProbability, thresholds.PrecipProbabilityPoints, "")...)
|
|
||||||
if previousHasTiming && currentHasTiming {
|
|
||||||
changes = append(changes, comparePrecipTiming(previousTiming.MaxPopTime, currentTiming.MaxPopTime, thresholds.PrecipTimingShiftMinutes, "")...)
|
|
||||||
}
|
|
||||||
changes = append(changes, compareWindValues(previousSummary.MaxWindGustMph, currentSummary.MaxWindGustMph, thresholds.WindGustMilesPerHour, "")...)
|
|
||||||
changes = append(changes, compareAlerts(previousAlerts.Relevant, currentAlerts.Relevant)...)
|
|
||||||
changes = append(changes, compareIndicators(aggregateIndicators(previousDayparts), aggregateIndicators(currentDayparts), "")...)
|
|
||||||
sortChanges(changes)
|
|
||||||
return changes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type dailySummaryStanza struct {
|
|
||||||
Date string `json:"date,omitempty"`
|
|
||||||
HighTempF *int `json:"high_temp_f,omitempty"`
|
|
||||||
LowTempF *int `json:"low_temp_f,omitempty"`
|
|
||||||
DailyPrecipitationProbability *int `json:"daily_precipitation_probability,omitempty"`
|
|
||||||
MaxWindGustMph *int `json:"max_wind_gust_mph,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type daypartSummaryStanza struct {
|
|
||||||
Date string `json:"date,omitempty"`
|
|
||||||
PeriodBegins string `json:"period_begins,omitempty"`
|
|
||||||
PeriodEnds string `json:"period_ends,omitempty"`
|
|
||||||
TempRangeF string `json:"temp_range_f,omitempty"`
|
|
||||||
MaxPopPercent *int `json:"max_pop_percent,omitempty"`
|
|
||||||
MaxPopTime string `json:"max_pop_time,omitempty"`
|
|
||||||
MaxWindGustMph *int `json:"max_wind_gust_mph,omitempty"`
|
|
||||||
Snow bool `json:"snow,omitempty"`
|
|
||||||
Ice bool `json:"ice,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type precipTimingStanza struct {
|
|
||||||
MaxPopPercent *int `json:"max_pop_percent,omitempty"`
|
|
||||||
MaxPopTime string `json:"max_pop_time,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type alertDigestStanza struct {
|
|
||||||
Relevant []alertSummaryStanza `json:"relevant,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type alertSummaryStanza struct {
|
|
||||||
Event string `json:"event,omitempty"`
|
|
||||||
Headline string `json:"headline,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type indicators struct {
|
|
||||||
Snow bool
|
|
||||||
Ice bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func requiredStanza[T any](snapshot module.Snapshot, name string) (T, error) {
|
|
||||||
value, ok, err := module.StanzaValue[T](snapshot, name)
|
|
||||||
if err != nil {
|
|
||||||
return value, err
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return value, fmt.Errorf("stanza %q is required", name)
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareTemperatureValues(label string, previous *int, current *int, threshold float64) []Change {
|
|
||||||
if previous == nil || current == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !differenceAtLeast(float64(*previous), float64(*current), threshold) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []Change{{
|
|
||||||
Type: "temperature_shift",
|
|
||||||
Message: fmt.Sprintf("%s temperature changed from %d to %d.", label, *previous, *current),
|
|
||||||
Previous: fmt.Sprintf("%d", *previous),
|
|
||||||
Current: fmt.Sprintf("%d", *current),
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func comparePrecipitationValues(previous *int, current *int, threshold int, prefix string) []Change {
|
|
||||||
if previous == nil || current == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
previousCategory := precipitationCategory(float64(*previous))
|
|
||||||
currentCategory := precipitationCategory(float64(*current))
|
|
||||||
if previousCategory == currentCategory && !differenceAtLeast(float64(*previous), float64(*current), float64(threshold)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
changeType := prefix + "precip_probability_change"
|
|
||||||
return []Change{{
|
|
||||||
Type: changeType,
|
|
||||||
Message: fmt.Sprintf("Peak precipitation chance changed from %d%% (%s) to %d%% (%s).", *previous, previousCategory, *current, currentCategory),
|
|
||||||
Previous: fmt.Sprintf("%d%% %s", *previous, previousCategory),
|
|
||||||
Current: fmt.Sprintf("%d%% %s", *current, currentCategory),
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func comparePrecipTiming(previous string, current string, thresholdMinutes int, prefix string) []Change {
|
|
||||||
if thresholdMinutes <= 0 || previous == "" || current == "" || previous == current {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
previousTime, previousOK := parseClock(previous)
|
|
||||||
currentTime, currentOK := parseClock(current)
|
|
||||||
if !previousOK || !currentOK {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if int(math.Abs(currentTime.Sub(previousTime).Minutes())) < thresholdMinutes {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []Change{{
|
|
||||||
Type: prefix + "precip_timing_shift",
|
|
||||||
Message: fmt.Sprintf("Peak precipitation timing shifted from %s to %s.", previous, current),
|
|
||||||
Previous: previous,
|
|
||||||
Current: current,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareWindValues(previous *int, current *int, threshold int, prefix string) []Change {
|
|
||||||
if previous == nil || current == nil || !differenceAtLeast(float64(*previous), float64(*current), float64(threshold)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []Change{{
|
|
||||||
Type: prefix + "wind_gust_change",
|
|
||||||
Message: fmt.Sprintf("Peak wind gust changed from %d mph to %d mph.", *previous, *current),
|
|
||||||
Previous: fmt.Sprintf("%d mph", *previous),
|
|
||||||
Current: fmt.Sprintf("%d mph", *current),
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareAlerts(previous []alertSummaryStanza, current []alertSummaryStanza) []Change {
|
|
||||||
previousSet := alertSet(previous)
|
|
||||||
currentSet := alertSet(current)
|
|
||||||
var changes []Change
|
|
||||||
for event := range currentSet {
|
|
||||||
if _, ok := previousSet[event]; !ok {
|
|
||||||
changes = append(changes, Change{Type: "alert_added", Message: fmt.Sprintf("Alert added: %s.", event), Current: event})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for event := range previousSet {
|
|
||||||
if _, ok := currentSet[event]; !ok {
|
|
||||||
changes = append(changes, Change{Type: "alert_removed", Message: fmt.Sprintf("Alert removed: %s.", event), Previous: event})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sortChanges(changes)
|
|
||||||
return changes
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareIndicators(previous indicators, current indicators, prefix string) []Change {
|
|
||||||
var changes []Change
|
|
||||||
for _, item := range []struct {
|
|
||||||
name string
|
|
||||||
previous bool
|
|
||||||
current bool
|
|
||||||
}{
|
|
||||||
{name: "snow", previous: previous.Snow, current: current.Snow},
|
|
||||||
{name: "ice", previous: previous.Ice, current: current.Ice},
|
|
||||||
} {
|
|
||||||
if item.previous == item.current {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
changeType := prefix + item.name + "_risk_change"
|
|
||||||
if item.current {
|
|
||||||
changes = append(changes, Change{Type: changeType, Message: fmt.Sprintf("%s risk is now present.", item.name), Current: "present"})
|
|
||||||
} else {
|
|
||||||
changes = append(changes, Change{Type: changeType, Message: fmt.Sprintf("%s risk is no longer present.", item.name), Previous: "present"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return changes
|
|
||||||
}
|
|
||||||
|
|
||||||
func aggregateIndicators(dayparts map[string]daypartSummaryStanza) indicators {
|
|
||||||
out := indicators{}
|
|
||||||
for _, daypart := range dayparts {
|
|
||||||
out.Snow = out.Snow || daypart.Snow
|
|
||||||
out.Ice = out.Ice || daypart.Ice
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func alertSet(alerts []alertSummaryStanza) map[string]struct{} {
|
|
||||||
out := map[string]struct{}{}
|
|
||||||
for _, alert := range alerts {
|
|
||||||
event := alert.Event
|
|
||||||
if event == "" {
|
|
||||||
event = alert.Headline
|
|
||||||
}
|
|
||||||
if event != "" {
|
|
||||||
out[event] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func precipitationCategory(value float64) string {
|
|
||||||
switch {
|
|
||||||
case value >= 70:
|
|
||||||
return "high"
|
|
||||||
case value >= 50:
|
|
||||||
return "likely"
|
|
||||||
case value >= 20:
|
|
||||||
return "possible"
|
|
||||||
default:
|
|
||||||
return "low"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func differenceAtLeast(previous float64, current float64, threshold float64) bool {
|
|
||||||
if threshold <= 0 {
|
|
||||||
return previous != current
|
|
||||||
}
|
|
||||||
return math.Abs(current-previous) >= threshold
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortChanges(items []Change) {
|
|
||||||
sort.SliceStable(items, func(i, j int) bool {
|
|
||||||
if items[i].Type == items[j].Type {
|
|
||||||
return items[i].Message < items[j].Message
|
|
||||||
}
|
|
||||||
return items[i].Type < items[j].Type
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseClock(value string) (time.Time, bool) {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
for _, layout := range []string{"3 PM", "3:04 PM", "15:04"} {
|
|
||||||
if parsed, err := time.Parse(layout, value); err == nil {
|
|
||||||
return parsed, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return time.Time{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTempRange(value string) (*int, *int) {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if value == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
parts := strings.Split(value, "-")
|
|
||||||
if len(parts) == 1 {
|
|
||||||
if parsed, err := strconv.Atoi(strings.TrimSpace(parts[0])); err == nil {
|
|
||||||
return &parsed, &parsed
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
minValue, minErr := strconv.Atoi(strings.TrimSpace(parts[0]))
|
|
||||||
maxValue, maxErr := strconv.Atoi(strings.TrimSpace(parts[len(parts)-1]))
|
|
||||||
if minErr != nil || maxErr != nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return &minValue, &maxValue
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
package changes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCompareDailyNoMeaningfulChanges(t *testing.T) {
|
|
||||||
previous := dailySnapshot(t, 60, 70, 30, "8 AM", nil, false)
|
|
||||||
current := dailySnapshot(t, 61, 71, 35, "8:30 AM", nil, false)
|
|
||||||
|
|
||||||
changes, err := CompareDaily(previous, current, testThresholds())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CompareDaily() error = %v", err)
|
|
||||||
}
|
|
||||||
if len(changes) != 0 {
|
|
||||||
t.Fatalf("changes = %#v, want none", changes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareDailyTemperatureThreshold(t *testing.T) {
|
|
||||||
previous := dailySnapshot(t, 50, 70, 10, "8 AM", nil, false)
|
|
||||||
current := dailySnapshot(t, 58, 79, 10, "8 AM", nil, false)
|
|
||||||
|
|
||||||
changes, err := CompareDaily(previous, current, testThresholds())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CompareDaily() error = %v", err)
|
|
||||||
}
|
|
||||||
if countType(changes, "temperature_shift") != 2 {
|
|
||||||
t.Fatalf("changes = %#v, want low and high temperature changes", changes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareDailyPrecipTimingShift(t *testing.T) {
|
|
||||||
previous := dailySnapshot(t, 60, 70, 60, "8 AM", nil, false)
|
|
||||||
current := dailySnapshot(t, 60, 70, 60, "11 AM", nil, false)
|
|
||||||
|
|
||||||
changes, err := CompareDaily(previous, current, testThresholds())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CompareDaily() error = %v", err)
|
|
||||||
}
|
|
||||||
if countType(changes, "precip_timing_shift") != 1 {
|
|
||||||
t.Fatalf("changes = %#v, want timing shift", changes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareDailyAlertAddedAndRemoved(t *testing.T) {
|
|
||||||
previous := dailySnapshot(t, 60, 70, 10, "8 AM", []string{"Wind Advisory"}, false)
|
|
||||||
current := dailySnapshot(t, 60, 70, 10, "8 AM", []string{"Flood Watch"}, false)
|
|
||||||
|
|
||||||
changes, err := CompareDaily(previous, current, testThresholds())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CompareDaily() error = %v", err)
|
|
||||||
}
|
|
||||||
if countType(changes, "alert_added") != 1 || countType(changes, "alert_removed") != 1 {
|
|
||||||
t.Fatalf("changes = %#v, want one alert added and one removed", changes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareDailyIndicatorChange(t *testing.T) {
|
|
||||||
previous := dailySnapshot(t, 60, 70, 10, "8 AM", nil, false)
|
|
||||||
current := dailySnapshot(t, 60, 70, 10, "8 AM", nil, true)
|
|
||||||
|
|
||||||
changes, err := CompareDaily(previous, current, testThresholds())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CompareDaily() error = %v", err)
|
|
||||||
}
|
|
||||||
if countType(changes, "snow_risk_change") != 1 {
|
|
||||||
t.Fatalf("changes = %#v, want snow risk change", changes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareDailyRequiresComparisonStanzas(t *testing.T) {
|
|
||||||
_, err := CompareDaily(snapshot(t), dailySnapshot(t, 60, 70, 10, "8 AM", nil, false), testThresholds())
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("CompareDaily() error = nil, want missing stanza error")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "derived_daily_summary") {
|
|
||||||
t.Fatalf("error = %q, want derived_daily_summary context", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dailySnapshot(t *testing.T, low int, high int, precip int, precipTime string, alerts []string, snow bool) module.Snapshot {
|
|
||||||
t.Helper()
|
|
||||||
relevant := make([]alertSummaryStanza, 0, len(alerts))
|
|
||||||
for _, alert := range alerts {
|
|
||||||
relevant = append(relevant, alertSummaryStanza{Event: alert})
|
|
||||||
}
|
|
||||||
return snapshot(t,
|
|
||||||
module.Output{ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", Value: dailySummaryStanza{
|
|
||||||
Date: "2026-05-29",
|
|
||||||
HighTempF: &high,
|
|
||||||
LowTempF: &low,
|
|
||||||
DailyPrecipitationProbability: &precip,
|
|
||||||
}},
|
|
||||||
module.Output{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]daypartSummaryStanza{
|
|
||||||
"morning": {
|
|
||||||
Date: "2026-05-29",
|
|
||||||
PeriodBegins: "2026-05-29 at 6:00 AM",
|
|
||||||
PeriodEnds: "2026-05-29 at 10:00 AM",
|
|
||||||
TempRangeF: "60-70",
|
|
||||||
Snow: snow,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
module.Output{ID: module.PrecipTiming, StanzaName: "precip_timing", Value: precipTimingStanza{MaxPopPercent: &precip, MaxPopTime: precipTime}},
|
|
||||||
module.Output{ID: module.AlertDigest, StanzaName: "alert_digest", Value: alertDigestStanza{Relevant: relevant}},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func snapshot(t *testing.T, outputs ...module.Output) module.Snapshot {
|
|
||||||
t.Helper()
|
|
||||||
snapshot, err := module.NewSnapshot(outputs)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewSnapshot() error = %v", err)
|
|
||||||
}
|
|
||||||
return snapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
func testThresholds() Thresholds {
|
|
||||||
return Thresholds{
|
|
||||||
TemperatureDegrees: 5,
|
|
||||||
PrecipProbabilityPoints: 20,
|
|
||||||
WindGustMilesPerHour: 10,
|
|
||||||
PrecipTimingShiftMinutes: 120,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func countType(changes []Change, changeType string) int {
|
|
||||||
var count int
|
|
||||||
for _, change := range changes {
|
|
||||||
if change.Type == changeType {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,6 @@ type Config struct {
|
|||||||
Promptkit PromptkitConfig `yaml:"promptkit"`
|
Promptkit PromptkitConfig `yaml:"promptkit"`
|
||||||
Workspace WorkspaceConfig `yaml:"workspace"`
|
Workspace WorkspaceConfig `yaml:"workspace"`
|
||||||
Dayparts []DaypartConfig `yaml:"dayparts"`
|
Dayparts []DaypartConfig `yaml:"dayparts"`
|
||||||
RecentChange RecentChangeConfig `yaml:"recent_change"`
|
|
||||||
Reports map[string]ReportConfig `yaml:"reports"`
|
Reports map[string]ReportConfig `yaml:"reports"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,13 +108,6 @@ type DaypartConfig struct {
|
|||||||
End string `yaml:"end"`
|
End string `yaml:"end"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecentChangeConfig struct {
|
|
||||||
TemperatureDegrees float64 `yaml:"temperature_degrees"`
|
|
||||||
PrecipProbabilityPoints int `yaml:"precip_probability_points"`
|
|
||||||
WindGustMilesPerHour int `yaml:"wind_gust_miles_per_hour"`
|
|
||||||
PrecipTimingShiftMinutes int `yaml:"precip_timing_shift_minutes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReportConfig struct {
|
type ReportConfig struct {
|
||||||
DeterministicModules []ModuleConfigItem `yaml:"deterministic_modules"`
|
DeterministicModules []ModuleConfigItem `yaml:"deterministic_modules"`
|
||||||
Distributor ReportDistributorConfig `yaml:"distributor"`
|
Distributor ReportDistributorConfig `yaml:"distributor"`
|
||||||
|
|||||||
@@ -165,6 +165,13 @@ func TestLoadRejectsRetiredExecutionConfiguration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadRejectsRemovedRecentChangeConfiguration(t *testing.T) {
|
||||||
|
_, err := LoadFile(writeConfig(t, "recent_change:\n temperature_degrees: 5\n"))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "recent_change") {
|
||||||
|
t.Fatalf("LoadFile() error = %v, want removed configuration rejection", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadReportModuleOverrides(t *testing.T) {
|
func TestLoadReportModuleOverrides(t *testing.T) {
|
||||||
path := writeConfig(t, `
|
path := writeConfig(t, `
|
||||||
reports:
|
reports:
|
||||||
|
|||||||
@@ -64,12 +64,6 @@ func Defaults() Config {
|
|||||||
{Name: "afternoon", Start: "15:00", End: "17:00"},
|
{Name: "afternoon", Start: "15:00", End: "17:00"},
|
||||||
{Name: "evening", Start: "17:00", End: "24:00"},
|
{Name: "evening", Start: "17:00", End: "24:00"},
|
||||||
},
|
},
|
||||||
RecentChange: RecentChangeConfig{
|
|
||||||
TemperatureDegrees: 5,
|
|
||||||
PrecipProbabilityPoints: 20,
|
|
||||||
WindGustMilesPerHour: 10,
|
|
||||||
PrecipTimingShiftMinutes: 120,
|
|
||||||
},
|
|
||||||
Reports: map[string]ReportConfig{},
|
Reports: map[string]ReportConfig{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -62,7 +63,9 @@ func mergeFile(cfg *Config, path string) error {
|
|||||||
if err := rejectRetiredExecutionConfig(data); err != nil {
|
if err := rejectRetiredExecutionConfig(data); err != nil {
|
||||||
return fmt.Errorf("parse config %q: %w", path, err)
|
return fmt.Errorf("parse config %q: %w", path, err)
|
||||||
}
|
}
|
||||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
decoder := yaml.NewDecoder(bytes.NewReader(data))
|
||||||
|
decoder.KnownFields(true)
|
||||||
|
if err := decoder.Decode(cfg); err != nil {
|
||||||
return fmt.Errorf("parse config %q: %w", path, err)
|
return fmt.Errorf("parse config %q: %w", path, err)
|
||||||
}
|
}
|
||||||
if cfg.MissingSource.Sources == nil {
|
if cfg.MissingSource.Sources == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user