feat: filter award references by mode class
All / CW / Phone / Digital, stacked on top of the worked/confirmed filter. The
question an operator actually has — which DXCC entities have I worked on CW but
not confirmed on CW — needs both at once; answering only one of them is what
sends people to a spreadsheet.
References carried bands but not modes, so the award computation now aggregates
mode CLASSES per reference, worked and confirmed separately. Classes, not ADIF
modes: nobody chasing CW cares whether the digital side was FT8 or RTTY, and a
list of twenty mode names would not answer the question. An unrecognised mode
counts as data rather than as nothing, so a new digital mode does not vanish
from the filter the day it appears.
Two rules the filter needs to be honest:
- Not-confirmed means not confirmed ON THIS MODE. An entity worked on CW and
confirmed on SSB is still a CW entity to chase, and that is exactly what the
filter is for.
- "Not worked" plus a mode is a contradiction — an unworked reference has no
mode — so the mode filter stands aside there instead of emptying the list.
This commit is contained in:
+48
-8
@@ -334,13 +334,19 @@ type BandCount struct {
|
||||
|
||||
// Ref is one reference's status within an award.
|
||||
type Ref struct {
|
||||
Ref string `json:"ref"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
SubGrp string `json:"subgrp,omitempty"`
|
||||
Worked bool `json:"worked"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Validated bool `json:"validated"`
|
||||
Ref string `json:"ref"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
SubGrp string `json:"subgrp,omitempty"`
|
||||
Worked bool `json:"worked"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Validated bool `json:"validated"`
|
||||
// Modes / ConfirmedModes are CLASSES, not ADIF modes: "CW", "PHONE",
|
||||
// "DIGI". An operator asking "which entities have I worked on CW but not
|
||||
// confirmed" does not care whether it was FT8 or RTTY on the digital side,
|
||||
// and a list of twenty mode names would not answer the question they asked.
|
||||
Modes []string `json:"modes"`
|
||||
ConfirmedModes []string `json:"confirmed_modes"`
|
||||
Bands []string `json:"bands"`
|
||||
ConfirmedBands []string `json:"confirmed_bands"`
|
||||
ValidatedBands []string `json:"validated_bands"`
|
||||
@@ -360,11 +366,33 @@ type Result struct {
|
||||
Error string `json:"error,omitempty"` // e.g. bad regexp pattern
|
||||
}
|
||||
|
||||
// ModeClass sorts an ADIF mode into the three classes an operator thinks in:
|
||||
// CW, PHONE, DIGI. Anything unrecognised returns "" and is simply not counted
|
||||
// under any class — better than inventing one, since these classes drive a
|
||||
// filter that decides what the operator is shown.
|
||||
func ModeClass(mode string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(mode)) {
|
||||
case "CW", "CWR":
|
||||
return "CW"
|
||||
case "SSB", "USB", "LSB", "AM", "FM", "DV", "PHONE", "DIGITALVOICE":
|
||||
return "PHONE"
|
||||
case "":
|
||||
return ""
|
||||
}
|
||||
// Everything else that a logbook actually carries is a data mode: FT8, FT4,
|
||||
// RTTY, PSK31, JS8, Q65, MSK144, OLIVIA, VARA… Listing them exhaustively
|
||||
// would mean a new mode silently vanishing from the filter the day it
|
||||
// appears, which is worse than treating an unknown data mode as data.
|
||||
return "DIGI"
|
||||
}
|
||||
|
||||
// NameResolver optionally maps a (field, ref) pair to a human name. May be nil.
|
||||
type NameResolver func(field, ref string) string
|
||||
|
||||
type refAgg struct {
|
||||
bands map[string]struct{}
|
||||
modes map[string]struct{}
|
||||
confirmedModes map[string]struct{}
|
||||
confirmedBands map[string]struct{}
|
||||
validatedBands map[string]struct{}
|
||||
anyConfirmed bool
|
||||
@@ -481,22 +509,33 @@ func Compute(defs []Def, qsos []qso.QSO, refMetas map[string][]RefMeta, nameOf N
|
||||
continue
|
||||
}
|
||||
band := strings.ToLower(strings.TrimSpace(q.Band))
|
||||
modeClass := ModeClass(q.Mode)
|
||||
isConf := confirmed(q, d.Confirm)
|
||||
isVal := confirmed(q, d.Validate)
|
||||
for _, ref := range refs {
|
||||
a := agg[i][ref]
|
||||
if a == nil {
|
||||
a = &refAgg{bands: map[string]struct{}{}, confirmedBands: map[string]struct{}{}, validatedBands: map[string]struct{}{}}
|
||||
a = &refAgg{
|
||||
bands: map[string]struct{}{}, confirmedBands: map[string]struct{}{},
|
||||
validatedBands: map[string]struct{}{},
|
||||
modes: map[string]struct{}{}, confirmedModes: map[string]struct{}{},
|
||||
}
|
||||
agg[i][ref] = a
|
||||
}
|
||||
if band != "" {
|
||||
a.bands[band] = struct{}{}
|
||||
}
|
||||
if modeClass != "" {
|
||||
a.modes[modeClass] = struct{}{}
|
||||
}
|
||||
if isConf {
|
||||
a.anyConfirmed = true
|
||||
if band != "" {
|
||||
a.confirmedBands[band] = struct{}{}
|
||||
}
|
||||
if modeClass != "" {
|
||||
a.confirmedModes[modeClass] = struct{}{}
|
||||
}
|
||||
}
|
||||
if isVal {
|
||||
a.anyValidated = true
|
||||
@@ -525,6 +564,7 @@ func Compute(defs []Def, qsos []qso.QSO, refMetas map[string][]RefMeta, nameOf N
|
||||
r.Validated++
|
||||
}
|
||||
rf := Ref{Ref: ref, Worked: true, Confirmed: a.anyConfirmed, Validated: a.anyValidated,
|
||||
Modes: setToSorted(a.modes), ConfirmedModes: setToSorted(a.confirmedModes),
|
||||
Bands: setToSorted(a.bands), ConfirmedBands: setToSorted(a.confirmedBands), ValidatedBands: setToSorted(a.validatedBands)}
|
||||
labelRef(&rf, d, ref, rl, hasList, nameOf)
|
||||
r.Refs = append(r.Refs, rf)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package award
|
||||
|
||||
import "testing"
|
||||
|
||||
// The three classes an operator thinks in — the ones the awards filter offers.
|
||||
//
|
||||
// "Which entities have I worked on CW but not confirmed" is the question this
|
||||
// serves, so the classes must match how the question is asked: CW, phone, data.
|
||||
// Which data mode it was does not enter into it.
|
||||
func TestModeClass(t *testing.T) {
|
||||
for _, c := range []struct{ mode, want string }{
|
||||
{"CW", "CW"}, {"cw", "CW"}, {" CW ", "CW"}, {"CWR", "CW"},
|
||||
{"SSB", "PHONE"}, {"USB", "PHONE"}, {"LSB", "PHONE"},
|
||||
{"AM", "PHONE"}, {"FM", "PHONE"}, {"DV", "PHONE"},
|
||||
{"FT8", "DIGI"}, {"FT4", "DIGI"}, {"RTTY", "DIGI"}, {"PSK31", "DIGI"},
|
||||
{"JS8", "DIGI"}, {"Q65", "DIGI"}, {"OLIVIA", "DIGI"},
|
||||
// A mode nobody has heard of yet is data, not nothing: the alternative
|
||||
// is a new digital mode silently vanishing from the filter the day it
|
||||
// appears.
|
||||
{"SOMETHINGNEW", "DIGI"},
|
||||
// No mode at all is not a class, and must not be counted as one.
|
||||
{"", ""}, {" ", ""},
|
||||
} {
|
||||
if got := ModeClass(c.mode); got != c.want {
|
||||
t.Errorf("ModeClass(%q) = %q, want %q", c.mode, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user