feat(lookup): optional QRZ nickname as the logged name
QRZ publishes <nickname> — the name an operator goes BY on the air — and OpsLog was composing fname + name instead. "Bob" is what belongs in a log; "Robert J Smith" is what belongs on a licence. QRZ only, and deliberately so: HamQTH's <nick> already fills the Name field that way, so the same switch there would toggle a behaviour it has no way to turn off. A published nickname is optional, so an empty one falls through to the registered name. That is the whole point of it being a fallback rather than a swap, and it is what the test pins — a blank nickname must never blank the name.
This commit is contained in:
+18
-18
@@ -21,29 +21,29 @@ var ErrNotFound = errors.New("callsign not found")
|
||||
|
||||
// Result is the normalized lookup output regardless of provider.
|
||||
type Result struct {
|
||||
Callsign string `json:"callsign"`
|
||||
Name string `json:"name,omitempty"`
|
||||
QTH string `json:"qth,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
County string `json:"cnty,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
Grid string `json:"grid,omitempty"`
|
||||
Lat float64 `json:"lat,omitempty"`
|
||||
Lon float64 `json:"lon,omitempty"`
|
||||
DXCC int `json:"dxcc,omitempty"`
|
||||
CQZ int `json:"cqz,omitempty"`
|
||||
ITUZ int `json:"ituz,omitempty"`
|
||||
Continent string `json:"cont,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
QSLVia string `json:"qsl_via,omitempty"`
|
||||
Callsign string `json:"callsign"`
|
||||
Name string `json:"name,omitempty"`
|
||||
QTH string `json:"qth,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
County string `json:"cnty,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
Grid string `json:"grid,omitempty"`
|
||||
Lat float64 `json:"lat,omitempty"`
|
||||
Lon float64 `json:"lon,omitempty"`
|
||||
DXCC int `json:"dxcc,omitempty"`
|
||||
CQZ int `json:"cqz,omitempty"`
|
||||
ITUZ int `json:"ituz,omitempty"`
|
||||
Continent string `json:"cont,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
QSLVia string `json:"qsl_via,omitempty"`
|
||||
// Web is the operator's own site. The QSO table has had a `web` column all
|
||||
// along and nothing ever filled it, because no provider mapping read the
|
||||
// field.
|
||||
Web string `json:"web,omitempty"`
|
||||
// Zip is the postal code. HamQTH and QRZ both send one.
|
||||
Zip string `json:"zip,omitempty"`
|
||||
ImageURL string `json:"image_url,omitempty"` // profile picture URL
|
||||
Zip string `json:"zip,omitempty"`
|
||||
ImageURL string `json:"image_url,omitempty"` // profile picture URL
|
||||
Source string `json:"source"` // "qrz", "hamqth", or "cache"
|
||||
FetchedAt time.Time `json:"fetched_at"`
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import "testing"
|
||||
|
||||
func TestTitleCase(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"NOEL CHENAVARD": "Noel Chenavard",
|
||||
"VETRAZ-MONTHOUX": "Vetraz-Monthoux",
|
||||
"o'brien": "O'Brien",
|
||||
"NOEL CHENAVARD": "Noel Chenavard",
|
||||
"VETRAZ-MONTHOUX": "Vetraz-Monthoux",
|
||||
"o'brien": "O'Brien",
|
||||
"866 ROUTE DES VOIRONS": "866 Route Des Voirons",
|
||||
"PARIS": "Paris",
|
||||
"": "",
|
||||
" saint-étienne ": "Saint-Étienne",
|
||||
"JOHN": "John",
|
||||
"PARIS": "Paris",
|
||||
"": "",
|
||||
" saint-étienne ": "Saint-Étienne",
|
||||
"JOHN": "John",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := titleCase(in); got != want {
|
||||
|
||||
+24
-3
@@ -21,8 +21,14 @@ type QRZ struct {
|
||||
|
||||
HTTP *http.Client
|
||||
|
||||
mu sync.Mutex
|
||||
session string
|
||||
// PreferNickname takes QRZ's <nickname> over the composed first+last name
|
||||
// when the operator has published one. It is the name they go BY on the air,
|
||||
// which is what belongs in a log — HamQTH's <nick> is already used that way,
|
||||
// and this brings QRZ into line for operators who want it.
|
||||
PreferNickname bool
|
||||
|
||||
mu sync.Mutex
|
||||
session string
|
||||
loggedAt time.Time
|
||||
}
|
||||
|
||||
@@ -117,7 +123,7 @@ func (q *QRZ) fetch(ctx context.Context, sessionKey, callsign string) (Result, e
|
||||
}
|
||||
r := Result{
|
||||
Callsign: strings.ToUpper(c.Call),
|
||||
Name: joinName(c.FName, c.Name),
|
||||
Name: qrzName(q.PreferNickname, c.Nickname, c.FName, c.Name),
|
||||
QTH: c.Addr2,
|
||||
Address: composeQRZAddress(c.Addr1, c.Addr2, c.Zip, c.Country),
|
||||
State: strings.ToUpper(c.State),
|
||||
@@ -169,6 +175,7 @@ type qrzSession struct {
|
||||
type qrzCallsign struct {
|
||||
Call string `xml:"call"`
|
||||
FName string `xml:"fname"`
|
||||
Nickname string `xml:"nickname"` // the name the operator goes by on the air
|
||||
Name string `xml:"name"`
|
||||
Addr1 string `xml:"addr1"`
|
||||
Addr2 string `xml:"addr2"`
|
||||
@@ -235,3 +242,17 @@ func firstNonEmpty(s ...string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// qrzName picks what goes in the log's Name field.
|
||||
//
|
||||
// The nickname is only taken when the operator asked for it AND QRZ has one —
|
||||
// a blank nickname must never blank the name, which is the whole reason this is
|
||||
// a fallback rather than a swap.
|
||||
func qrzName(preferNickname bool, nickname, fname, name string) string {
|
||||
if preferNickname {
|
||||
if n := strings.TrimSpace(nickname); n != "" {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return joinName(fname, name)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package lookup
|
||||
|
||||
import "testing"
|
||||
|
||||
// The nickname is the name an operator goes BY on the air, and that is what
|
||||
// belongs in a log — "Bob", not "Robert J Smith". HamQTH's <nick> is already
|
||||
// used that way; this brings QRZ into line for operators who ask for it.
|
||||
//
|
||||
// The one thing it must never do is blank the name. A published nickname is
|
||||
// optional, so an empty one has to fall through to the registered name rather
|
||||
// than win by being "preferred".
|
||||
func TestQRZNamePrefersNicknameButFallsBack(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
prefer bool
|
||||
nickname, fname, name string
|
||||
want string
|
||||
}{
|
||||
{true, "Bob", "Robert", "Smith", "Bob"},
|
||||
{true, "", "Robert", "Smith", "Robert Smith"}, // no nickname published
|
||||
{true, " ", "Robert", "Smith", "Robert Smith"}, // blank is not a nickname
|
||||
{false, "Bob", "Robert", "Smith", "Robert Smith"}, // option off
|
||||
{true, "Bob", "", "", "Bob"},
|
||||
{false, "", "Robert", "", "Robert"},
|
||||
{true, "", "", "Smith", "Smith"},
|
||||
} {
|
||||
if got := qrzName(tc.prefer, tc.nickname, tc.fname, tc.name); got != tc.want {
|
||||
t.Errorf("qrzName(%v, %q, %q, %q) = %q, want %q",
|
||||
tc.prefer, tc.nickname, tc.fname, tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user