fix(awards): apply the reference Prefix where it can actually help
Prefix exists so a field holding a bare value counts for an award whose codes carry a letter: a French operator writes "74" in STATE, DDFM's codes are "D74". Def's own doc says exactly that. But searchOne applied the prefix AFTER looking the token up in the reference list — after the step that had just failed — so the bare form matched nothing and the prefix decorated an empty result. The only way through was a regex, in a mode where the operator had chosen "code" and explicitly not "pattern". The token lookup now tries the prefixed form when the bare one is not a known reference. The list stays the authority: an unknown number still matches nothing, so the prefix completes references rather than inventing them. Second bug in the same pass: the blanket prefix also hit codes that came straight OUT of the list, so a field already holding "D74" produced "DD74" as soon as a prefix was configured — for both the token lookup and the description matcher. Branches that yield whole codes are now excluded from it; the ones that yield a raw capture (regex, whole-field split) still get it.
This commit is contained in:
+20
-1
@@ -793,6 +793,10 @@ func searchOne(field, matchBy string, re *regexp.Regexp, exact bool, leading, tr
|
||||
byDesc := predefined && strings.EqualFold(strings.TrimSpace(matchBy), "description")
|
||||
|
||||
var found []string
|
||||
// codesAreFinal: this branch produced references straight from the award's
|
||||
// LIST, so they are already whole codes. The blanket prefix at the end must
|
||||
// leave them alone — prefixing a code that is already "D74" yields "DD74".
|
||||
codesAreFinal := false
|
||||
switch {
|
||||
case re != nil:
|
||||
// Award-level regex: capture group 1 (or whole match) for each hit.
|
||||
@@ -801,6 +805,7 @@ func searchOne(field, matchBy string, re *regexp.Regexp, exact bool, leading, tr
|
||||
// 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.
|
||||
codesAreFinal = true
|
||||
up := strings.ToUpper(raw)
|
||||
for _, nc := range rl.names {
|
||||
if exact {
|
||||
@@ -823,9 +828,23 @@ func searchOne(field, matchBy string, re *regexp.Regexp, exact bool, leading, tr
|
||||
// "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
|
||||
// references that declare a regex.
|
||||
codesAreFinal = true
|
||||
for _, tok := range tokenize(raw) {
|
||||
if _, ok := rl.byCode[tok]; ok {
|
||||
found = append(found, tok)
|
||||
continue
|
||||
}
|
||||
// The field may carry the reference WITHOUT the award's letter: a French
|
||||
// operator writes "74" in STATE while the DDFM codes are "D74". Prefix is
|
||||
// exactly what that case is for, so try the prefixed form HERE — applying
|
||||
// it after the lookup (as the blanket pass below used to) could never
|
||||
// help, because the lookup is the step that failed. Without this, the
|
||||
// only way to match a bare department was to write a regex, in a mode
|
||||
// where the operator had chosen "code" and not "pattern".
|
||||
if prefix != "" {
|
||||
if _, ok := rl.byCode[prefix+tok]; ok {
|
||||
found = append(found, prefix+tok)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, code := range rl.withPattern {
|
||||
@@ -839,7 +858,7 @@ func searchOne(field, matchBy string, re *regexp.Regexp, exact bool, leading, tr
|
||||
// counts each reference separately.
|
||||
found = splitRefs(raw)
|
||||
}
|
||||
if prefix != "" {
|
||||
if prefix != "" && !codesAreFinal {
|
||||
for i := range found {
|
||||
found[i] = prefix + found[i]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package award
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hamlog/internal/qso"
|
||||
)
|
||||
|
||||
// ddfmLike is a small stand-in for the French departments award: predefined
|
||||
// references whose codes carry a letter the operator does not type.
|
||||
func ddfmLike() []Def {
|
||||
return []Def{{
|
||||
Code: "DDFM", Name: "Departments", Type: TypeQSOFields,
|
||||
Field: "state", MatchBy: "code", Prefix: "D",
|
||||
}}
|
||||
}
|
||||
|
||||
func ddfmRefs() map[string][]RefMeta {
|
||||
return map[string][]RefMeta{"DDFM": {
|
||||
{Code: "D29", Name: "Finistère", Valid: true},
|
||||
{Code: "D49", Name: "Maine-et-Loire", Valid: true},
|
||||
{Code: "D74", Name: "Haute-Savoie", Valid: true},
|
||||
}}
|
||||
}
|
||||
|
||||
func refsOf(t *testing.T, defs []Def, q qso.QSO) []string {
|
||||
t.Helper()
|
||||
res := Compute(defs, []qso.QSO{q}, ddfmRefs(), nil)
|
||||
if len(res) != 1 {
|
||||
t.Fatalf("want 1 result, got %d", len(res))
|
||||
}
|
||||
var out []string
|
||||
for _, r := range res[0].Refs {
|
||||
if r.Worked {
|
||||
out = append(out, r.Ref)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// The reported case: the operator writes just "74" in STATE, the award's codes
|
||||
// are "D74", match-by is "code" and a Prefix of "D" is set. That found nothing —
|
||||
// the prefix was applied only AFTER the list lookup, i.e. after the step that
|
||||
// had already failed — so the only workaround was a regex, in a mode where the
|
||||
// operator had explicitly chosen "code" rather than "pattern".
|
||||
func TestPrefixCompletesABareReference(t *testing.T) {
|
||||
got := refsOf(t, ddfmLike(), qso.QSO{Callsign: "F5AYE", State: "74"})
|
||||
if len(got) != 1 || got[0] != "D74" {
|
||||
t.Errorf("bare state 74 with prefix D → %v, want [D74]", got)
|
||||
}
|
||||
}
|
||||
|
||||
// And a field that ALREADY holds the whole code must not be prefixed twice.
|
||||
// The blanket pass turned "D74" into "DD74" whenever a prefix was configured.
|
||||
func TestPrefixDoesNotDoubleUpOnACompleteCode(t *testing.T) {
|
||||
got := refsOf(t, ddfmLike(), qso.QSO{Callsign: "F5AYE", State: "D74"})
|
||||
if len(got) != 1 || got[0] != "D74" {
|
||||
t.Errorf("state D74 with prefix D → %v, want [D74]", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A token that is neither a code nor a prefixable one stays unmatched: the
|
||||
// prefix must not invent references.
|
||||
func TestPrefixDoesNotInventReferences(t *testing.T) {
|
||||
if got := refsOf(t, ddfmLike(), qso.QSO{Callsign: "F5AYE", State: "99"}); len(got) != 0 {
|
||||
t.Errorf("unknown department 99 → %v, want none", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Without a prefix nothing changes: a bare number matches nothing, a full code
|
||||
// matches itself.
|
||||
func TestNoPrefixKeepsExactCodeMatching(t *testing.T) {
|
||||
defs := ddfmLike()
|
||||
defs[0].Prefix = ""
|
||||
if got := refsOf(t, defs, qso.QSO{Callsign: "F5AYE", State: "74"}); len(got) != 0 {
|
||||
t.Errorf("no prefix, state 74 → %v, want none", got)
|
||||
}
|
||||
if got := refsOf(t, defs, qso.QSO{Callsign: "F5AYE", State: "D74"}); len(got) != 1 || got[0] != "D74" {
|
||||
t.Errorf("no prefix, state D74 → %v, want [D74]", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user