Files
OpsLog/cmd/cntygen/main.go
T
rouggy 30143b01bf fix(counties): make the cluster and the entry panel agree on a US county
An operator asked why K1SEI showed "Middlesex" in Info (F2) and "Lower
Connecticut River Valley" in the cluster. Two sources: the entry panel has
a callbook answer, a spot carries only a callsign so the cluster derives one
from the FCC licence ZIP through GeoNames — and GeoNames has followed the
Census in replacing Connecticut's counties with the 2022 planning regions.
No award, callbook or log uses those, so every CT station matched nothing:
new county for ever, and counting toward nothing.

Measured against a full ULS import (1 556 444 US callsigns), 23 223 resolved
to a name the USA-CA reference does not contain. Three causes, three fixes:

  - Spelling. "City and County of San Francisco", "Baltimore (city)",
    "Nome (CA)", plus counties renamed since the award list was drawn
    (Kusilvak, Oglala Lakota, the Valdez-Cordova split) and Alaska's four
    "X City and Borough", whose reference codes read "JUNEAUCITYAND" because
    the county-type suffix strip eats the wrong end. Normalised in
    award.USCountyKey, which both sides already go through. 7 515 callsigns,
    no re-download needed.

  - Doña Ana, NM shipped into the reference as "NM/DO̱AANA" — mangled by a
    non-UTF-8 CSV line, a code nothing could ever produce, so that county was
    unwinnable and silent about it. Row repaired, cntygen now refuses such a
    line, and a test makes every one of the 3 102 references reproduce its own
    code from its own name.

  - Connecticut. A planning region is drawn from towns in several counties, so
    no name maps to a name — only the ZIP can resolve it. cmd/ctzipgen builds
    the table from the Census 2020 crosswalk, filling PO-box-only ZIPs from the
    nearest resolved centroid; all 11 ZIPs GeoNames still labels with a real
    county agree with the result. 15 037 callsigns, applied at import, so the
    store now carries a rules version and Settings says when a re-download is
    needed.

Alignment itself is the last piece: a spot now shows the county the station is
logged with when we have one, and falls back to the ZIP-derived county only for
stations never worked.
2026-08-14 00:42:27 +02:00

111 lines
3.3 KiB
Go

package main
// One-shot generator: reads the FIPS county CSV and emits
// internal/awardref/uscounties_gen.go. Not part of the build.
import (
"bufio"
"fmt"
"os"
"sort"
"strings"
"unicode/utf8"
"hamlog/internal/award"
)
// dxccForState maps the two US "states" that are separate DXCC entities.
func dxccForState(st string) int {
switch st {
case "AK":
return 6
case "HI":
return 110
default:
return 291
}
}
// territories we exclude entirely — plus DC, which USA-CA does not count as a
// county.
var skipState = map[string]bool{"PR": true, "GU": true, "VI": true, "AS": true, "MP": true, "NA": true, "DC": true}
// excludedUSACA reports county-equivalents that the CQ USA-CA award does NOT
// count as counties: the independent cities of Virginia and Carson City (NV).
// (Baltimore MD and St. Louis MO ARE counted, so they are kept.) A contact in
// one of these counts toward a bordering county under the award rules.
func excludedUSACA(state, name string) bool {
n := strings.ToLower(strings.TrimSpace(name))
switch state {
case "VA":
return strings.HasSuffix(n, " city")
case "NV":
return n == "carson city"
}
return false
}
func main() {
f, err := os.Open(os.Args[1])
if err != nil {
panic(err)
}
defer f.Close()
type row struct{ code, name string; dxcc int }
var rows []row
seen := map[string]bool{}
sc := bufio.NewScanner(f)
sc.Scan() // header
for sc.Scan() {
line := sc.Text()
// The FIPS CSV is not reliably UTF-8, and the one county whose name is
// not ASCII (Doña Ana, NM) came through mangled once already — it landed
// in the reference as "NM/DO̱AANA", a code no log could ever match,
// silently costing that county for every operator. Refuse the row rather
// than emit a broken one.
if !utf8.ValidString(line) {
fmt.Fprintf(os.Stderr, "cntygen: skipping non-UTF-8 line: %q\n", line)
continue
}
parts := strings.SplitN(line, ",", 3)
if len(parts) < 3 {
continue
}
name := strings.TrimSpace(parts[1])
st := strings.TrimSpace(parts[2])
if skipState[st] || len(st) != 2 || strings.EqualFold(name, "UNITED STATES") {
continue
}
if excludedUSACA(st, name) {
continue
}
// State header rows have an all-caps state NAME and state code "NA"
// (already skipped). County rows have a 2-letter state code.
code := award.USCountyKey(st, name)
if code == "" || seen[code] {
continue
}
seen[code] = true
rows = append(rows, row{code: code, name: name + ", " + st, dxcc: dxccForState(st)})
}
sort.Slice(rows, func(i, j int) bool { return rows[i].code < rows[j].code })
var b strings.Builder
b.WriteString("// Code generated by cmd/cntygen from FIPS county data. DO NOT EDIT.\n")
b.WriteString("package awardref\n\n")
b.WriteString("// usCounties is the US Counties (USA-CA) reference list: one entry per county\n")
b.WriteString("// in the 50 states, keyed by the canonical \"STATE,COUNTY\" match code that\n")
b.WriteString("// award.usCountyKey produces from a QSO's state + cnty fields.\n")
fmt.Fprintf(&b, "func usCounties() []Ref {\n\treturn []Ref{\n")
for _, r := range rows {
fmt.Fprintf(&b, "\t\tref(%q, %q, %d),\n", r.code, r.name, r.dxcc)
}
b.WriteString("\t}\n}\n")
if err := os.WriteFile(os.Args[2], []byte(b.String()), 0o644); err != nil {
panic(err)
}
fmt.Printf("wrote %d counties to %s\n", len(rows), os.Args[2])
}