This commit is contained in:
2026-06-07 01:11:37 +02:00
parent 17f7a00bd7
commit 16c04fc12b
13 changed files with 418 additions and 52 deletions
+43 -1
View File
@@ -152,7 +152,7 @@ func Migrate(defs []Def) ([]Def, bool) {
func Fields() []string {
return []string{
"dxcc", "cqz", "ituz", "prefix", "callsign",
"state", "cont", "country", "grid",
"state", "cont", "country", "grid", "grid4",
"iota", "sota_ref", "pota_ref", "wwff",
"name", "qth", "address", "comment", "note",
}
@@ -421,6 +421,12 @@ func MatchQSO(d Def, metas []RefMeta, q *qso.QSO) []string {
// sources (lotw|qsl|eqsl). Exported for the statistics view.
func Confirmed(q *qso.QSO, sources []string) bool { return confirmed(q, sources) }
// InScope reports whether a QSO falls within an award's scope (DXCC entity,
// bands, modes, emission, dates) — independent of whether a reference was
// found. Used to surface "in scope but no reference" gaps (e.g. a French QSO
// missing its department for DDFM).
func InScope(d Def, q *qso.QSO) bool { return inScope(&d, q) }
// EmissionOf maps an ADIF mode to its broad category (CW|PHONE|DIGITAL).
func EmissionOf(mode string) string { return emissionOf(mode) }
@@ -709,6 +715,11 @@ func fieldRaw(field string, q *qso.QSO) string {
return q.Country
case "grid":
return q.Grid
case "grid4":
// VUCC: distinct 4-character grid squares. A QSO on a grid line carries
// several in VUCC_GRIDS; otherwise the 4-char prefix of GRIDSQUARE. The
// comma-joined result is split into one reference per square downstream.
return grid4Refs(q)
case "iota":
return q.IOTA
case "sota_ref":
@@ -738,6 +749,37 @@ func fieldRaw(field string, q *qso.QSO) string {
return ""
}
// grid4Refs returns the distinct 4-character grid squares a QSO contributes —
// from VUCC_GRIDS (a comma list when the contact straddles grid lines) if set,
// else the 4-char prefix of GRIDSQUARE. Joined with commas so the matcher
// counts each square separately (VUCC awards).
func grid4Refs(q *qso.QSO) string {
src := strings.TrimSpace(q.VUCCGrids)
if src == "" {
src = strings.TrimSpace(q.Grid)
}
if src == "" {
return ""
}
seen := map[string]struct{}{}
var out []string
for _, tok := range strings.FieldsFunc(src, func(r rune) bool { return r == ',' || r == ';' || r == ' ' }) {
g := strings.ToUpper(strings.TrimSpace(tok))
if len(g) > 4 {
g = g[:4]
}
if g == "" {
continue
}
if _, ok := seen[g]; ok {
continue
}
seen[g] = struct{}{}
out = append(out, g)
}
return strings.Join(out, ",")
}
func dxccAllowed(dxcc *int, filter []int) bool {
if dxcc == nil {
return false
+18
View File
@@ -128,6 +128,24 @@ func TestComputeMatchByDescription(t *testing.T) {
}
}
// VUCC: a grid4 award counts distinct 4-char grid squares, and a QSO on a grid
// line (VUCC_GRIDS) contributes several. grid4 derives from VUCC_GRIDS else the
// 4-char prefix of GRIDSQUARE.
func TestComputeGrid4VUCC(t *testing.T) {
def := Def{Code: "VUCC", Type: TypeGrid, Field: "grid4", Dynamic: true,
Confirm: []string{"lotw", "qsl"}, Valid: true}
qsos := []qso.QSO{
{Callsign: "K1ABC", Band: "6m", Grid: "FN31PR", LOTWRcvd: "Y"}, // → FN31
{Callsign: "W2DEF", Band: "6m", VUCCGrids: "FN20,FN21,FN30,FN31"}, // grid-line: 4 squares
{Callsign: "W3GHI", Band: "2m", Grid: "FN20XX"}, // → FN20 (dup of above)
}
r := Compute([]Def{def}, qsos, nil, nil)[0]
// Distinct squares: FN31, FN20, FN21, FN30 = 4
if r.Worked != 4 {
t.Errorf("VUCC worked = %d, want 4 (%v)", r.Worked, refCodes(r))
}
}
func refCodes(r Result) []string {
out := make([]string, 0, len(r.Refs))
for _, rf := range r.Refs {
+2
View File
@@ -17,6 +17,7 @@ const hunterLogURL = "https://api.pota.app/user/logbook?hunterOnly=1&page=%d&siz
// a park activator, carrying the park reference to stamp onto the local QSO.
type HunterQSO struct {
Worked string `json:"worked"` // activator callsign (the station worked)
Hunter string `json:"hunter"` // YOUR callsign for this hunt (may vary: F4BPO, XV9Q…)
Date time.Time `json:"date"` // QSO date/time (UTC)
Band string `json:"band"` // ADIF band, e.g. "20m"
Mode string `json:"mode"` // logged mode
@@ -95,6 +96,7 @@ func FetchHunterLog(ctx context.Context, token string, logf func(string, ...any)
}
out = append(out, HunterQSO{
Worked: act,
Hunter: strings.ToUpper(strings.TrimSpace(e.WorkedCallsign)), // your call for this hunt
Date: parseHunterTime(e.QSODateTime),
Band: strings.ToLower(strings.TrimSpace(e.Band)),
Mode: strings.ToUpper(strings.TrimSpace(e.LoggedMode)),