package dxcc import ( "sort" "strings" ) // The dxccByName table itself lives in dxcc_names_gen.go (generated by joining // cty.dat to the authoritative ARRL/ADIF entity list). cty.dat doesn't carry // the ADIF DXCC number, so we map its entity names → numbers here to stamp // MY_DXCC / DXCC at log time without a network round-trip. // EntityDXCC returns the ADIF DXCC entity number for the given cty.dat entity // name, or 0 when unknown (callers should then leave the field empty rather // than guess). Case-insensitive and whitespace-tolerant. func EntityDXCC(name string) int { if name == "" { return 0 } // Fast path: exact (lower-cased) match against the cty.dat names. if n := dxccByName[strings.ToLower(strings.TrimSpace(name))]; n != 0 { return n } // Fallback: canonicalise so abbreviation/spelling differences still match // (e.g. an ADIF import that wrote "Lord Howe I." instead of cty.dat's // "Lord Howe Island"). if n := dxccByCanon[canonEntity(name)]; n != 0 { return n } // Last resort: cty.dat pseudo-entities (Sicily, African Italy) report a // parent DXCC entity for the number. if c := CanonicalEntityName(name); !strings.EqualFold(c, name) { return dxccByName[strings.ToLower(strings.TrimSpace(c))] } return 0 } // nameByDXCC reverses dxccByName (number → a representative entity name), // built once. When several names share a number, the longest (usually the most // complete) wins. Names are Title-cased for display. var nameByDXCC = func() map[int]string { m := make(map[int]string, len(dxccByName)) for name, num := range dxccByName { if cur, ok := m[num]; !ok || len(name) > len(cur) { m[num] = name } } return m }() // NameForDXCC returns a display name for an ADIF DXCC entity number, or "" if // unknown. func NameForDXCC(n int) string { name, ok := nameByDXCC[n] if !ok { return "" } return strings.Title(name) //nolint:staticcheck // ASCII entity names } // ZoneByCallDistrict returns the CQ and ITU zone for a callsign in a country // that is split across zones by call district (USA, Australia…). cty.dat and // ClubLog's cty.xml only carry one zone per entity, so loggers apply this // district→zone convention to get e.g. W6 = CQ3/ITU6 instead of the entity // default CQ5/ITU8. ok=false means no district rule applies (use the entity // default). The district is the first digit of the callsign. func ZoneByCallDistrict(adif int, call string) (cqz, ituz int, ok bool) { d := districtDigit(call) if d < 0 { return 0, 0, false } switch adif { case 291: // United States — standard district defaults (state-level // exceptions exist, but this matches what Log4OM/DXKeeper default to). if z, o := usDistrictZones[d]; o { return z[0], z[1], true } case 150: // Australia — VK6/VK8 (west/north) are CQ29/ITU58, rest CQ30/ITU59. if d == 6 || d == 8 { return 29, 58, true } return 30, 59, true } return 0, 0, false } // usDistrictZones maps a US call district digit to its {CQ, ITU} zone. var usDistrictZones = map[int][2]int{ 0: {4, 7}, 1: {5, 8}, 2: {5, 8}, 3: {5, 8}, 4: {5, 8}, 5: {4, 7}, 6: {3, 6}, 7: {3, 6}, 8: {5, 8}, 9: {4, 8}, } // firstDigit returns the first 0-9 digit in a callsign, or -1 if none. func firstDigit(call string) int { for i := 0; i < len(call); i++ { if call[i] >= '0' && call[i] <= '9' { return int(call[i] - '0') } } return -1 } // districtDigit returns the effective call-area digit: a trailing "/N" (single // digit) re-homes the call to area N (W6ABC/7 → area 7), otherwise the first // digit of the call. func districtDigit(call string) int { if i := strings.LastIndex(call, "/"); i >= 0 && i == len(call)-2 { if c := call[len(call)-1]; c >= '0' && c <= '9' { return int(c - '0') } } return firstDigit(call) } // EntityNumberName pairs a DXCC entity number with its display name. type EntityNumberName struct { Num int Name string } // AllEntities returns every known DXCC entity (number + display name), sorted by // number. Used to seed the DXCC award's reference list. func AllEntities() []EntityNumberName { out := make([]EntityNumberName, 0, len(nameByDXCC)) for num := range nameByDXCC { out = append(out, EntityNumberName{Num: num, Name: NameForDXCC(num)}) } sort.Slice(out, func(i, j int) bool { return out[i].Num < out[j].Num }) return out } // dxccByCanon is dxccByName re-keyed by the canonical entity form, built once. var dxccByCanon = func() map[string]int { m := make(map[string]int, len(dxccByName)) for name, num := range dxccByName { m[canonEntity(name)] = num } return m }() // canonEntity reduces an entity name to a canonical token stream, expanding the // common abbreviations that differ between naming conventions and normalising // punctuation / "&". func canonEntity(s string) string { s = strings.ToLower(strings.TrimSpace(s)) s = strings.ReplaceAll(s, "&", " and ") fields := strings.FieldsFunc(s, func(r rune) bool { switch r { case ' ', '.', ',', '-', '\'', '(', ')', '/': return true } return false }) for i, w := range fields { switch w { case "i": fields[i] = "island" case "is": fields[i] = "islands" case "st": fields[i] = "saint" case "mt": fields[i] = "mount" case "rep": fields[i] = "republic" case "dem": fields[i] = "democratic" case "fed": fields[i] = "federal" } } return strings.Join(fields, " ") } // ctyEntityAliases maps cty.dat's non-DXCC pseudo-entities — the CQ-zone / // WAE splits AD1C lists separately — onto the ARRL DXCC entity they belong // to. cty.dat reports e.g. "Sicily" so contesters get the right zones, but // for DXCC (and the COUNTRY field) they are Italy. var ctyEntityAliases = map[string]string{ "sicily": "Italy", "african italy": "Italy", // NB: Sardinia is intentionally absent — it's a real DXCC entity. } // CanonicalEntityName normalises a cty.dat entity name to its ARRL DXCC // entity. Names that already are DXCC entities pass through unchanged. func CanonicalEntityName(name string) string { if c, ok := ctyEntityAliases[strings.ToLower(strings.TrimSpace(name))]; ok { return c } return name }