Files
OpsLog/internal/dxcc/dxcc_test.go
T
2026-06-06 01:21:24 +02:00

218 lines
6.6 KiB
Go

package dxcc
import (
"strings"
"testing"
)
const sampleCty = `Sov Mil Order of Malta: 15: 28: EU: 41.90: -12.43: -1.0: 1A:
1A;
Monaco: 14: 27: EU: 43.73: -7.40: -1.0: 3A:
3A;
France: 14: 27: EU: 46.00: -2.00: -1.0: F:
F,HW,HX,HY,TH,TM,TO,TP,TQ,TV,TX;
Germany: 14: 28: EU: 51.00: -10.00: -1.0: DL:
DA,DB,DC,DD,DE,DF,DG,DH,DI,DJ,DK,DL,DM,DN,DO,DP,DQ,DR;
United States: 05: 08: NA: 37.53: 91.67: 5.0: K:
=W1AW(5)[7],K,N,W,AA,AB,AC,AD,AE,AF,AG,AH,AI,AJ,AK;
`
func TestLookup(t *testing.T) {
db, err := Load(strings.NewReader(sampleCty))
if err != nil {
t.Fatalf("load: %v", err)
}
cases := []struct {
call string
wantEnt string
}{
{"F4NIE", "France"},
{"F4BPO", "France"},
{"F4BPO/P", "France"},
{"DL/F4NIE", "Germany"},
{"DL5XYZ", "Germany"},
{"K1ABC", "United States"},
{"N0CALL", "United States"},
{"3A2MD", "Monaco"},
{"W1AW", "United States"}, // exact match wins
}
for _, c := range cases {
m, ok := db.Lookup(c.call)
if !ok {
t.Errorf("%s: no match", c.call)
continue
}
if m.Entity.Name != c.wantEnt {
t.Errorf("%s: got %q, want %q", c.call, m.Entity.Name, c.wantEnt)
}
}
// W1AW exact match has CQ override 5 and ITU override 7.
m, _ := db.Lookup("W1AW")
if m.CQZone != 5 || m.ITUZone != 7 {
t.Errorf("W1AW overrides: got CQ=%d ITU=%d, want 5/7", m.CQZone, m.ITUZone)
}
}
// A "/N" call-area suffix can change the DXCC entity: HD5MW/8 re-homes to the
// HD8 area (Galápagos), not the base call's Ecuador.
func TestCallAreaSuffix(t *testing.T) {
const cty = `Ecuador: 10: 12: SA: -1.40: 78.40: 5.0: HC:
HC,HD;
Galapagos Islands: 10: 12: SA: 0.00: 91.00: 6.0: HC8:
HC8,HD8;
`
db, err := Load(strings.NewReader(cty))
if err != nil {
t.Fatalf("load: %v", err)
}
cases := map[string]string{
"HD5MW": "Ecuador",
"HD5MW/8": "Galapagos Islands",
"HC2AO": "Ecuador",
"HD8M": "Galapagos Islands",
"HC1WW/8": "Galapagos Islands",
}
for call, want := range cases {
m, ok := db.Lookup(call)
if !ok {
t.Errorf("%s: no match", call)
continue
}
if m.Entity.Name != want {
t.Errorf("%s: got %q, want %q", call, m.Entity.Name, want)
}
}
}
// US/VK call-district zone refinement (W6 = CQ3/ITU6, not the entity default).
func TestZoneByCallDistrict(t *testing.T) {
cases := []struct {
adif int
call string
cqz, ituz int
ok bool
}{
{291, "W6XYZ", 3, 6, true},
{291, "K7AB", 3, 6, true},
{291, "W4ABC", 5, 8, true},
{291, "N0CALL", 4, 7, true},
{291, "AA5XX", 4, 7, true},
{150, "VK6AA", 29, 58, true}, // West Australia
{150, "VK3XY", 30, 59, true}, // Victoria
{230, "DL1ABC", 0, 0, false}, // Germany: no district rule
{291, "WABC", 0, 0, false}, // no digit
}
for _, c := range cases {
cqz, ituz, ok := ZoneByCallDistrict(c.adif, c.call)
if ok != c.ok || (ok && (cqz != c.cqz || ituz != c.ituz)) {
t.Errorf("ZoneByCallDistrict(%d,%q) = %d/%d ok=%v, want %d/%d ok=%v",
c.adif, c.call, cqz, ituz, ok, c.cqz, c.ituz, c.ok)
}
}
}
// KG4 is Guantanamo Bay only with a 2-character suffix (KG4XX); 1- or 3-char
// suffixes (KG4W, KG4ABC) are continental USA. cty.dat carries a bare "KG4"
// prefix, so the resolver must apply the suffix-length rule.
func TestKG4SuffixRule(t *testing.T) {
const cty = `United States: 05: 08: NA: 37.53: 91.67: 5.0: K:
K,N,W;
Guantanamo Bay: 08: 11: NA: 19.92: 75.18: -5.0: KG4:
KG4;
`
db, err := Load(strings.NewReader(cty))
if err != nil {
t.Fatalf("load: %v", err)
}
cases := map[string]string{
"KG4W": "United States", // 1-char suffix
"KG4AA": "Guantanamo Bay", // 2-char suffix
"KG4ABC": "United States", // 3-char suffix
"KG4": "United States", // no suffix
"KG4W/P": "United States", // modifier stripped, still 1-char
}
for call, want := range cases {
m, ok := db.Lookup(call)
if !ok {
t.Errorf("%s: no match", call)
continue
}
if m.Entity.Name != want {
t.Errorf("%s: got %q, want %q", call, m.Entity.Name, want)
}
}
}
// cty.dat marks non-DXCC entities (Sicily *IT9, African Italy *IG9) with a
// leading '*'; the parser must fold those into their parent DXCC entity
// "Italy" while leaving real DXCC entities — Sardinia (IS0), no '*' — alone.
func TestCanonicalEntityNames(t *testing.T) {
const cty = `Italy: 15: 28: EU: 42.82: -12.58: -1.0: I:
I,IK,IZ;
African Italy: 33: 37: AF: 35.67: -12.67: -1.0: *IG9:
IG9,IH9;
Sardinia: 15: 28: EU: 40.15: -9.27: -1.0: IS0:
IM0,IS,IW0U,IW0V;
Sicily: 15: 28: EU: 37.50: -14.00: -1.0: *IT9:
IT9,IW9;
`
db, err := Load(strings.NewReader(cty))
if err != nil {
t.Fatalf("load: %v", err)
}
cases := map[string]string{
"IW9EZO": "Italy", // Sicily (*IT9) → Italy
"IT9CLY": "Italy",
"IG9A": "Italy", // African Italy (*IG9) → Italy
"IK0ABC": "Italy",
"IS0XYZ": "Sardinia", // real DXCC entity — must stay Sardinia
"IM0ABC": "Sardinia",
}
for call, want := range cases {
m, ok := db.Lookup(call)
if !ok {
t.Errorf("%s: no match", call)
continue
}
if m.Entity.Name != want {
t.Errorf("%s: country = %q, want %q", call, m.Entity.Name, want)
}
}
// African Italy keeps its own zones/continent even though it reports
// Italy as the entity.
if m, _ := db.Lookup("IG9A"); m.CQZone != 33 || m.Continent != "AF" {
t.Errorf("IG9A: got CQ=%d cont=%s, want 33/AF", m.CQZone, m.Continent)
}
if EntityDXCC("Sicily") != 248 {
t.Errorf("EntityDXCC(Sicily) = %d, want 248 (Italy)", EntityDXCC("Sicily"))
}
if EntityDXCC("Sardinia") != 225 {
t.Errorf("EntityDXCC(Sardinia) = %d, want 225", EntityDXCC("Sardinia"))
}
}
func TestNormalize(t *testing.T) {
cases := map[string]string{
"F4BPO": "F4BPO",
"f4bpo": "F4BPO",
" F4BPO ": "F4BPO",
"F4BPO/P": "F4BPO",
"F4BPO/MM": "", // maritime mobile → no DXCC entity
"F4BPO/AM": "", // aeronautical mobile → no DXCC entity
"F4BPO/M": "F4BPO", // plain mobile keeps the home entity
"F4BPO/5": "F5BPO", // "/5" re-homes to call area 5
"HD5MW/8": "HD8MW", // "/8" → Galápagos call area (HD8)
"DL/F4BPO": "DL",
"MM/KA9P": "MM", // leading MM = Scotland operating prefix
"MM/LY3X/P": "MM",
"F4BPO/W6": "W6",
"VK9/F4BPO": "VK9",
}
for in, want := range cases {
if got := normalizeCallsign(in); got != want {
t.Errorf("normalize(%q) = %q, want %q", in, got, want)
}
}
}