diff --git a/app.go b/app.go index 983d6c4..3bfe71d 100644 --- a/app.go +++ b/app.go @@ -10249,14 +10249,26 @@ func (a *App) applyClublogException(q *qso.QSO, force bool) bool { } e, ok := a.clublog.Resolve(q.Callsign, date) if !ok { - // No exception COVERS this QSO's date. If the call nonetheless HAS a - // date-ranged exception (e.g. G1T = Scotland only from 2024-02-21), then - // cty.dat's date-blind "=G1T → Scotland" override is WRONG for an older - // QSO — resolve it by ClubLog's date-aware PREFIX table instead (G1 → - // England for a 2012 contact). Ordinary calls (no exception) are left to - // cty.dat. - if a.clublog.HasException(q.Callsign) { - if pe, pok := a.clublog.ResolvePrefix(q.Callsign, date); pok { + // No exception covers this QSO's date, so the question becomes which + // country file knows the PREFIX better. + // + // ClubLog's prefix table is date-ranged and cty.dat's is not, and that + // is not a detail: cty.dat describes the world as it is TODAY. Niue + // moved to E6, so ZK2 went back to New Zealand there — and every ZK2 + // contact ever made was silently relabelled New Zealand, taking a + // confirmed entity out of an operator's DXCC with it. ZK1 loses the + // Cook Islands the same way. ClubLog still knows both. + // + // It only speaks when it HAS an answer (E6, TO5A and their like are not + // in its prefix table at all), and never over an exact "=CALLSIGN" entry + // in cty.dat: that is somebody having looked at this very callsign, and + // a prefix rule does not overrule it. + if pe, pok := a.clublog.ResolvePrefix(q.Callsign, date); pok { + ctyExact := false + if m, mok := a.dxcc.Lookup(q.Callsign); mok { + ctyExact = m.Exact + } + if !ctyExact { e, ok = pe, true } } diff --git a/changelog.json b/changelog.json index 427ac37..b9b094f 100644 --- a/changelog.json +++ b/changelog.json @@ -1,4 +1,14 @@ [ + { + "version": "0.27.11", + "date": "", + "en": [ + "Country resolution with the ClubLog file enabled: retired prefixes are recognised again. cty.dat describes the world as it is TODAY — Niue moved to E6, so ZK2 reverted to New Zealand there, and every ZK2 contact ever made was silently relabelled New Zealand, taking a confirmed entity out of the operator’s DXCC with it. ZK1 lost the Cook Islands the same way. ClubLog’s prefix table still knows both and is now consulted whenever no per-callsign exception applies, instead of only for callsigns that happened to have one. It never overrules an exact “=CALLSIGN” entry in cty.dat, and where it has no answer cty.dat still decides. Reprocess an affected import with Update from ClubLog." + ], + "fr": [ + "Résolution des entités avec le fichier ClubLog activé : les préfixes retirés sont de nouveau reconnus. cty.dat décrit le monde tel qu’il est AUJOURD’HUI — Niue est passée en E6, donc ZK2 y est revenu à la Nouvelle-Zélande, et tous les contacts ZK2 jamais faits étaient silencieusement réétiquetés Nouvelle-Zélande, emportant une entité confirmée hors du DXCC de l’opérateur. ZK1 perdait les Cook du Sud de la même façon. La table de préfixes ClubLog connaît toujours les deux : elle est désormais consultée dès qu’aucune exception par indicatif ne s’applique, et non plus seulement pour les indicatifs qui en avaient une. Elle ne prime jamais sur une entrée exacte « =INDICATIF » de cty.dat, et là où elle n’a pas de réponse c’est cty.dat qui tranche. Repassez un import concerné par « Mettre à jour depuis ClubLog »." + ] + }, { "version": "0.27.10", "date": "", diff --git a/clublogprefix_test.go b/clublogprefix_test.go new file mode 100644 index 0000000..b6f6ee4 --- /dev/null +++ b/clublogprefix_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + "path/filepath" + "testing" + "time" + + "hamlog/internal/clublog" + "hamlog/internal/dxcc" + "hamlog/internal/qso" +) + +// Reported from a real import: every ZK2 contact came back as New Zealand and a +// confirmed entity — Niue — disappeared from the operator's DXCC. +// +// cty.dat is not wrong, it is CURRENT: Niue moved to E6, so ZK2 reverted to New +// Zealand there. ClubLog still knows ZK2 was Niue, and knowing that is the whole +// reason for enabling its country file. +func TestClublogPrefixRescuesRetiredPrefixes(t *testing.T) { + dir := filepath.Join("build", "bin", "data") + dm := dxcc.NewManager(dir) + if err := dm.LoadFromDisk(); err != nil { + t.Skipf("cty.dat not available here: %v", err) + } + cm := clublog.NewManager("", dir) + if err := cm.EnsureLoaded(); err != nil { + t.Skipf("ClubLog country file not available here: %v", err) + } + a := &App{ctx: context.Background(), dxcc: dm, clublog: cm} + + when := time.Date(2005, 6, 1, 0, 0, 0, 0, time.UTC) + cases := []struct { + call string + want int // ADIF entity + why string + }{ + {"ZK2KK", 188, "Niue — the reported case"}, + {"ZK1XYZ", 234, "South Cook Islands, lost the same way"}, + } + for _, c := range cases { + q := qso.QSO{Callsign: c.call, QSODate: when} + if !a.applyClublogException(&q, true) { + t.Errorf("%s: ClubLog changed nothing (%s)", c.call, c.why) + continue + } + if q.DXCC == nil || *q.DXCC != c.want { + got := 0 + if q.DXCC != nil { + got = *q.DXCC + } + t.Errorf("%s resolved to %d (%s), want %d — %s", c.call, got, q.Country, c.want, c.why) + } + } + + // A callsign ClubLog's prefix table does not know must be left to cty.dat + // rather than blanked: silence is not an answer. + q := qso.QSO{Callsign: "E6AG", QSODate: when} + if a.applyClublogException(&q, true) && q.DXCC != nil && *q.DXCC != 188 { + t.Errorf("E6AG was moved off Niue, to %d", *q.DXCC) + } +} diff --git a/internal/dxcc/dxcc.go b/internal/dxcc/dxcc.go index 184368c..6e61211 100644 --- a/internal/dxcc/dxcc.go +++ b/internal/dxcc/dxcc.go @@ -40,6 +40,14 @@ type Match struct { Continent string `json:"continent"` Lat float64 `json:"lat"` Lon float64 `json:"lon"` + // Exact marks a hit on cty.dat's "=CALLSIGN" list rather than on a prefix. + // + // The two carry very different authority. A prefix match is a rule of thumb + // about a block of callsigns; an exact entry is somebody having looked at + // THIS callsign and written down where it was. A second country file may + // improve on the first kind and should not be allowed to overrule the + // second. + Exact bool `json:"exact,omitempty"` } type prefixEntry struct { @@ -149,7 +157,9 @@ func (db *DB) Lookup(callsign string) (Match, bool) { return Match{}, false } if e, ok := db.exact[call]; ok { - return materialize(e), true + m := materialize(e) + m.Exact = true + return m, true } // KG4 special case: Guantanamo Bay (DXCC 105) is "KG4" followed by EXACTLY // two characters (KG4XX). "KG4", "KG4X", "KG4XYZ"… are continental USA.