fix(stats): confirmed outranks worked in the band/mode matrix
Reported by VK4DX with the case that names itself: YB confirmed on 20m digital, painted blue, because one unconfirmed YB station had also been worked on that slot. The ladder ran call_c > call_w > dxcc_c > dxcc_w, so a callsign worked and not confirmed beat an entity CONFIRMED on the same band and mode. The grid answers 'what do I still need here', and a confirmed entity needs nothing whoever was worked afterwards — so the order is now call_c > dxcc_c > call_w > dxcc_w. Two things made it easy to get wrong, and both are fixed rather than merely corrected. The code was a run of assignments where the later test silently overwrote the earlier, so 'call worked' erased 'entity confirmed'; it now takes the maximum. And the rule lived inside a scan loop where nothing could reach it, so it is lifted into bandStatusCode with a table test — including this exact case, and one that fails if anybody reorders the constants.
This commit is contained in:
+58
-19
@@ -1895,7 +1895,8 @@ type WorkedBefore struct {
|
||||
|
||||
// Status grid driving the band×class matrix in the UI. One entry per
|
||||
// (band, class) where ANY QSO exists in this DXCC. Only the highest
|
||||
// status for that cell is kept (call_c > call_w > dxcc_c > dxcc_w).
|
||||
// status for that cell is kept (call_c > dxcc_c > call_w > dxcc_w —
|
||||
// confirmed outranks worked).
|
||||
BandStatus []BandStatus `json:"band_status"`
|
||||
}
|
||||
|
||||
@@ -1906,6 +1907,48 @@ type BandStatus struct {
|
||||
Status string `json:"status"` // "call_c" | "call_w" | "dxcc_c" | "dxcc_w"
|
||||
}
|
||||
|
||||
// Band-status codes, lowest first. The ORDER is the rule: a cell shows the
|
||||
// highest that applies.
|
||||
const (
|
||||
stDxccW = iota // the entity was worked on this slot
|
||||
stCallW // …and this callsign was one of them
|
||||
stDxccC // the entity is CONFIRMED here
|
||||
stCallC // …and by this callsign
|
||||
)
|
||||
|
||||
// bandStatusNames maps those codes to what the UI colours by.
|
||||
var bandStatusNames = [...]string{"dxcc_w", "call_w", "dxcc_c", "call_c"}
|
||||
|
||||
// bandStatusCode picks the status of one cell of the band × mode matrix.
|
||||
//
|
||||
// CONFIRMED OUTRANKS WORKED, and that is the whole of it. The ladder used to
|
||||
// run call_c > call_w > dxcc_c > dxcc_w, so a callsign worked and not confirmed
|
||||
// beat an entity confirmed on the same slot: an operator with YB confirmed on
|
||||
// 20m digital saw that cell as "worked, not confirmed" because he had also
|
||||
// worked one unconfirmed YB station there. The grid answers "what do I still
|
||||
// need", and a confirmed entity needs nothing, whoever was worked afterwards.
|
||||
//
|
||||
// Taken as a MAXIMUM rather than as a run of assignments, which is how the
|
||||
// later test came to overwrite the earlier one in the first place.
|
||||
func bandStatusCode(callWorked, callConfirmed, entityConfirmed bool) int {
|
||||
code := stDxccW // there is a row at all ⇒ the entity was worked here
|
||||
raise := func(c int) {
|
||||
if c > code {
|
||||
code = c
|
||||
}
|
||||
}
|
||||
if callWorked {
|
||||
raise(stCallW)
|
||||
}
|
||||
if entityConfirmed {
|
||||
raise(stDxccC)
|
||||
}
|
||||
if callConfirmed {
|
||||
raise(stCallC)
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// modeClass collapses ADIF modes into the three buckets DXers care about.
|
||||
// Anything not voice and not CW is treated as digital.
|
||||
func modeClass(mode string) string {
|
||||
@@ -2167,7 +2210,18 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int,
|
||||
// ---- Per-(band, class) status grid ----
|
||||
// One pass over every distinct (band, mode) in the DXCC, aggregating
|
||||
// "did this call work it?" and "was anything confirmed?" via MAX.
|
||||
// Status precedence: call_c > call_w > dxcc_c > dxcc_w.
|
||||
// Status precedence: CONFIRMED OUTRANKS WORKED — call_c > dxcc_c > call_w >
|
||||
// dxcc_w.
|
||||
//
|
||||
// It used to run call_c > call_w > dxcc_c > dxcc_w, which made a call worked
|
||||
// and not confirmed outrank an entity confirmed on the same slot. Reported
|
||||
// from a real log: YB confirmed on 20m digital showed BLUE, because that
|
||||
// operator had also worked one unconfirmed YB station there. The cell said
|
||||
// "not confirmed" about a slot that is confirmed.
|
||||
//
|
||||
// The grid answers "what do I still need on this band and mode", and for
|
||||
// that question confirmation is the axis that matters: a confirmed entity
|
||||
// needs nothing, whoever else was worked afterwards.
|
||||
// Filter NULL/empty band+mode rows — they'd create a NULL group key
|
||||
// that Scan into *string can't handle and would error out the whole
|
||||
// WorkedBefore call, blanking the matrix in the UI.
|
||||
@@ -2188,12 +2242,6 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int,
|
||||
return wb, fmt.Errorf("band status: %w", err)
|
||||
}
|
||||
type cellKey struct{ band, class string }
|
||||
const (
|
||||
stDxccW = 0
|
||||
stDxccC = 1
|
||||
stCallW = 2
|
||||
stCallC = 3
|
||||
)
|
||||
best := map[cellKey]int{}
|
||||
for statusRows.Next() {
|
||||
var band, mode string
|
||||
@@ -2202,23 +2250,14 @@ func (r *Repo) WorkedBefore(ctx context.Context, callsign string, dxccHint int,
|
||||
statusRows.Close()
|
||||
return wb, fmt.Errorf("scan band status: %w", err)
|
||||
}
|
||||
code := stDxccW // row exists ⇒ entity worked at minimum
|
||||
if dxccConfirmed == 1 {
|
||||
code = stDxccC
|
||||
}
|
||||
if callW == 1 {
|
||||
code = stCallW
|
||||
}
|
||||
if callC == 1 {
|
||||
code = stCallC
|
||||
}
|
||||
code := bandStatusCode(callW == 1, callC == 1, dxccConfirmed == 1)
|
||||
k := cellKey{band: band, class: modeClass(mode)}
|
||||
if cur, ok := best[k]; !ok || code > cur {
|
||||
best[k] = code
|
||||
}
|
||||
}
|
||||
statusRows.Close()
|
||||
codeStr := [...]string{"dxcc_w", "dxcc_c", "call_w", "call_c"}
|
||||
codeStr := bandStatusNames
|
||||
for k, code := range best {
|
||||
wb.BandStatus = append(wb.BandStatus, BandStatus{
|
||||
Band: k.band, Class: k.class, Status: codeStr[code],
|
||||
|
||||
Reference in New Issue
Block a user