fix(worked-before): a prefixed call is one operator, not a whole country

Reported from a screenshot: working ZA/OE8NDR, the worked-before list showed
ZA/IZ2DPX and ZA/IW2JOP beside him and the header counted all four as contacts
"with this call".

callMatch folded portable forms together by taking the part before the slash.
That is right when the slash carries a suffix — RK3DWA/3, RK3DWA/P are one
person — and exactly wrong when it carries a prefix: ZA/OE8NDR became the
station "ZA", and the predicate `callsign LIKE 'ZA/%'` then selected every other
visitor to Albania.

The operator's own call is now picked out properly: drop the known qualifiers
(P, M, MM, AM, QRP, a bare call-area digit) and of what remains take the longest
part — a country prefix is short by nature, ZA, F, KH6, VP2E, and a callsign is
not. The predicate matches the prefixed forms too, so ZA/OE8NDR and a plain
OE8NDR still find each other, which is the whole point of the fold.

A base under three characters falls back to an exact match: a malformed entry
must not produce a LIKE that selects half the logbook.
This commit is contained in:
2026-08-16 18:08:28 +02:00
parent b5a88ee5a2
commit 14ac73028e
4 changed files with 146 additions and 31 deletions
+57 -6
View File
@@ -1723,26 +1723,77 @@ type BandMode struct {
// rendering a recent-contacts mini-list.
const maxWorkedEntries = 50
// operatorSuffixes are the appendages that qualify a callsign without changing
// whose it is. A bare digit (RK3DWA/3) counts too, handled separately.
var operatorSuffixes = map[string]bool{
"P": true, "M": true, "MM": true, "AM": true, "QRP": true,
"A": true, "B": true, "J": true, "LH": true, "R": true, "T": true,
}
// baseCall extracts the operator's OWN callsign from a portable form.
//
// The slash carries the qualifier on either side, and which side depends on
// what it is: RK3DWA/3 and RK3DWA/P append to the call, while ZA/OE8NDR and
// F/DL1ABC put a country prefix in front of it. Taking the part before the
// slash — which is what this used to do — reads ZA/OE8NDR as the station "ZA".
//
// So: drop the known qualifiers, and of what is left take the longest part. A
// prefix is short by nature (ZA, F, KH6, VP2E) and a callsign is not.
func baseCall(call string) string {
if !strings.Contains(call, "/") {
return call
}
best := ""
for _, p := range strings.Split(call, "/") {
if p == "" || operatorSuffixes[p] {
continue
}
if len(p) == 1 && p[0] >= '0' && p[0] <= '9' {
continue // the call-area digit
}
if len(p) > len(best) {
best = p
}
}
if best == "" {
return call
}
return best
}
// callMatch builds the WHERE fragment that selects one station's QSOs.
//
// Exact by default. With variants on, an operator's portable forms count as the
// same station: RK3DWA, RK3DWA/3, RK3DWA/P and RK3DWA/QRP are one person, and
// someone asking "have I worked RK3DWA?" means the person, not the string.
// Typing 21 QSOs' worth of history only when you happen to add "/3" is the
// behaviour this replaces. The suffix is stripped from what was TYPED too, so
// it matches both ways round — RK3DWA/3 also finds the plain RK3DWA contacts.
// behaviour this replaces.
//
// PREFIXED FORMS MATCH TOO, and getting that wrong is what prompted this.
// Matching on the part before the slash turned ZA/OE8NDR into the station "ZA",
// so the worked-before list for one Austrian operating from Albania showed
// every OTHER visitor to Albania — two Italians and himself — and counted them
// as four contacts "with this call".
//
// Deliberately NOT a bare "starts with": LIKE 'RK3DWA%' would also drag in
// RK3DWAB, which is a different station. The '/' is what makes it the same one.
// And the variant match is only used on a base of three characters or more —
// below that a prefix could match half the log.
func callMatch(call string, variants bool) (string, []any) {
if !variants {
return "callsign = ?", []any{call}
}
base := call
if i := strings.IndexByte(base, '/'); i > 0 {
base = base[:i]
base := baseCall(call)
if len(base) < 3 {
return "callsign = ?", []any{call}
}
return "(callsign = ? OR callsign LIKE ?)", []any{base, base + "/%"}
return "(callsign = ? OR callsign LIKE ? OR callsign LIKE ? OR callsign LIKE ?)",
[]any{
base, // OE8NDR
base + "/%", // OE8NDR/P
"%/" + base, // ZA/OE8NDR
"%/" + base + "/%", // ZA/OE8NDR/P
}
}
// WorkedBefore returns aggregated history at both callsign and DXCC level.