This commit is contained in:
2026-06-07 02:51:00 +02:00
parent 16c04fc12b
commit 8040a37315
11 changed files with 1150 additions and 224 deletions
+21 -18
View File
@@ -1241,44 +1241,47 @@ type EntitySlot struct {
Slots map[string]map[string]struct{} // band → modes worked
}
// EntitySlotMap returns slot data for every QSO, grouping by entity.
// EntitySlotMap returns slot data for every QSO, grouped by DXCC entity NUMBER.
//
// `resolveEntity` maps a callsign to its canonical entity name (we use
// cty.dat for this). When non-nil, the resolved name wins over the
// stored `country` column — that's important because QRZ's "Turkey"
// disagrees with cty.dat's "Asiatic Turkey" and the cluster status
// comparison would otherwise miss past QSOs. When nil, we fall back to
// the stored country (useful for tests).
// keyFor maps a QSO (its callsign + stored DXCC + stored country) to a DXCC
// entity number. Keying by NUMBER — not name — is what makes the cluster
// "new / new-band / new-slot" check robust: QRZ's "Turkey" and cty.dat's
// "Asiatic Turkey" are the same entity (390), and a logged Lord Howe Island
// QSO (stored DXCC 147) matches a VJ2L spot even though cty.dat resolves the
// logged callsign "VK2/SP9FIH" to Australia by prefix. The caller decides the
// precedence (stored DXCC → stored country → cty.dat prefix). keyFor returning
// 0 (unresolvable) skips the QSO.
//
// One DB scan regardless of input size. Cheap to call per cluster batch.
func (r *Repo) EntitySlotMap(ctx context.Context, resolveEntity func(callsign string) string) (map[string]*EntitySlot, error) {
func (r *Repo) EntitySlotMap(ctx context.Context, keyFor func(call string, storedDXCC int, country string) int) (map[int]*EntitySlot, error) {
rows, err := r.db.QueryContext(ctx,
`SELECT callsign, lower(coalesce(country,'')), lower(band), upper(mode) FROM qso
`SELECT callsign, coalesce(dxcc,0), lower(coalesce(country,'')), lower(band), upper(mode) FROM qso
WHERE band IS NOT NULL AND band != ''
AND mode IS NOT NULL AND mode != ''`)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[string]*EntitySlot, 256)
out := make(map[int]*EntitySlot, 256)
for rows.Next() {
var call, country, band, mode string
if err := rows.Scan(&call, &country, &band, &mode); err != nil {
var storedDXCC int
if err := rows.Scan(&call, &storedDXCC, &country, &band, &mode); err != nil {
return nil, err
}
key := country
if resolveEntity != nil {
if name := strings.ToLower(strings.TrimSpace(resolveEntity(call))); name != "" {
key = name
}
key := 0
if keyFor != nil {
key = keyFor(call, storedDXCC, country)
} else {
key = storedDXCC
}
if key == "" {
if key == 0 {
continue
}
e, ok := out[key]
if !ok {
e = &EntitySlot{
Country: key,
Country: country,
Bands: make(map[string]struct{}),
Slots: make(map[string]map[string]struct{}),
}