From a8bca8c3165520c772ed48411eb7822b967dd87a Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Mon, 17 Aug 2026 02:26:52 +0200 Subject: [PATCH] feat(awards): an entity that IS one island group fills its own IOTA reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QRZ carries only for the operators who filled it in, and most have not. But for 99 entities the reference follows from the entity alone — a station in Ascension is on AF-003 whatever its callbook record says — and the entity is known for every callsign from cty.dat, with no callbook at all. So this reaches the operator with no QRZ subscription and the station that has never touched a callbook, and it lands before the contact is logged, which is when a reference is worth having. Filled ONLY when the callbook gave nothing: an operator who typed a reference knows something a table cannot — an IOTA-heavy entity, a rare island being activated — and that still wins, as does one picked by hand on the entry. The table is the IOTA programme's own dxcc_matches_one_iota.json, which lists exactly the entities that map to ONE reference. France is not among them: a French station is usually on the mainland and on no island, and guessing would put a reference on hundreds of contacts that earn none. Held as a table rather than a download — 3.5 kB that changes when an entity appears, so fetching it daily would buy nothing and would fail exactly where a portable station usually is. The header says where it came from and how to refresh it. Follows 747c2b9 and eab11db, which read the callbook's own tag. --- app.go | 13 +++ changelog.json | 8 +- internal/awardref/dxcciota.go | 129 +++++++++++++++++++++++++++++ internal/awardref/dxcciota_test.go | 51 ++++++++++++ 4 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 internal/awardref/dxcciota.go create mode 100644 internal/awardref/dxcciota_test.go diff --git a/app.go b/app.go index c3d500d..3c250d4 100644 --- a/app.go +++ b/app.go @@ -7237,6 +7237,19 @@ func (a *App) lookupCallsign(callsign string, force bool, qsoDate string) (looku } } a.enrichFromULS(&r, callsign) + // An entity that IS a single island group answers for itself. QRZ carries + // only for the operators who filled it in, and most have not; but a + // station in Ascension is on AF-003 whatever its callbook record says, and + // the entity is known for every callsign from cty.dat alone. + // + // So this reaches the operator with no QRZ subscription, and the station + // that has never touched a callbook — and it is right when the two + // disagree only in the sense that the callbook wins: filled ONLY when the + // callbook gave nothing, because an operator who typed their own reference + // knows something a table cannot (an IOTA-heavy entity, a rare island). + if r.IOTA == "" && r.DXCC != 0 { + r.IOTA = awardref.IOTAForDXCC(r.DXCC) + } // Custom outbound rows bound to a lookup. After the enrichment, so a // template sees the same grid and county the entry panel is about to show — // two answers for one callsign is how a rotator ends up pointed elsewhere diff --git a/changelog.json b/changelog.json index 7658c12..dc2db35 100644 --- a/changelog.json +++ b/changelog.json @@ -2,8 +2,12 @@ { "version": "0.25.8", "date": "", - "en": [], - "fr": [] + "en": [ + "An entity that is a single island group now fills the IOTA reference on its own — no callbook subscription needed." + ], + "fr": [ + "Une entité qui est un seul groupe d’îles remplit désormais la référence IOTA toute seule, sans abonnement callbook." + ] }, { "version": "0.25.7", diff --git a/internal/awardref/dxcciota.go b/internal/awardref/dxcciota.go new file mode 100644 index 0000000..841faa4 --- /dev/null +++ b/internal/awardref/dxcciota.go @@ -0,0 +1,129 @@ +package awardref + +// One-IOTA DXCC entities: a table of the entities that ARE a single IOTA group. +// +// WHY IT EXISTS. QRZ.com carries for an operator who fills it in, and +// most do not. But for a great many entities the island reference follows from +// the entity alone — a station in Ascension Island is on AF-003, there is +// nothing else it could be — and the entity is known for every callsign, from +// cty.dat, without any callbook at all. So the reference can be filled for an +// operator with no QRZ subscription, on a station that has never touched a +// callbook, before the contact is logged. +// +// Only entities that map to EXACTLY ONE reference are here. France is not: a +// French station is usually on the mainland and on no island at all, and +// guessing would put a reference on hundreds of contacts that earn none. +// +// SOURCE: dxcc_matches_one_iota.json from the IOTA programme +// (www.iota-world.org/islands-on-the-air/downloads/), fetched 2026-08-17, +// 99 entities. It changes only when an entity appears or IOTA re-maps one, so +// it is a table here rather than a download: it then works offline, which is +// where a portable station usually is. To refresh, fetch that file again and +// re-emit this map, sorted by entity number. +// +// Entity names are comments only, joined from internal/dxcc for readability. +var iotaByDXCC = map[int]string{ + 5: "EU-002", // Aland Islands + 10: "AF-002", // Amsterdam & St. Paul Is. + 12: "NA-022", // Anguilla + 17: "NA-020", // Aves Island + 20: "OC-089", // Baker & Howland Islands + 21: "EU-004", // Balearic Islands + 24: "AN-002", // Bouvet + 29: "AF-004", // Canary Islands + 34: "OC-038", // Chatham Islands + 35: "OC-002", // Christmas Island + 36: "NA-011", // Clipperton Island + 37: "NA-012", // Cocos Island + 38: "OC-003", // Cocos (Keeling) Islands + 41: "AF-008", // Crozet Island + 43: "NA-095", // Desecheo Island + 45: "EU-001", // Dodecanese + 62: "NA-021", // Barbados + 64: "NA-005", // Bermuda + 65: "NA-023", // British Virgin Islands + 69: "NA-016", // Cayman Islands + 71: "SA-004", // Galapagos Islands + 82: "NA-097", // Jamaica + 84: "NA-107", // Martinique + 91: "SA-036", // Aruba + 94: "NA-100", // Antigua & Barbuda + 95: "NA-101", // Dominica + 96: "NA-103", // Montserrat + 97: "NA-108", // St. Lucia + 99: "AF-011", // Glorioso Islands + 103: "OC-026", // Guam + 105: "NA-015", // Guantanamo Bay + 106: "EU-114", // Guernsey + 111: "AN-003", // Heard Island + 114: "EU-116", // Isle Of Man + 118: "EU-022", // Jan Mayen + 123: "OC-023", // Johnston Island + 131: "AF-048", // Kerguelen Islands + 133: "OC-039", // Kermadec Islands + 138: "OC-020", // Kure Island + 141: "SA-002", // Falkland Islands + 147: "OC-004", // Lord Howe Island + 153: "AN-005", // Macquarie Island + 157: "OC-031", // Nauru + 159: "AS-013", // Maldives + 161: "SA-007", // Malpelo Island + 165: "AF-049", // Mauritius + 166: "OC-086", // Mariana Islands + 167: "EU-053", // Market Reef + 169: "AF-027", // Mayotte + 171: "OC-072", // Mellish Reef + 174: "OC-030", // Midway Island + 177: "OC-073", // Minami Torishima + 182: "NA-098", // Navassa Island + 188: "OC-040", // Niue + 189: "OC-005", // Norfolk Island + 190: "OC-097", // Samoa + 195: "AF-039", // Annobon Island + 199: "AN-004", // Peter 1 Island + 201: "AF-021", // Pr. Edward & Marion Is. + 205: "AF-003", // Ascension Island + 207: "AF-017", // Rodriguez Island + 211: "NA-063", // Sable Island + 217: "SA-013", // San Felix & San Ambrosio + 222: "EU-018", // Faroe Islands + 238: "AN-008", // South Orkney Islands + 240: "AN-009", // South Sandwich Islands + 241: "AN-010", // South Shetland Islands + 247: "AS-051", // Spratly Islands + 249: "NA-104", // St. Kitts & Nevis + 250: "AF-022", // St. Helena + 252: "NA-094", // St. Paul Island + 253: "SA-014", // St. Peter & St. Paul + 257: "EU-023", // Malta + 270: "OC-048", // Tokelau Islands + 273: "SA-010", // Trindade & Martim Vaz + 276: "AF-031", // Tromelin Island + 277: "NA-032", // St. Pierre & Miquelon + 282: "OC-015", // Tuvalu + 283: "AS-004", // Uk Base Areas On Cyprus + 285: "NA-106", // Us Virgin Islands + 297: "OC-053", // Wake Island + 301: "OC-017", // Western Kiribati + 303: "OC-007", // Willis Island + 381: "AS-019", // Singapore + 411: "AF-007", // Comoros + 453: "AF-016", // Reunion Island + 460: "OC-060", // Rotuma Island + 489: "OC-112", // Conway Reef + 490: "OC-018", // Banaba Island + 505: "AS-110", // Pratas Island + 506: "AS-116", // Scarborough Reef + 509: "OC-027", // Marquesas Islands + 512: "OC-176", // Chesterfield Islands + 513: "OC-182", // Ducie Island + 515: "OC-200", // Swains Island + 516: "NA-146", // St. Barthelemy + 517: "SA-099", // Curacao + 519: "NA-145", // Saba & St. Eustatius + 520: "SA-006", // Bonaire +} + +// IOTAForDXCC returns the island reference of a DXCC entity that is a single +// IOTA group, or "" for an entity that holds several islands or none. +func IOTAForDXCC(dxcc int) string { return iotaByDXCC[dxcc] } diff --git a/internal/awardref/dxcciota_test.go b/internal/awardref/dxcciota_test.go new file mode 100644 index 0000000..7a1e4c5 --- /dev/null +++ b/internal/awardref/dxcciota_test.go @@ -0,0 +1,51 @@ +package awardref + +import ( + "regexp" + "testing" +) + +// The entities an operator meets: an island group that is its own DXCC, and a +// mainland country that is not. +func TestIOTAForDXCC(t *testing.T) { + cases := []struct { + dxcc int + want string + why string + }{ + {205, "AF-003", "Ascension Island is one island and one reference"}, + {5, "EU-002", "Aland Islands"}, + {12, "NA-022", "Anguilla"}, + {24, "AN-002", "Bouvet — the entity whose entry is checked most often and worked least"}, + // France holds hundreds of islands and, far more to the point, a + // mainland. Filling a reference here would earn nothing and would put + // one on nearly every European contact in the log. + {227, "", "France is not one IOTA"}, + {291, "", "the United States is not one IOTA"}, + {0, "", "no entity resolved"}, + {99999, "", "not an entity at all"}, + } + for _, c := range cases { + if got := IOTAForDXCC(c.dxcc); got != c.want { + t.Errorf("IOTAForDXCC(%d) = %q, want %q — %s", c.dxcc, got, c.want, c.why) + } + } +} + +// Every reference in the table must be a well-formed IOTA reference, because +// one that is not would be written onto a contact's award references and count +// for nothing — and would then have to be found by hand, one QSO at a time. +func TestEveryOneIOTAEntryIsWellFormed(t *testing.T) { + ref := regexp.MustCompile(`^(AF|AN|AS|EU|NA|OC|SA)-\d{3}$`) + if len(iotaByDXCC) < 50 { + t.Fatalf("the table holds %d entities — it has been truncated", len(iotaByDXCC)) + } + for dxcc, r := range iotaByDXCC { + if dxcc <= 0 { + t.Errorf("entity number %d is not a DXCC entity", dxcc) + } + if !ref.MatchString(r) { + t.Errorf("entity %d maps to %q, which is not an IOTA reference", dxcc, r) + } + } +}