diff --git a/internal/briefing/derived_daypart_summaries_module.go b/internal/briefing/derived_daypart_summaries_module.go index 3e3223d..ef27344 100644 --- a/internal/briefing/derived_daypart_summaries_module.go +++ b/internal/briefing/derived_daypart_summaries_module.go @@ -400,7 +400,7 @@ func multipleSummaryDates(summaries []forecast.DailySummary) bool { } func daypartKey(daypart forecast.DaypartSummary, prefixDate bool) string { - key := normalizedKey(daypart.Name) + key := forecast.CanonicalDaypartKey(daypart.Name) if key == "" { key = "unnamed" } @@ -409,21 +409,3 @@ func daypartKey(daypart forecast.DaypartSummary, prefixDate bool) string { } return daypart.Period.Start.Format(timeutil.DateLayout) + "_" + key } - -func normalizedKey(value string) string { - lower := strings.ToLower(strings.TrimSpace(value)) - var out strings.Builder - lastUnderscore := false - for _, r := range lower { - if unicode.IsLetter(r) || unicode.IsDigit(r) { - out.WriteRune(r) - lastUnderscore = false - continue - } - if !lastUnderscore { - out.WriteByte('_') - lastUnderscore = true - } - } - return strings.Trim(out.String(), "_") -} diff --git a/internal/forecast/dayparts.go b/internal/forecast/dayparts.go index f9f981c..b017619 100644 --- a/internal/forecast/dayparts.go +++ b/internal/forecast/dayparts.go @@ -2,7 +2,9 @@ package forecast import ( "fmt" + "strings" "time" + "unicode" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" @@ -14,6 +16,25 @@ type DaypartDefinition struct { End string `json:"end"` } +// CanonicalDaypartKey returns the stable identity for a configured daypart name. +func CanonicalDaypartKey(value string) string { + lower := strings.ToLower(strings.TrimSpace(value)) + var out strings.Builder + lastUnderscore := false + for _, r := range lower { + if unicode.IsLetter(r) || unicode.IsDigit(r) { + out.WriteRune(r) + lastUnderscore = false + continue + } + if !lastUnderscore { + out.WriteByte('_') + lastUnderscore = true + } + } + return strings.Trim(out.String(), "_") +} + type DaypartWindow struct { Name string `json:"name"` Start time.Time `json:"start"` diff --git a/internal/forecast/derive_test.go b/internal/forecast/derive_test.go index 433eb80..88519f4 100644 --- a/internal/forecast/derive_test.go +++ b/internal/forecast/derive_test.go @@ -658,3 +658,26 @@ func assertTimedValueClose(t *testing.T, name string, got *TimedValue, want floa t.Fatalf("%s = %#v, want %v", name, got, want) } } + +func TestCanonicalDaypartKey(t *testing.T) { + for _, tt := range []struct { + name string + value string + want string + }{ + {name: "blank", value: " \t ", want: ""}, + {name: "casing", value: "MORNING", want: "morning"}, + {name: "punctuation runs", value: " Morning--Commute / School ", want: "morning_commute_school"}, + {name: "Unicode", value: "Déjà Vu", want: "déjà_vu"}, + {name: "letters and digits", value: "Day 1 Outlook", want: "day_1_outlook"}, + } { + t.Run(tt.name, func(t *testing.T) { + if got := CanonicalDaypartKey(tt.value); got != tt.want { + t.Fatalf("CanonicalDaypartKey(%q) = %q, want %q", tt.value, got, tt.want) + } + }) + } + if CanonicalDaypartKey("Morning commute") != CanonicalDaypartKey("MORNING--COMMUTE") { + t.Fatal("equivalent configured names must have the same canonical daypart key") + } +} diff --git a/internal/generatedtext/render_context.go b/internal/generatedtext/render_context.go index c56d9c3..b9baa49 100644 --- a/internal/generatedtext/render_context.go +++ b/internal/generatedtext/render_context.go @@ -3,9 +3,7 @@ package generatedtext import ( "fmt" "sort" - "strings" "time" - "unicode" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/facts" @@ -698,7 +696,7 @@ func orderedDaypartRows(dayparts *map[string]briefing.DerivedDaypartSummaryModul } func daypartModuleKeyCandidates(daypart forecast.DaypartSummary) []string { - key := normalizedDaypartKey(daypart.Name) + key := forecast.CanonicalDaypartKey(daypart.Name) if key == "" { key = "unnamed" } @@ -708,24 +706,6 @@ func daypartModuleKeyCandidates(daypart forecast.DaypartSummary) []string { return []string{key, daypart.Period.Start.Format(timeutil.DateLayout) + "_" + key} } -func normalizedDaypartKey(value string) string { - lower := strings.ToLower(strings.TrimSpace(value)) - var out strings.Builder - lastUnderscore := false - for _, r := range lower { - if unicode.IsLetter(r) || unicode.IsDigit(r) { - out.WriteRune(r) - lastUnderscore = false - continue - } - if !lastUnderscore { - out.WriteByte('_') - lastUnderscore = true - } - } - return strings.Trim(out.String(), "_") -} - func locationName(metadata briefing.Metadata) string { if metadata.Location != nil { if metadata.Location.Name != "" && metadata.Location.Region != "" {