Skip to content

Commit

Permalink
Infer and validate CQ Zone from DXCC/country and state
Browse files Browse the repository at this point in the history
  • Loading branch information
flwyd committed Jan 24, 2025
1 parent e1f7a9f commit 6fbb009
Show file tree
Hide file tree
Showing 6 changed files with 755 additions and 31 deletions.
562 changes: 562 additions & 0 deletions adif/spec/cq_zones.go

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions adif/spec/cq_zones_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package spec

import "testing"

func TestAllCountriesCQZone(t *testing.T) {
var active, inactive []CountryEnum
for _, e := range CountryEnumeration.Values {
c := e.(CountryEnum)
if c == CountryNone {
continue
}
if c.Deleted == "true" {
inactive = append(inactive, c)
} else {
active = append(active, c)
}
}
tests := []struct {
name string
countries []CountryEnum
}{
{name: "Active", countries: active},
{name: "Inactive", countries: inactive},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
for _, c := range tc.countries {
if len(CQZoneFor(c.EntityName)) == 0 {
t.Errorf("CQZoneFor(%q) is missing", c.EntityName)
}
if len(CQZoneFor(c.EntityName)) == 0 {
t.Errorf("CQZoneFor(%q) is missing", c.EntityCode)
}
}
})
}
}
18 changes: 18 additions & 0 deletions adif/spec/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"unicode/utf8"

"golang.org/x/exp/constraints"
"golang.org/x/exp/slices"
)

type Validity int
Expand Down Expand Up @@ -246,6 +247,23 @@ func ValidateNumber(val string, f Field, ctx ValidationContext) Validation {
return errorf("%s value %s above maximum %s", f.Name, val, maxstr)
}
}

if f.Name == CqzField.Name || f.Name == MyCqZoneField.Name {
var dxcc string
if f.Name == CqzField.Name {
dxcc = ctx.FieldValue(DxccField.Name)
} else if f.Name == MyCqZoneField.Name {
dxcc = ctx.FieldValue(MyDxccField.Name)
}
if dxcc != "" {
zs := CQZoneFor(dxcc)
if len(zs) > 0 && !slices.Contains(zs, int(num)) {
return errorf("%s value %s is not valid for DXCC %s, not in %v", f.Name, val, dxcc, zs)
}
}
// TODO if len(zs) > 1 check (My)StateField too, but ADIF CQ Zone data on
// PrimaryAdministrativeSubdivision is incomplete for China/Australia/Yemen
}
return valid()
}

Expand Down
28 changes: 28 additions & 0 deletions adif/spec/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -875,3 +875,31 @@ func TestValidateWWFFRef(t *testing.T) {
testValidator(t, tc, emptyCtx, "ValidateWWFFRef")
}
}

func TestValidateCQZone(t *testing.T) {
tests := []struct {
validateTest
dxcc, mydxcc string
}{
{validateTest: validateTest{field: CqzField, value: "9", want: Valid}, dxcc: CountryTrinidadTobago.EntityCode, mydxcc: ""},
{validateTest: validateTest{field: CqzField, value: "09", want: Valid}, dxcc: CountryTrinidadTobago.EntityCode, mydxcc: ""},
{validateTest: validateTest{field: CqzField, value: "29", want: InvalidError}, dxcc: CountryTrinidadTobago.EntityCode, mydxcc: ""},
{validateTest: validateTest{field: MyCqZoneField, value: "40", want: Valid}, dxcc: "", mydxcc: CountryIceland.EntityCode},
{validateTest: validateTest{field: MyCqZoneField, value: "14", want: InvalidError}, dxcc: "", mydxcc: CountryIceland.EntityCode},
{validateTest: validateTest{field: CqzField, value: "24", want: Valid}, dxcc: CountryChina.EntityCode, mydxcc: ""},
{validateTest: validateTest{field: CqzField, value: "19", want: InvalidError}, dxcc: CountryChina.EntityCode, mydxcc: ""},
}
for _, tc := range tests {
ctx := ValidationContext{FieldValue: func(name string) string {
switch name {
case DxccField.Name:
return tc.dxcc
case MyDxccField.Name:
return tc.mydxcc
default:
return ""
}
}}
testValidator(t, tc.validateTest, ctx, "TestValidateCQZone")
}
}
95 changes: 64 additions & 31 deletions cmd/infer.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ var inferrers = map[string]inferrer{
spec.ModeField.Name: inferMode,
spec.CountryField.Name: inferCountry,
spec.MyCountryField.Name: inferCountry,
spec.DxccField.Name: inferDxcc,
spec.MyDxccField.Name: inferDxcc,
spec.DxccField.Name: inferDXCC,
spec.MyDxccField.Name: inferDXCC,
spec.CqzField.Name: inferCQZone,
spec.MyCqZoneField.Name: inferCQZone,
spec.GridsquareField.Name: inferGridsquare,
spec.GridsquareExtField.Name: inferGridsquare,
spec.MyGridsquareField.Name: inferGridsquare,
Expand Down Expand Up @@ -82,6 +84,8 @@ func helpInfer() string {
fmt.Fprintf(res, fromfmt, spec.MyCountryField.Name, spec.MyDxccField.Name)
fmt.Fprintf(res, fromfmt, spec.DxccField.Name, spec.CountryField.Name)
fmt.Fprintf(res, fromfmt, spec.MyDxccField.Name, spec.MyCountryField.Name)
fmt.Fprintf(res, fromfmt, spec.CqzField.Name, spec.DxccField.Name)
fmt.Fprintf(res, fromfmt, spec.MyCqZoneField.Name, spec.MyDxccField.Name)
fmt.Fprintf(res, fromfmt, spec.CntyField.Name, spec.UsacaCountiesField.Name)
fmt.Fprintf(res, fromfmt, spec.MyCntyField.Name, spec.MyUsacaCountiesField.Name)
fmt.Fprintf(res, fromfmt, spec.UsacaCountiesField.Name, spec.CntyField.Name)
Expand Down Expand Up @@ -168,6 +172,13 @@ func runInfer(ctx *Context, args []string) error {
return write(ctx, acc.Out)
}

func myPrefix(name string) func(string) string {
if strings.HasPrefix(name, "MY_") {
return func(s string) string { return "MY_" + s }
}
return func(s string) string { return s }
}

func inferBand(r *adif.Record, name string) bool {
freqname := spec.FreqField.Name
if name == spec.BandRxField.Name {
Expand Down Expand Up @@ -200,10 +211,7 @@ func inferBand(r *adif.Record, name string) bool {
}

func inferCountry(r *adif.Record, name string) bool {
my := func(s string) string { return s }
if strings.HasPrefix(name, "MY_") {
my = func(s string) string { return "MY_" + s }
}
my := myPrefix(name)
code, ok := r.Get(my(spec.DxccField.Name))
if !ok || code.Value == "" || code.Value == "0" {
return false
Expand All @@ -219,11 +227,8 @@ func inferCountry(r *adif.Record, name string) bool {
return false
}

func inferDxcc(r *adif.Record, name string) bool {
my := func(s string) string { return s }
if strings.HasPrefix(name, "MY_") {
my = func(s string) string { return "MY_" + s }
}
func inferDXCC(r *adif.Record, name string) bool {
my := myPrefix(name)
c, ok := r.Get(my(spec.CountryField.Name))
if !ok || c.Value == "" {
return false
Expand Down Expand Up @@ -253,10 +258,7 @@ func inferMode(r *adif.Record, name string) bool {
}

func inferSigInfo(r *adif.Record, name string) bool {
my := func(s string) string { return s }
if strings.HasPrefix(name, "MY_") {
my = func(s string) string { return "MY_" + s }
}
my := myPrefix(name)
islota, iotaok := r.Get(my(spec.IotaField.Name))
pota, potaok := r.Get(my(spec.PotaRefField.Name))
sota, sotaok := r.Get(my(spec.SotaRefField.Name))
Expand Down Expand Up @@ -316,10 +318,7 @@ func inferSigInfo(r *adif.Record, name string) bool {

func inferProgramRef(wantSig string) inferrer {
return func(r *adif.Record, name string) bool {
my := func(s string) string { return s }
if strings.HasPrefix(name, "MY_") {
my = func(s string) string { return "MY_" + s }
}
my := myPrefix(name)
siginfo, ok := r.Get(my(spec.SigInfoField.Name))
if !ok || siginfo.Value == "" {
return false
Expand Down Expand Up @@ -368,10 +367,7 @@ func inferUSCounty(r *adif.Record, name string) bool {
if f, ok := r.Get(name); ok && f.Value != "" {
return false
}
my := func(s string) string { return s }
if strings.HasPrefix(name, "MY_") {
my = func(s string) string { return "MY_" + s }
}
my := myPrefix(name)
if dx, ok := r.Get(my(spec.DxccField.Name)); ok && dx.Value != "" {
if !spec.CountryCodeUSA.IncludesDXCC(dx.Value) {
return false // not a US contact
Expand All @@ -394,11 +390,51 @@ func inferUSCounty(r *adif.Record, name string) bool {
return true
}

func inferLatLon(r *adif.Record, name string) bool {
my := func(s string) string { return s }
if strings.HasPrefix(name, "MY_") {
my = func(s string) string { return "MY_" + s }
func inferCQZone(r *adif.Record, name string) bool {
if f, ok := r.Get(name); ok && f.Value != "" {
return false
}
my := myPrefix(name)
var c, dxcc string
if dx, ok := r.Get(my(spec.DxccField.Name)); ok && dx.Value != "" {
c = dx.Value
dxcc = c
} else if cc, ok := r.Get(my(spec.CountryField.Name)); ok && cc.Value != "" {
c = cc.Value
d := spec.CountryEnumeration.Value(c)
if len(d) == 1 {
dxcc = d[0].(spec.CountryEnum).EntityCode
}
} else {
return false
}
zs := spec.CQZoneFor(c)
if len(zs) == 0 {
return false
}
if len(zs) == 1 {
r.Set(adif.Field{Name: name, Value: strconv.Itoa(zs[0])})
return true
}
// Multiple CQ zones for some countries, check zone of subdivision
if st, ok := r.Get(my(spec.StateField.Name)); ok && st.Value != "" {
vs := spec.PrimaryAdministrativeSubdivisionEnumeration.Value(st.Value)
for _, v := range vs {
a := v.(spec.PrimaryAdministrativeSubdivisionEnum)
if a.DxccEntityCode == dxcc && a.CqZone != "" {
// Some Canadian provinces/territories have comma-separated zones
if z, err := strconv.Atoi(a.CqZone); err == nil {
r.Set(adif.Field{Name: name, Value: strconv.Itoa(z)})
return true
}
}
}
}
return false
}

func inferLatLon(r *adif.Record, name string) bool {
my := myPrefix(name)
f, ok := r.Get(my(spec.GridsquareField.Name))
if !ok || f.Value == "" {
return false
Expand Down Expand Up @@ -431,10 +467,7 @@ func inferLatLon(r *adif.Record, name string) bool {
}

func inferGridsquare(r *adif.Record, name string) bool {
my := func(s string) string { return s }
if strings.HasPrefix(name, "MY_") {
my = func(s string) string { return "MY_" + s }
}
my := myPrefix(name)
var latf, lonf string
if f, ok := r.Get(my(spec.LatField.Name)); ok && f.Value != "" {
latf = f.Value
Expand Down
32 changes: 32 additions & 0 deletions cmd/infer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,38 @@ func TestInfer(t *testing.T) {
want: []adif.Field{{Name: "COUNTRY", Value: "Canada"}, {Name: "MY_COUNTRY", Value: "Republic of Kosovo"}, {Name: "MY_DXCC", Value: "522"}},
},

{
name: "cqz India",
infer: FieldList{"CQZ"},
start: []adif.Field{{Name: "COUNTRY", Value: "India"}, {Name: "MY_COUNTRY", Value: "Armenia"}},
want: []adif.Field{{Name: "COUNTRY", Value: "India"}, {Name: "MY_COUNTRY", Value: "Armenia"}, {Name: "CQZ", Value: "22"}},
},
{
name: "my_cq_zone Mexico",
infer: FieldList{"MY_CQ_ZONE"},
start: []adif.Field{{Name: "MY_DXCC", Value: spec.CountryMexico.EntityCode}, {Name: "DXCC", Value: "108"}},
want: []adif.Field{{Name: "MY_DXCC", Value: spec.CountryMexico.EntityCode}, {Name: "DXCC", Value: "108"}, {Name: "MY_CQ_ZONE", Value: "6"}},
},
{
name: "cqz USA",
infer: FieldList{"CQZ"},
start: []adif.Field{{Name: "DXCC", Value: spec.CountryUnitedStatesOfAmerica.EntityCode}, {Name: "STATE", Value: "MT"}, {Name: "MY_DXCC", Value: "245"}, {Name: "MY_STATE", Value: "MO"}},
want: []adif.Field{{Name: "DXCC", Value: spec.CountryUnitedStatesOfAmerica.EntityCode}, {Name: "STATE", Value: "MT"}, {Name: "MY_DXCC", Value: "245"}, {Name: "MY_STATE", Value: "MO"}, {Name: "CQZ", Value: "4"}},
},
{
name: "my_cq_zone Russia",
infer: FieldList{"MY_CQ_ZONE"},
start: []adif.Field{{Name: "MY_COUNTRY", Value: spec.CountryAsiaticRussia.EntityName}, {Name: "MY_STATE", Value: "IR"}, {Name: "COUNTRY", Value: "ARGENTINA"}, {Name: "STATE", Value: "H"}},
want: []adif.Field{{Name: "MY_COUNTRY", Value: spec.CountryAsiaticRussia.EntityName}, {Name: "MY_STATE", Value: "IR"}, {Name: "COUNTRY", Value: "ARGENTINA"}, {Name: "STATE", Value: "H"}, {Name: "MY_CQ_ZONE", Value: "18"}},
},
{
// Newfounland and Labrador span the CQ Zone 2 and 5 boundary
name: "can't infer multi-zone states",
infer: FieldList{"CQZ"},
start: []adif.Field{{Name: "DXCC", Value: spec.CountryCanada.EntityCode}, {Name: "STATE", Value: "NL"}, {Name: "MY_DXCC", Value: "230"}, {Name: "STATE", Value: "BY"}},
want: []adif.Field{{Name: "DXCC", Value: spec.CountryCanada.EntityCode}, {Name: "STATE", Value: "NL"}, {Name: "MY_DXCC", Value: "230"}, {Name: "STATE", Value: "BY"}},
},

{
name: "mode MFSK",
infer: FieldList{"MODE"},
Expand Down

0 comments on commit 6fbb009

Please sign in to comment.