From 6f2f9236b03f51f1d45b9f8b5f4c15e1b17b6ac7 Mon Sep 17 00:00:00 2001 From: rouggy Date: Fri, 10 Jul 2026 10:58:33 +0200 Subject: [PATCH] fix: bug correction in awards where a reference regex was not checked --- app.go | 2 ++ internal/award/award.go | 32 ++++++++++++++++++++++++++++---- internal/award/award_test.go | 22 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/app.go b/app.go index 0b88a4a..72cf78b 100644 --- a/app.go +++ b/app.go @@ -6821,12 +6821,14 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe } else { 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) if err != nil { emit("Download failed: " + err.Error()) done(matched, total) return } + emit(fmt.Sprintf("LoTW returned %d KB of ADIF", len(adifText)/1024)) keyIDs, kerr := a.qso.DedupeKeyIDs(ctx) if kerr != nil { emit("Error reading local log: " + kerr.Error()) diff --git a/internal/award/award.go b/internal/award/award.go index 69cc265..ae27847 100644 --- a/internal/award/award.go +++ b/internal/award/award.go @@ -13,6 +13,7 @@ package award import ( + "errors" "regexp" "sort" "strconv" @@ -257,6 +258,21 @@ type RefMeta struct { } // 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 { rl := refList{byCode: make(map[string]RefMeta, len(metas))} for _, m := range metas { @@ -265,7 +281,7 @@ func NewRefList(metas []RefMeta) refList { continue } 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 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)) for i := range defs { if p := strings.TrimSpace(defs[i].Pattern); p != "" { - re, err := regexp.Compile(p) + re, err := compileAwardRE(p) if err != nil { perr[i] = "bad pattern: " + err.Error() } else { @@ -432,7 +448,7 @@ func MatchQSO(d Def, metas []RefMeta, q *qso.QSO) []string { } var re *regexp.Regexp if p := strings.TrimSpace(d.Pattern); p != "" { - if c, err := regexp.Compile(p); err == nil { + if c, err := compileAwardRE(p); err == nil { re = c } else { return nil @@ -565,6 +581,14 @@ func searchOne(field, matchBy string, re *regexp.Regexp, exact bool, leading, tr 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: // "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 @@ -602,7 +626,7 @@ func candidates(d *Def, re *regexp.Regexp, q *qso.QSO, rl refList, hasList bool) r := &d.OrRules[i] var rre *regexp.Regexp if p := strings.TrimSpace(r.Pattern); p != "" { - c, err := regexp.Compile(p) + c, err := compileAwardRE(p) if err != nil { continue // skip a rule with a bad regex rather than failing the award } diff --git a/internal/award/award_test.go b/internal/award/award_test.go index 6917e1c..6758cbc 100644 --- a/internal/award/award_test.go +++ b/internal/award/award_test.go @@ -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 // line (VUCC_GRIDS) contributes several. grid4 derives from VUCC_GRIDS else the // 4-char prefix of GRIDSQUARE.