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.
119 lines
4.6 KiB
Go
119 lines
4.6 KiB
Go
package awardref
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"unicode/utf8"
|
|
|
|
"hamlog/internal/award"
|
|
)
|
|
|
|
// TestUSCountyRefsSelfConsistent is the guard that was missing when the Doña Ana
|
|
// row shipped mangled: its code was "NM/DO̱AANA", which no log, no callbook and
|
|
// no county database could ever produce, so that county was unwinnable and
|
|
// nothing said so.
|
|
//
|
|
// Every reference must reproduce its own code from its own display name through
|
|
// award.USCountyKey — the same function cmd/cntygen used to build it. That ties
|
|
// the generated file to the matcher: change the normalisation rules and this
|
|
// fails the moment one of the 3 102 references stops matching itself.
|
|
func TestUSCountyRefsSelfConsistent(t *testing.T) {
|
|
refs := usCounties()
|
|
if len(refs) < 3000 {
|
|
t.Fatalf("only %d counties — the generated list looks truncated", len(refs))
|
|
}
|
|
for _, r := range refs {
|
|
if !utf8.ValidString(r.Name) || !utf8.ValidString(r.Code) {
|
|
t.Errorf("%s (%q): not valid UTF-8", r.Code, r.Name)
|
|
continue
|
|
}
|
|
// Name is "County Name, ST".
|
|
i := strings.LastIndex(r.Name, ", ")
|
|
if i < 0 {
|
|
t.Errorf("%s: name %q is not \"County, ST\"", r.Code, r.Name)
|
|
continue
|
|
}
|
|
name, st := r.Name[:i], r.Name[i+2:]
|
|
if got := award.USCountyKey(st, name); got != r.Code {
|
|
t.Errorf("USCountyKey(%q, %q) = %q, want %q", st, name, got, r.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUSCountyRefsUnique catches a normalisation rule loose enough to collapse
|
|
// two real counties onto one key — the failure mode that would silently merge
|
|
// them in every award total.
|
|
func TestUSCountyRefsUnique(t *testing.T) {
|
|
seen := map[string]string{}
|
|
for _, r := range usCounties() {
|
|
if prev, dup := seen[r.Code]; dup {
|
|
t.Errorf("code %s is shared by %q and %q", r.Code, prev, r.Name)
|
|
continue
|
|
}
|
|
seen[r.Code] = r.Name
|
|
}
|
|
}
|
|
|
|
// TestUSCountyResolverShapes pins the spellings OpsLog's own offline resolver
|
|
// produces (GeoNames, via internal/uls) against the reference. These are the
|
|
// real strings that were failing to match, with the number of US callsigns each
|
|
// covers in a full FCC ULS import.
|
|
func TestUSCountyResolverShapes(t *testing.T) {
|
|
valid := map[string]bool{}
|
|
for _, r := range usCounties() {
|
|
valid[r.Code] = true
|
|
}
|
|
cases := []struct {
|
|
st, county, want string
|
|
}{
|
|
{"CA", "City and County of San Francisco", "CA/SANFRANCISCO"}, // 4 217 calls
|
|
{"NM", "Doña Ana", "NM/DONAANA"}, // 1 214
|
|
{"NM", "Dona Ana", "NM/DONAANA"}, // what logs hold
|
|
{"MD", "Baltimore (city)", "MD/BALTIMORECITY"}, // 885
|
|
{"MO", "St. Louis (city)", "MO/STLOUISCITY"}, // 574
|
|
{"MO", "St. Louis", "MO/STLOUIS"}, // the county, distinct
|
|
{"AK", "Nome (CA)", "AK/NOME"}, // 252
|
|
{"AK", "Yukon-Koyukuk (CA)", "AK/YUKONKOYUKUK"}, // 84
|
|
{"AK", "City and Borough of Wrangell", "AK/WRANGELLCITYAND"}, // 27
|
|
{"AK", "Juneau", "AK/JUNEAUCITYAND"}, // a log that just says Juneau
|
|
{"AK", "Kusilvak", "AK/WADEHAMPTON"}, // renamed 2015
|
|
{"SD", "Oglala Lakota County", "SD/SHANNON"}, // renamed 2015
|
|
{"AK", "Chugach Census Area", "AK/VALDEZCORDOVA"}, // split 2019
|
|
{"AK", "Copper River Census Area", "AK/VALDEZCORDOVA"},
|
|
{"CO", "City and County of Denver", "CO/DENVER"},
|
|
{"HI", "City and County of Honolulu", "HI/HONOLULU"},
|
|
}
|
|
for _, c := range cases {
|
|
got := award.USCountyKey(c.st, c.county)
|
|
if got != c.want {
|
|
t.Errorf("USCountyKey(%q, %q) = %q, want %q", c.st, c.county, got, c.want)
|
|
continue
|
|
}
|
|
if !valid[got] {
|
|
t.Errorf("USCountyKey(%q, %q) = %q, which is not in the reference list", c.st, c.county, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUSCountyKeepsVirginiaCitiesOut guards the other direction: cmd/cntygen
|
|
// drops Virginia's independent cities and Carson City on purpose, because CQ's
|
|
// USA-CA does not count them. Normalising their names must not smuggle them
|
|
// back in as some neighbouring county.
|
|
func TestUSCountyKeepsVirginiaCitiesOut(t *testing.T) {
|
|
valid := map[string]bool{}
|
|
for _, r := range usCounties() {
|
|
valid[r.Code] = true
|
|
}
|
|
for _, c := range []string{"Virginia Beach (city)", "City of Alexandria", "Lynchburg (city)"} {
|
|
if k := award.USCountyKey("VA", c); valid[k] {
|
|
t.Errorf("%q resolved to %s, which USA-CA does not count", c, k)
|
|
}
|
|
}
|
|
// Charles City and James City ARE counties, despite the name.
|
|
for _, c := range []string{"Charles City", "James City"} {
|
|
if k := award.USCountyKey("VA", c); !valid[k] {
|
|
t.Errorf("%q resolved to %s, which is missing from the reference", c, k)
|
|
}
|
|
}
|
|
}
|