Add current conditions condition code consensus helper

This commit is contained in:
2026-06-29 11:56:20 +00:00
parent cfe6748330
commit 3c7ba89e15
2 changed files with 223 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
// conditions_codes.go selects current-conditions WMO codes from source candidates.
// Layer: adapters/outbound/postgres conditions feature.
package postgres
import "gitea.maximumdirect.net/ejr/weatherfeeder/model"
type currentConditionsConditionCodeCandidate struct {
EventSource string
ConditionCode model.WMOCode
}
type conditionCodeFamily int
const (
conditionCodeFamilyUnknown conditionCodeFamily = iota
conditionCodeFamilyClearOrCloud
conditionCodeFamilyFog
conditionCodeFamilyDrizzle
conditionCodeFamilyRain
conditionCodeFamilySnow
conditionCodeFamilyThunderstorm
)
var currentConditionsConditionCodeRankings = map[conditionCodeFamily][]model.WMOCode{
conditionCodeFamilyClearOrCloud: {0, 1, 2, 3},
conditionCodeFamilyFog: {45, 48},
conditionCodeFamilyDrizzle: {51, 53, 55, 56, 57},
conditionCodeFamilyRain: {61, 63, 65, 80, 81, 82, 66, 67},
conditionCodeFamilySnow: {71, 73, 75, 85, 86, 77},
conditionCodeFamilyThunderstorm: {95, 96, 99},
}
var currentConditionsConditionCodeFamilies = buildCurrentConditionsConditionCodeFamilies()
func buildCurrentConditionsConditionCodeFamilies() map[model.WMOCode]conditionCodeFamily {
families := make(map[model.WMOCode]conditionCodeFamily)
for family, ranking := range currentConditionsConditionCodeRankings {
for _, code := range ranking {
families[code] = family
}
}
return families
}
func selectCurrentConditionsConditionCode(candidates []currentConditionsConditionCodeCandidate) model.WMOCode {
seenSources := make(map[string]struct{})
familyCounts := make(map[conditionCodeFamily]int)
codeCounts := make(map[model.WMOCode]int)
for _, candidate := range candidates {
if _, seen := seenSources[candidate.EventSource]; seen {
continue
}
seenSources[candidate.EventSource] = struct{}{}
family, ok := currentConditionsConditionCodeFamilies[candidate.ConditionCode]
if !ok {
continue
}
familyCounts[family]++
codeCounts[candidate.ConditionCode]++
}
winningFamily, ok := currentConditionsWinningConditionCodeFamily(familyCounts)
if !ok {
return model.WMOUnknown
}
return currentConditionsWinningConditionCode(winningFamily, codeCounts)
}
func currentConditionsWinningConditionCodeFamily(counts map[conditionCodeFamily]int) (conditionCodeFamily, bool) {
winningFamily := conditionCodeFamilyUnknown
winningCount := 0
tied := false
for family, count := range counts {
if count > winningCount {
winningFamily = family
winningCount = count
tied = false
continue
}
if count == winningCount {
tied = true
}
}
if winningCount == 0 || tied {
return conditionCodeFamilyUnknown, false
}
return winningFamily, true
}
func currentConditionsWinningConditionCode(family conditionCodeFamily, counts map[model.WMOCode]int) model.WMOCode {
winningCode := model.WMOUnknown
winningCount := 0
for _, code := range currentConditionsConditionCodeRankings[family] {
if counts[code] > winningCount {
winningCode = code
winningCount = counts[code]
}
}
return winningCode
}

View File

@@ -0,0 +1,116 @@
// conditions_codes_test.go tests current-conditions WMO code selection.
// Layer: adapters/outbound/postgres conditions feature.
package postgres
import (
"testing"
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
)
func TestSelectCurrentConditionsConditionCode(t *testing.T) {
tests := []struct {
name string
candidates []currentConditionsConditionCodeCandidate
want model.WMOCode
}{
{
name: "clear cloud ranking breaks exact tie",
candidates: conditionCodeCandidates(
0,
1,
2,
),
want: 0,
},
{
name: "clear cloud family wins over thunderstorm",
candidates: conditionCodeCandidates(
1,
2,
95,
),
want: 1,
},
{
name: "tied families return unknown",
candidates: conditionCodeCandidates(
0,
95,
),
want: model.WMOUnknown,
},
{
name: "rain ranking breaks exact tie",
candidates: conditionCodeCandidates(
61,
63,
80,
),
want: 61,
},
{
name: "three tied families return unknown",
candidates: conditionCodeCandidates(
61,
95,
0,
),
want: model.WMOUnknown,
},
{
name: "single thunderstorm code wins",
candidates: conditionCodeCandidates(
95,
),
want: 95,
},
{
name: "unrecognized only returns unknown",
candidates: conditionCodeCandidates(
4,
100,
),
want: model.WMOUnknown,
},
{
name: "duplicate source only votes once",
candidates: []currentConditionsConditionCodeCandidate{
{EventSource: "source-1", ConditionCode: 95},
{EventSource: "source-1", ConditionCode: 95},
{EventSource: "source-2", ConditionCode: 0},
{EventSource: "source-3", ConditionCode: 0},
},
want: 0,
},
{
name: "exact code frequency wins before ranking",
candidates: []currentConditionsConditionCodeCandidate{
{EventSource: "source-1", ConditionCode: 61},
{EventSource: "source-2", ConditionCode: 63},
{EventSource: "source-3", ConditionCode: 63},
},
want: 63,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := selectCurrentConditionsConditionCode(tt.candidates)
if got != tt.want {
t.Fatalf("expected condition code %d, got %d", tt.want, got)
}
})
}
}
func conditionCodeCandidates(codes ...model.WMOCode) []currentConditionsConditionCodeCandidate {
candidates := make([]currentConditionsConditionCodeCandidate, 0, len(codes))
for i, code := range codes {
candidates = append(candidates, currentConditionsConditionCodeCandidate{
EventSource: string(rune('a' + i)),
ConditionCode: code,
})
}
return candidates
}