A log imported from HAMLOG.online carries the Russian district in CNTY, and the offline RDA database has its own answer for the same contact. Which wins is a real question — but only if the two are independent. If HAMLOG's value is itself a lookup in the same kind of database, there is nothing to arbitrate and a precedence rule would be ceremony around a redundancy. So this measures instead of deciding: Settings → RDA gains a read-only compare that reports agreements, disagreements, and the contacts where only one side knows, then lists the disagreements — callsign, date, both districts, whether the database answer was a DATED record (the award administrators' own fact) or the callsign's current district (an assumption), and whether the QSO is confirmed on HAMLOG (their value has then been through a match of two logs). It changes nothing in the log. What to do about a conflict is the next decision, and it should be taken with the operator's own numbers in hand. Also: the four HAMLOG fields become filterable, so 'never sent there' is a filter rather than a guess. A field that can be written and not read back is half a feature — pinned by a test against the bulk-editable list.
146 lines
5.0 KiB
Go
146 lines
5.0 KiB
Go
package main
|
|
|
|
// Comparing the two sources of a Russian district, BEFORE deciding which wins.
|
|
//
|
|
// A QSO can carry an RDA district from two places now:
|
|
//
|
|
// - the offline RDA database (internal/rda), compiled by the award's own
|
|
// administrators, which knows where a callsign was on a GIVEN DAY;
|
|
// - the CNTY field of a log downloaded from HAMLOG.online, whose export
|
|
// writes the district there (<CNTY:5>RO-19 on a real record).
|
|
//
|
|
// Whether that second source is EVIDENCE or merely a COPY is not a matter of
|
|
// opinion, and it decides everything downstream: if HAMLOG's value always
|
|
// equals the database's, there is nothing to arbitrate and a precedence rule
|
|
// would be ceremony around a redundancy. If they genuinely differ on some
|
|
// contacts, then two independent records disagree about a QSO that counts for
|
|
// an award, and that has to be surfaced rather than silently resolved.
|
|
//
|
|
// So: measure first. This reports agreements, disagreements, and the contacts
|
|
// where only one side has an answer — and it changes nothing in the log.
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"hamlog/internal/applog"
|
|
"hamlog/internal/award"
|
|
"hamlog/internal/qso"
|
|
"hamlog/internal/rda"
|
|
)
|
|
|
|
// rdaDistrictRe is the shape of an RDA reference: two letters, a dash, two
|
|
// digits (RO-19, KO-05). Used to tell a real district in CNTY from a US county
|
|
// name or whatever else a callbook dropped in that field.
|
|
var rdaDistrictRe = regexp.MustCompile(`^[A-Z]{2}-[0-9]{2}$`)
|
|
|
|
// RDAConflict is one contact where the two sources disagree.
|
|
type RDAConflict struct {
|
|
QSOID int64 `json:"qso_id"`
|
|
Callsign string `json:"callsign"`
|
|
Date string `json:"date"`
|
|
Band string `json:"band"`
|
|
Mode string `json:"mode"`
|
|
// FromLog is the district the imported log carries (CNTY).
|
|
FromLog string `json:"from_log"`
|
|
// FromDB is what the offline RDA database answers for that callsign on that
|
|
// day, and Dated says whether it was a dated activity record (a fact) or the
|
|
// callsign's current district (an assumption).
|
|
FromDB string `json:"from_db"`
|
|
Dated bool `json:"dated"`
|
|
// Confirmed is whether the QSO is confirmed on HAMLOG — a confirmed contact's
|
|
// CNTY has been through their matching of two logs, which is what makes it
|
|
// worth weighing against the database at all.
|
|
Confirmed bool `json:"confirmed"`
|
|
}
|
|
|
|
// RDACompareResult is the measurement.
|
|
type RDACompareResult struct {
|
|
Scanned int `json:"scanned"` // Russian QSOs examined
|
|
BothKnown int `json:"both_known"` // a district from each source
|
|
Agree int `json:"agree"`
|
|
Disagree int `json:"disagree"`
|
|
OnlyLog int `json:"only_log"` // CNTY has it, the database does not
|
|
OnlyDB int `json:"only_db"` // the database has it, the log does not
|
|
Neither int `json:"neither"`
|
|
// Conflicts lists the disagreements, newest first, capped so a large log
|
|
// cannot produce a dialog nobody can read. Count is in Disagree regardless.
|
|
Conflicts []RDAConflict `json:"conflicts"`
|
|
}
|
|
|
|
// rdaCompareMax bounds the returned list. The COUNT is always exact; it is the
|
|
// enumeration that is capped, because a hundred rows is already more than
|
|
// anyone reviews in one sitting.
|
|
const rdaCompareMax = 200
|
|
|
|
// CompareRDASources measures the two sources against each other. Read-only.
|
|
func (a *App) CompareRDASources() (RDACompareResult, error) {
|
|
var res RDACompareResult
|
|
if a.qso == nil {
|
|
return res, fmt.Errorf("db not initialized")
|
|
}
|
|
rows, err := a.qso.List(a.ctx, qso.ListFilter{Limit: 1_000_000})
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
for i := range rows {
|
|
q := &rows[i]
|
|
// The same three entities the award covers — see BackfillRDA.
|
|
if q.DXCC == nil {
|
|
continue
|
|
}
|
|
switch *q.DXCC {
|
|
case 15, 54, 126:
|
|
default:
|
|
continue
|
|
}
|
|
res.Scanned++
|
|
|
|
fromLog := strings.ToUpper(strings.TrimSpace(q.County))
|
|
if !rdaDistrictRe.MatchString(fromLog) {
|
|
fromLog = "" // a county name, a stray value: not a district
|
|
}
|
|
var fromDB string
|
|
var dated bool
|
|
if m, ok := rda.Lookup(q.Callsign, q.QSODate); ok {
|
|
fromDB, dated = m.District, m.Dated
|
|
}
|
|
|
|
switch {
|
|
case fromLog == "" && fromDB == "":
|
|
res.Neither++
|
|
case fromLog == "":
|
|
res.OnlyDB++
|
|
case fromDB == "":
|
|
res.OnlyLog++
|
|
default:
|
|
res.BothKnown++
|
|
if fromLog == fromDB {
|
|
res.Agree++
|
|
continue
|
|
}
|
|
res.Disagree++
|
|
if len(res.Conflicts) < rdaCompareMax {
|
|
res.Conflicts = append(res.Conflicts, RDAConflict{
|
|
QSOID: q.ID, Callsign: q.Callsign,
|
|
Date: q.QSODate.UTC().Format("2006-01-02"),
|
|
Band: q.Band, Mode: q.Mode,
|
|
FromLog: fromLog, FromDB: fromDB, Dated: dated,
|
|
Confirmed: q.Extras != nil && isHamlogConfirmed(q.Extras[award.HamlogQSLKey]),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
applog.Printf("rda compare: %d Russian QSOs — both %d (agree %d, disagree %d), only-log %d, only-db %d, neither %d",
|
|
res.Scanned, res.BothKnown, res.Agree, res.Disagree, res.OnlyLog, res.OnlyDB, res.Neither)
|
|
return res, nil
|
|
}
|
|
|
|
// isHamlogConfirmed reads their Y — anything that is not an explicit no counts,
|
|
// the same rule the award engine applies.
|
|
func isHamlogConfirmed(v string) bool {
|
|
v = strings.ToUpper(strings.TrimSpace(v))
|
|
return v != "" && v != "N" && v != "NO"
|
|
}
|