feat(hamlog): show what the confirmation import did, and export what it could not place
The import printed four numbers and left nothing behind. 1106 matched and 395 unmatched out of 1501 is not a report: the operator cannot see which contacts were confirmed, nor work through the discrepancies. Two additions. The Results view is now filled with the confirmed contacts, flagged new entity/band/mode/slot against what LoTW and paper QSL already confirm -- the only sense in which a HAMLOG confirmation changes anything. And the unmatched ones can be written out as ADIF, because a few hundred of them are a list to open and compare, not something to read in a log window.
This commit is contained in:
+87
-1
@@ -34,6 +34,11 @@ import (
|
||||
"hamlog/internal/qso"
|
||||
)
|
||||
|
||||
// hamlogUnmatchedMax bounds what is kept for export. A whole log's worth of
|
||||
// unmatched records means the file belongs to another station, not that the
|
||||
// operator wants 50 000 of them written back out.
|
||||
const hamlogUnmatchedMax = 20000
|
||||
|
||||
// HamlogCfmResult is what the import did.
|
||||
type HamlogCfmResult struct {
|
||||
Total int `json:"total"` // records read from the file
|
||||
@@ -42,7 +47,9 @@ type HamlogCfmResult struct {
|
||||
ByClass int `json:"by_class"` // matched on mode CLASS rather than exact mode
|
||||
Unmatched int `json:"unmatched"` // confirmations with no local QSO
|
||||
// Samples names a few unmatched contacts, so "12 unmatched" can be looked
|
||||
// into rather than merely worried about.
|
||||
// into rather than merely worried about. The full list is kept for export —
|
||||
// see ExportHamlogUnmatched — because 395 of them is not a sample-sized
|
||||
// problem: it is a list to work through.
|
||||
Samples []string `json:"samples"`
|
||||
}
|
||||
|
||||
@@ -86,6 +93,14 @@ func (a *App) ImportHamlogConfirmations(path string) (HamlogCfmResult, error) {
|
||||
}
|
||||
emit("Reading " + path + "…")
|
||||
|
||||
// What already counts towards an award, so each confirmation can be flagged
|
||||
// NEW. LoTW and paper QSL are the two award-valid sources; a HAMLOG
|
||||
// confirmation is "new" when it lands on a slot neither of them holds — which
|
||||
// is the only sense in which it changes anything.
|
||||
sets, _ := a.qso.ConfirmedSlots(ctx, []string{"lotw_rcvd", "qsl_rcvd"})
|
||||
var items []ConfirmationItem
|
||||
var unmatched []qso.QSO
|
||||
|
||||
perr := adif.Parse(f, func(rec adif.Record) error {
|
||||
q, ok := adif.RecordToQSO(rec)
|
||||
if !ok {
|
||||
@@ -116,6 +131,9 @@ func (a *App) ImportHamlogConfirmations(path string) (HamlogCfmResult, error) {
|
||||
res.Samples = append(res.Samples, fmt.Sprintf("%s · %s · %s · %s",
|
||||
q.Callsign, q.QSODate.UTC().Format("2006-01-02 15:04Z"), q.Band, q.Mode))
|
||||
}
|
||||
if len(unmatched) < hamlogUnmatchedMax {
|
||||
unmatched = append(unmatched, q)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
date := hamlogCfmDate(rec)
|
||||
@@ -128,12 +146,45 @@ func (a *App) ImportHamlogConfirmations(path string) (HamlogCfmResult, error) {
|
||||
// from their website would otherwise read "never sent, yet confirmed".
|
||||
_ = a.qso.SetExtra(ctx, id, hamlogSentKey, "Y")
|
||||
res.Matched++
|
||||
|
||||
// Feed the Results view, the same rows the LoTW download produces: an
|
||||
// import that only prints counts leaves the operator with no way to see
|
||||
// WHICH contacts were confirmed, which is the reason they ran it.
|
||||
a.enrichContactedFromCty(&q) // country/dxcc, for the entity flags
|
||||
it := ConfirmationItem{
|
||||
Callsign: q.Callsign,
|
||||
QSODate: q.QSODate.UTC().Format(time.RFC3339),
|
||||
Band: q.Band,
|
||||
Mode: q.Mode,
|
||||
Country: q.Country,
|
||||
}
|
||||
if q.DXCC != nil && *q.DXCC != 0 {
|
||||
n := *q.DXCC
|
||||
it.NewDXCC = !sets.DXCC[n]
|
||||
it.NewBand = !sets.Band[qso.BandKey(n, q.Band)]
|
||||
it.NewMode = !sets.Mode[qso.ModeClassKey(n, q.Mode)]
|
||||
it.NewSlot = !sets.Slot[qso.SlotClassKey(n, q.Band, q.Mode)]
|
||||
// Fold it in, so a repeat inside the same file isn't flagged twice.
|
||||
sets.DXCC[n] = true
|
||||
sets.Band[qso.BandKey(n, q.Band)] = true
|
||||
sets.Mode[qso.ModeClassKey(n, q.Mode)] = true
|
||||
sets.Slot[qso.SlotClassKey(n, q.Band, q.Mode)] = true
|
||||
}
|
||||
items = append(items, it)
|
||||
return nil
|
||||
})
|
||||
if perr != nil {
|
||||
return res, perr
|
||||
}
|
||||
a.invalidateAwardStats() // confirmations move award counts
|
||||
// Kept for ExportHamlogUnmatched. Held rather than written now: the operator
|
||||
// decides whether a list of 395 is worth a file.
|
||||
a.hamlogUnmatchedMu.Lock()
|
||||
a.hamlogUnmatched = unmatched
|
||||
a.hamlogUnmatchedMu.Unlock()
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "qslmgr:confirmations", items)
|
||||
}
|
||||
applog.Printf("hamlog cfm import: %d records, %d confirmed, %d matched (%d by mode class), %d unmatched",
|
||||
res.Total, res.Confirmed, res.Matched, res.ByClass, res.Unmatched)
|
||||
emit(fmt.Sprintf("%d records read, %d confirmed by HAMLOG.online, %d matched in the log (%d by mode class), %d unmatched",
|
||||
@@ -141,12 +192,47 @@ func (a *App) ImportHamlogConfirmations(path string) (HamlogCfmResult, error) {
|
||||
for _, s := range res.Samples {
|
||||
emit(" unmatched: " + s)
|
||||
}
|
||||
if res.Unmatched > len(res.Samples) {
|
||||
emit(fmt.Sprintf(" …and %d more — use \"Export unmatched (ADIF)…\" to get the whole list.",
|
||||
res.Unmatched-len(res.Samples)))
|
||||
}
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "qslmgr:done", map[string]any{"uploaded": res.Matched, "total": res.Confirmed})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ExportHamlogUnmatched writes the confirmations the last import could not
|
||||
// place onto a QSO.
|
||||
//
|
||||
// They are the interesting half of the result: each one is a contact HAMLOG
|
||||
// believes it holds and this log does not agree about — a minute of drift, a
|
||||
// portable call, a band written differently, or a QSO genuinely missing. A
|
||||
// count cannot be worked through; a file can be opened, sorted and compared.
|
||||
//
|
||||
// Written as ADIF because that is what every other tool reads, and because it
|
||||
// can be handed straight back to an import once the discrepancies are settled.
|
||||
func (a *App) ExportHamlogUnmatched(path string) (int, error) {
|
||||
a.hamlogUnmatchedMu.Lock()
|
||||
rows := a.hamlogUnmatched
|
||||
a.hamlogUnmatchedMu.Unlock()
|
||||
if len(rows) == 0 {
|
||||
return 0, fmt.Errorf("nothing to export: the last import left no unmatched confirmations")
|
||||
}
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return 0, fmt.Errorf("empty path")
|
||||
}
|
||||
recs := make([]string, 0, len(rows))
|
||||
for i := range rows {
|
||||
recs = append(recs, adif.FullRecordADIF(rows[i]))
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(adif.BatchRecordsADIF(recs)), 0o644); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
applog.Printf("hamlog cfm import: exported %d unmatched confirmation(s) to %s", len(rows), path)
|
||||
return len(rows), nil
|
||||
}
|
||||
|
||||
// hamlogQSLDateKey stamps WHEN the confirmation was read back. Their export
|
||||
// carries no confirmation date of its own, so this is the import date — which
|
||||
// is honest about what it knows, unlike borrowing the QSO date.
|
||||
|
||||
Reference in New Issue
Block a user