fix: bug correction in awards where a reference regex was not checked

This commit is contained in:
2026-07-10 10:58:33 +02:00
parent efb61107fe
commit 6f2f9236b0
3 changed files with 52 additions and 4 deletions
+2
View File
@@ -6821,12 +6821,14 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
} else { } else {
emit(fmt.Sprintf("Downloading all LoTW confirmations for %s…", callLabel)) emit(fmt.Sprintf("Downloading all LoTW confirmations for %s…", callLabel))
} }
emit(fmt.Sprintf("Window: since=%q → resolved=%q (scope owncall=%q)", since, sinceDate, ownCall))
adifText, err := extsvc.DownloadLoTWConfirmations(ctx, nil, cfg.LoTW, sinceDate, ownCall) adifText, err := extsvc.DownloadLoTWConfirmations(ctx, nil, cfg.LoTW, sinceDate, ownCall)
if err != nil { if err != nil {
emit("Download failed: " + err.Error()) emit("Download failed: " + err.Error())
done(matched, total) done(matched, total)
return return
} }
emit(fmt.Sprintf("LoTW returned %d KB of ADIF", len(adifText)/1024))
keyIDs, kerr := a.qso.DedupeKeyIDs(ctx) keyIDs, kerr := a.qso.DedupeKeyIDs(ctx)
if kerr != nil { if kerr != nil {
emit("Error reading local log: " + kerr.Error()) emit("Error reading local log: " + kerr.Error())
+28 -4
View File
@@ -13,6 +13,7 @@
package award package award
import ( import (
"errors"
"regexp" "regexp"
"sort" "sort"
"strconv" "strconv"
@@ -257,6 +258,21 @@ type RefMeta struct {
} }
// NewRefList builds the engine's reference view from (code, meta) pairs. // NewRefList builds the engine's reference view from (code, meta) pairs.
// compileAwardRE compiles an award / reference regex. Award patterns are run
// over free-text log fields (QTH, NOTES) whose capitalization is arbitrary — a
// log may hold "Tokyo", "TOKYO" or "TOKIO" — so we match case-insensitively by
// default. An author who set their own flag group (e.g. "(?s)") is respected.
func compileAwardRE(p string) (*regexp.Regexp, error) {
p = strings.TrimSpace(p)
if p == "" {
return nil, errors.New("empty pattern")
}
if !strings.HasPrefix(p, "(?") {
p = "(?i)" + p
}
return regexp.Compile(p)
}
func NewRefList(metas []RefMeta) refList { func NewRefList(metas []RefMeta) refList {
rl := refList{byCode: make(map[string]RefMeta, len(metas))} rl := refList{byCode: make(map[string]RefMeta, len(metas))}
for _, m := range metas { for _, m := range metas {
@@ -265,7 +281,7 @@ func NewRefList(metas []RefMeta) refList {
continue continue
} }
if p := strings.TrimSpace(m.Pattern); p != "" { if p := strings.TrimSpace(m.Pattern); p != "" {
if re, err := regexp.Compile(p); err == nil { if re, err := compileAwardRE(p); err == nil {
m.re = re m.re = re
rl.withPattern = append(rl.withPattern, code) rl.withPattern = append(rl.withPattern, code)
} }
@@ -297,7 +313,7 @@ func Compute(defs []Def, qsos []qso.QSO, refMetas map[string][]RefMeta, nameOf N
perr := make([]string, len(defs)) perr := make([]string, len(defs))
for i := range defs { for i := range defs {
if p := strings.TrimSpace(defs[i].Pattern); p != "" { if p := strings.TrimSpace(defs[i].Pattern); p != "" {
re, err := regexp.Compile(p) re, err := compileAwardRE(p)
if err != nil { if err != nil {
perr[i] = "bad pattern: " + err.Error() perr[i] = "bad pattern: " + err.Error()
} else { } else {
@@ -432,7 +448,7 @@ func MatchQSO(d Def, metas []RefMeta, q *qso.QSO) []string {
} }
var re *regexp.Regexp var re *regexp.Regexp
if p := strings.TrimSpace(d.Pattern); p != "" { if p := strings.TrimSpace(d.Pattern); p != "" {
if c, err := regexp.Compile(p); err == nil { if c, err := compileAwardRE(p); err == nil {
re = c re = c
} else { } else {
return nil return nil
@@ -565,6 +581,14 @@ func searchOne(field, matchBy string, re *regexp.Regexp, exact bool, leading, tr
found = append(found, nc.code) found = append(found, nc.code)
} }
} }
// A reference may also declare its own regex to broaden matching beyond
// its plain name (e.g. Tokyo's `\bTok[iy]o\b` also catches "TOKIO"). Test
// those too — the name match and the pattern are OR'd.
for _, code := range rl.withPattern {
if m := rl.byCode[code]; m.re != nil && m.re.MatchString(raw) {
found = append(found, code)
}
}
case predefined && !exact: case predefined && !exact:
// "Search reference inside the field": look up each token of the field in // "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 // the list — O(tokens), not O(all references) — plus test the few
@@ -602,7 +626,7 @@ func candidates(d *Def, re *regexp.Regexp, q *qso.QSO, rl refList, hasList bool)
r := &d.OrRules[i] r := &d.OrRules[i]
var rre *regexp.Regexp var rre *regexp.Regexp
if p := strings.TrimSpace(r.Pattern); p != "" { if p := strings.TrimSpace(r.Pattern); p != "" {
c, err := regexp.Compile(p) c, err := compileAwardRE(p)
if err != nil { if err != nil {
continue // skip a rule with a bad regex rather than failing the award continue // skip a rule with a bad regex rather than failing the award
} }
+22
View File
@@ -128,6 +128,28 @@ func TestComputeMatchByDescription(t *testing.T) {
} }
} }
// A description-match award must also honour a REFERENCE's own regex (used to
// broaden matching past the plain name) AND match case-insensitively — a log
// QTH "ITABASHIKU, TOKIO" (uppercase, "Tokio" spelling) must count for Tokyo
// via its `\bTok[iy]o\b` pattern, which the plain name "Tokyo" can't catch.
func TestComputeDescriptionRefPattern(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: "JA1LZB", Band: "10m", DXCC: ip(339), QTH: "ITABASHIKU, TOKIO"}, // pattern + case
{Callsign: "JA3DEF", Band: "40m", DXCC: ip(339), QTH: "osaka pref"}, // lowercase name
}
refMetas := map[string][]RefMeta{"WAJA": {
{Code: "13", Name: "Tokyo", Pattern: `\bTok[iy]o\b`, Valid: true},
{Code: "27", Name: "Osaka", Valid: true},
{Code: "01", Name: "Hokkaido", Valid: true},
}}
r := Compute([]Def{def}, qsos, refMetas, nil)[0]
if r.Worked != 2 {
t.Errorf("WAJA worked = %d, want 2 (Tokyo via pattern + Osaka via lowercase name); got %v", r.Worked, refCodes(r))
}
}
// VUCC: a grid4 award counts distinct 4-char grid squares, and a QSO on a grid // VUCC: a grid4 award counts distinct 4-char grid squares, and a QSO on a grid
// line (VUCC_GRIDS) contributes several. grid4 derives from VUCC_GRIDS else the // line (VUCC_GRIDS) contributes several. grid4 derives from VUCC_GRIDS else the
// 4-char prefix of GRIDSQUARE. // 4-char prefix of GRIDSQUARE.