feat(awards): refuse an ambiguous match on one-reference awards
Two DARC DOKs are both called "Gießen" — two clubs share the town. Exact
matching finds both and both are right as far as the matcher can tell, so
it kept both and the operator got a QSO credited to a DOK that may well
have been the other one.
Picking one is not an option: it writes a reference into the log with no
evidence behind it. So a Def can now be marked one reference per QSO, and
an ambiguous match on such an award assigns none, names the candidates in
the trace and leaves the contact under missing references — where the
operator's choice now sticks (06e3437).
Off by default, and deliberately NOT a revival of the old `Multi` flag: a
field holding several references (an n-fer POTA activation, a VUCC grid
line) is several references, correctly, and stays that way.
This commit is contained in:
+29
-5
@@ -75,10 +75,22 @@ type Def struct {
|
||||
LeadingStr string `json:"leading_str,omitempty"` // strip this prefix before matching
|
||||
TrailingStr string `json:"trailing_str,omitempty"` // strip this suffix before matching
|
||||
Dynamic bool `json:"dynamic,omitempty"` // references not predefined (any value counts)
|
||||
// NOTE: there is no "one reference per QSO" switch, and there was never any
|
||||
// point in one. A QSO ALWAYS yields every reference its field holds — an n-fer
|
||||
// POTA activation ("US-6544,US-0680"), a VUCC contact on a grid line. The old
|
||||
// `Multi` flag was read by nothing; its checkbox changed nothing.
|
||||
// OneRefPerQSO refuses an AMBIGUOUS match rather than guessing.
|
||||
//
|
||||
// This is not the old `Multi` flag, which was removed for good reason: a QSO
|
||||
// always yields every reference its FIELD holds — an n-fer POTA activation
|
||||
// ("US-6544,US-0680"), a VUCC contact on a grid line. That is several
|
||||
// references, correctly.
|
||||
//
|
||||
// The case here is the opposite. The field holds ONE value and two entries in
|
||||
// the reference list carry it: two German DOKs are both called "Gießen",
|
||||
// because two clubs share the town. The matcher is not wrong, the data is
|
||||
// ambiguous — and picking one would write a reference into the log that may
|
||||
// well be the other. So on an award where a QSO can only ever count for one
|
||||
// reference, an ambiguous match assigns NONE and the contact appears under
|
||||
// "missing references", where the operator picks. Their choice then sticks:
|
||||
// a manual reference overrides the matcher.
|
||||
OneRefPerQSO bool `json:"one_ref_per_qso,omitempty"`
|
||||
|
||||
// OrRules are ordered FALLBACK searches for the primary one above: they are
|
||||
// tried IN ORDER and only while nothing has matched yet — the first rule that
|
||||
@@ -863,7 +875,11 @@ type Explanation struct {
|
||||
// replaced it — shown in the trace so a correction is explainable rather
|
||||
// than mysterious.
|
||||
Superseded []string `json:"superseded,omitempty"`
|
||||
Result []string `json:"result"` // what the QSO finally counts for
|
||||
// Ambiguous lists the references that matched when the award allows only one
|
||||
// — none of them was kept. Shown so the operator sees WHICH ones to choose
|
||||
// between rather than an unexplained blank.
|
||||
Ambiguous []string `json:"ambiguous,omitempty"`
|
||||
Result []string `json:"result"` // what the QSO finally counts for
|
||||
}
|
||||
|
||||
// Explain runs the matcher on a single QSO and reports what it did. It goes
|
||||
@@ -998,6 +1014,14 @@ func candidatesTrace(d *Def, re *regexp.Regexp, q *qso.QSO, rl refList, hasList
|
||||
found = manual
|
||||
}
|
||||
out := dedupe(found)
|
||||
// Ambiguity is not a result. See OneRefPerQSO.
|
||||
if d.OneRefPerQSO && len(manual) == 0 && len(out) > 1 {
|
||||
if ex != nil {
|
||||
ex.Ambiguous = out
|
||||
ex.Result = []string{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if ex != nil {
|
||||
ex.Result = out
|
||||
}
|
||||
|
||||
@@ -54,3 +54,47 @@ func TestManualReferenceReplacesTheMatch(t *testing.T) {
|
||||
t.Error("an untouched QSO lost its automatic references")
|
||||
}
|
||||
}
|
||||
|
||||
// On an award where a QSO can only count for ONE reference, an ambiguous match
|
||||
// assigns none.
|
||||
//
|
||||
// Two DARC DOKs are called "Gießen" because two clubs share the town. Exact
|
||||
// matching finds both, and both are right as far as the matcher can tell.
|
||||
// Picking one would write a reference into the log that may well be the other —
|
||||
// so the contact is left for the operator, who now has the last word.
|
||||
func TestAmbiguousMatchIsRefusedWhenOnlyOneReferenceIsAllowed(t *testing.T) {
|
||||
def := Def{
|
||||
Code: "DLD", Name: "DARC DOK", Field: "qth",
|
||||
MatchBy: "description", ExactMatch: true, Type: "QSOFIELDS", Valid: true,
|
||||
OneRefPerQSO: true,
|
||||
}
|
||||
metas := []RefMeta{
|
||||
{Code: "F07", Name: "Gießen", Valid: true},
|
||||
{Code: "F61", Name: "Gießen", Valid: true},
|
||||
{Code: "B11", Name: "Kassel", Valid: true},
|
||||
}
|
||||
|
||||
amb := Explain(def, metas, &qso.QSO{Callsign: "DL2FDM", QTH: "Gießen"})
|
||||
if len(amb.Result) != 0 {
|
||||
t.Errorf("an ambiguous match produced %v — it should produce nothing", amb.Result)
|
||||
}
|
||||
if len(amb.Ambiguous) != 2 {
|
||||
t.Errorf("the trace lists %v — it should name both candidates", amb.Ambiguous)
|
||||
}
|
||||
|
||||
// An unambiguous one is untouched: this must not make the award stricter in
|
||||
// general, only where the data genuinely does not decide.
|
||||
ok := Explain(def, metas, &qso.QSO{Callsign: "DL3XYZ", QTH: "Kassel"})
|
||||
if len(ok.Result) != 1 || ok.Result[0] != "B11" {
|
||||
t.Errorf("an unambiguous QSO gave %v — want B11", ok.Result)
|
||||
}
|
||||
|
||||
// And the operator's choice still wins over the ambiguity.
|
||||
fixed := Explain(def, metas, &qso.QSO{
|
||||
Callsign: "DL2FDM", QTH: "Gießen",
|
||||
Extras: map[string]string{ManualRefsKey: "DLD@F61"},
|
||||
})
|
||||
if len(fixed.Result) != 1 || fixed.Result[0] != "F61" {
|
||||
t.Errorf("after the operator picked F61 the QSO counts for %v", fixed.Result)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user