This commit is contained in:
2026-06-07 12:50:04 +02:00
parent eb64b8f2f9
commit 9189f54df5
7 changed files with 126 additions and 9 deletions
+21
View File
@@ -0,0 +1,21 @@
package lookup
import "testing"
func TestHomeCall(t *testing.T) {
cases := map[string]string{
"JW/OR1A": "OR1A", // portable prefix → home call
"DL/F4NIE": "F4NIE", // prefix/home
"F4BPO/P": "F4BPO", // suffix dropped
"F4BPO/9": "F4BPO", // call-area digit dropped
"VP8/F4BPO": "F4BPO", // both have digits → longest wins
"MM/KA9P": "KA9P", // Scotland prefix + US home
"OH2BH": "OH2BH", // no slash → unchanged
"3D2/RW3RN": "RW3RN",
}
for in, want := range cases {
if got := homeCall(in); got != want {
t.Errorf("homeCall(%q) = %q, want %q", in, got, want)
}
}
}
+49
View File
@@ -125,6 +125,32 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
lastErr = fmt.Errorf("%s: %w", p.Name(), err)
}
// Portable / slashed call not found under its full form: the operator's
// record lives under the HOME call (JW/OR1A → OR1A, DL/F4NIE → F4NIE). Look
// THAT up for the name/QTH/QSL info, then overwrite the location-determining
// fields with the SLASHED call's entity (JW = Svalbard, not OR1A's Belgium).
if home := homeCall(call); home != "" && home != call {
for _, p := range providers {
r, err := p.Lookup(ctx, home)
if err != nil {
continue
}
r.Callsign = call
r.Source = p.Name()
r.FetchedAt = time.Now().UTC()
// The home record's location is the operator's HOME, not where they
// are portable now — clear it so cty.dat fills the real entity.
r.Country, r.Continent = "", ""
r.CQZ, r.ITUZ, r.DXCC = 0, 0, 0
r.Lat, r.Lon = 0, 0
r.Grid, r.State, r.County = "", "", ""
fillFromDXCC(&r, dxcc) // entity/zones/lat-lon from the FULL (slashed) call
normalizeNames(&r)
_ = m.cache.Put(ctx, r)
return r, nil
}
}
// All providers exhausted (not-found or errored). Try the cty.dat
// resolver as a last resort — at least we can hand back country/zones
// even for unknown callsigns. Not cached: a "cty.dat-only" result
@@ -149,6 +175,29 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
return Result{}, lastErr
}
// homeCall extracts the operator's home callsign from a slashed/portable call
// so its provider record (name/QTH/QSL) can be fetched when the full form isn't
// registered: JW/OR1A → OR1A, DL/F4NIE → F4NIE, F4BPO/P → F4BPO, VP8/F4BPO →
// F4BPO. The home call is the "/"-part that looks like a real callsign (has a
// digit, ≥3 chars); the longest such part wins (handles PREFIX/HOMECALL where
// both could qualify, e.g. VP8/F4BPO). Returns "" if none qualifies.
func homeCall(call string) string {
if !strings.ContainsRune(call, '/') {
return call
}
best := ""
for _, p := range strings.Split(call, "/") {
p = strings.TrimSpace(p)
if len(p) < 3 || !strings.ContainsAny(p, "0123456789") {
continue // a prefix (JW, DL) or a suffix (P, M, MM, QRP, a digit)
}
if len(p) > len(best) {
best = p
}
}
return best
}
// normalizeNames title-cases the human-readable text fields so a QRZ/HamQTH
// reply in ALL CAPS ("NOEL CHENAVARD", "VETRAZ-MONTHOUX") is stored and shown
// consistently ("Noel Chenavard", "Vetraz-Monthoux"). State/zones/grid are