feat: move ADIF fields on import, for contest exports that misplace the exchange

An operator worked the RSGB IOTA contest in another logger and got back records
carrying <STATE:5>EU005: N1MM-class software stores the received exchange in the
column its contest module uses, not the one ADIF reserves. Imported verbatim,
every QSO gains a US-state field reading "EU005" and the IOTA award sees
nothing — the reference is in the log, just not where anything looks for it.

The import dialog gains optional source → destination rows, prefilled with
STATE → IOTA since that is the case that prompted it. Applied to the RAW record
before conversion, so it works for promoted columns and Extras alike and the
destination is parsed exactly as if the file had carried it there.

Two rules, both from the same principle that the file outranks our guess:
  - the source is CLEARED, so a reference does not linger in STATE where it
    would show as the contacted station's US state in the grid and every export;
  - a destination the file already filled is kept, judged against the record as
    it ARRIVED — otherwise the result would depend on Go's map iteration order,
    which is deliberately random.

The swap case in the test is what forced that second rule to be stated: "never
overwrite" and "exchange two fields" cannot both hold silently, so a destination
that is itself a mapping source is treated as an explicit swap and nothing else
is overwritten.
This commit is contained in:
2026-07-30 14:57:05 +02:00
parent d322ea9a07
commit 4d5c5c3eb3
8 changed files with 195 additions and 9 deletions
+19 -1
View File
@@ -5925,11 +5925,17 @@ func (a *App) OpenADIFFile() (string, error) {
// Log4OM / LoTW without clobbering fields the file omits
// - "all" : insert every record, duplicates included
//
// fieldMap moves ADIF fields before conversion (source → destination, ADIF tag
// names in any case). Contest exports put the exchange where their module keeps
// it rather than where ADIF says: the RSGB IOTA contest arrives with the island
// reference in STATE, which imports as a US state and leaves the IOTA award
// empty. The destination is only filled when the file left it blank.
//
// applyCty, when true, recomputes country / continent / DXCC / CQ / ITU from
// cty.dat for every record, overriding what the file carries — corrects the
// wrong COUNTRY that contest software often exports (e.g. RG2Y as Asiatic
// Russia). Everything else in the ADIF is still preserved verbatim.
func (a *App) ImportADIF(path string, dupMode string, applyCty bool, applyStation bool) (adif.ImportResult, error) {
func (a *App) ImportADIF(path string, dupMode string, applyCty bool, applyStation bool, fieldMap map[string]string) (adif.ImportResult, error) {
if a.qso == nil {
return adif.ImportResult{}, fmt.Errorf("db not initialized")
}
@@ -5943,6 +5949,18 @@ func (a *App) ImportADIF(path string, dupMode string, applyCty bool, applyStatio
// descriptive metadata and safe to fill (identity fields are still left
// alone, see applyStationDefaults).
im := &adif.Importer{Repo: a.qso}
if len(fieldMap) > 0 {
lower := make(map[string]string, len(fieldMap))
for from, to := range fieldMap {
from = strings.ToLower(strings.TrimSpace(from))
to = strings.ToLower(strings.TrimSpace(to))
if from != "" && to != "" && from != to {
lower[from] = to
}
}
im.FieldMap = lower
applog.Printf("import: field mapping %v", lower)
}
switch dupMode {
case "update":
im.UpdateDuplicates = true