Add current conditions condition code consensus helper
This commit is contained in:
107
internal/adapters/outbound/postgres/conditions_codes.go
Normal file
107
internal/adapters/outbound/postgres/conditions_codes.go
Normal 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
|
||||||
|
}
|
||||||
116
internal/adapters/outbound/postgres/conditions_codes_test.go
Normal file
116
internal/adapters/outbound/postgres/conditions_codes_test.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user