This commit is contained in:
2026-06-06 14:16:30 +02:00
parent f91f9ff3b8
commit 17f7a00bd7
19 changed files with 1278 additions and 91 deletions
+32
View File
@@ -210,8 +210,13 @@ type refList struct {
byCode map[string]RefMeta // uppercased code → metadata
codes []string // codes in input order (for stable unworked listing)
withPattern []string // codes whose reference declares a regex (usually none)
names []nameCode // (uppercased name → code) for MatchBy="description"
}
// nameCode pairs a reference's uppercased description with its code, for
// description-based matching (e.g. WAJA finding a prefecture NAME in the QTH).
type nameCode struct{ name, code string }
// RefMeta is one reference's metadata for the engine: enough to enforce a
// predefined list, per-reference DXCC scoping, a per-reference pattern, and to
// label results.
@@ -243,6 +248,9 @@ func NewRefList(metas []RefMeta) refList {
m.Code = code
if _, dup := rl.byCode[code]; !dup {
rl.codes = append(rl.codes, code)
if nm := strings.ToUpper(strings.TrimSpace(m.Name)); nm != "" {
rl.names = append(rl.names, nameCode{name: nm, code: code})
}
}
rl.byCode[code] = m
}
@@ -376,6 +384,15 @@ func Compute(defs []Def, qsos []qso.QSO, refMetas map[string][]RefMeta, nameOf N
for _, b := range sortedBands(bandWorked) {
r.Bands = append(r.Bands, BandCount{Band: b, Worked: bandWorked[b], Confirmed: bandConfirmed[b]})
}
// Never return nil slices: they marshal to JSON null, and the UI calls
// .filter/.length on them (an award with nothing worked yet — e.g. a
// freshly-created WWFF/WAJA — would otherwise white-screen the panel).
if r.Refs == nil {
r.Refs = []Ref{}
}
if r.Bands == nil {
r.Bands = []BandCount{}
}
out[i] = r
}
return out
@@ -428,12 +445,27 @@ func candidates(d *Def, re *regexp.Regexp, q *qso.QSO, rl refList, hasList bool)
return nil
}
predefined := hasList && !d.Dynamic
byDesc := predefined && strings.EqualFold(strings.TrimSpace(d.MatchBy), "description")
var found []string
switch {
case re != nil:
// Award-level regex: capture group 1 (or whole match) for each hit.
found = regexTokens(re, raw)
case byDesc:
// Match references by their DESCRIPTION/name appearing in the field
// (e.g. WAJA finds the prefecture name inside the QTH). ExactMatch means
// the field equals the name; otherwise the name is a substring of it.
up := strings.ToUpper(raw)
for _, nc := range rl.names {
if d.ExactMatch {
if up == nc.name {
found = append(found, nc.code)
}
} else if strings.Contains(up, nc.name) {
found = append(found, nc.code)
}
}
case predefined && !d.ExactMatch:
// "Search reference inside the field": look up each token of the field in
// the list — O(tokens), not O(all references) — plus test the few
+32
View File
@@ -96,6 +96,38 @@ func TestComputeMultiRef(t *testing.T) {
}
}
// WAJA-style award: MatchBy="description", non-exact, scanning the QTH for a
// reference's NAME (the prefecture). Also guards against the nil-slice crash:
// an award with nothing worked must return empty (non-nil) Refs/Bands.
func TestComputeMatchByDescription(t *testing.T) {
def := Def{Code: "WAJA", Type: TypeQSOFields, Field: "qth", MatchBy: "description",
DXCCFilter: []int{339}, Confirm: []string{"lotw", "qsl"}, Valid: true}
qsos := []qso.QSO{
{Callsign: "JA1ABC", Band: "20m", DXCC: ip(339), QTH: "Tokyo city", LOTWRcvd: "Y"},
{Callsign: "JA3DEF", Band: "40m", DXCC: ip(339), QTH: "Osaka"},
{Callsign: "JA9XYZ", Band: "20m", DXCC: ip(339), QTH: "nowhere special"}, // no prefecture name
}
refMetas := map[string][]RefMeta{"WAJA": {
{Code: "100", Name: "Tokyo", Valid: true},
{Code: "270", Name: "Osaka", Valid: true},
{Code: "010", Name: "Hokkaido", Valid: true},
}}
r := Compute([]Def{def}, qsos, refMetas, nil)[0]
if r.Worked != 2 { // Tokyo + Osaka found by name inside QTH
t.Errorf("WAJA worked = %d, want 2 (%v)", r.Worked, refCodes(r))
}
if r.Total != 3 { // predefined denominator = list size
t.Errorf("WAJA total = %d, want 3", r.Total)
}
// Nil-slice guard: an award with zero worked refs must still return
// non-nil (empty) Refs/Bands so the JSON isn't null (UI white-screen).
empty := Compute([]Def{{Code: "WWFF", Type: TypeReference, Field: "wwff", Dynamic: true, Valid: true}}, nil, nil, nil)[0]
if empty.Refs == nil || empty.Bands == nil {
t.Errorf("empty award must have non-nil Refs/Bands, got Refs=%v Bands=%v", empty.Refs, empty.Bands)
}
}
func refCodes(r Result) []string {
out := make([]string, 0, len(r.Refs))
for _, rf := range r.Refs {