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