Skip to content

Commit

Permalink
Infer and validate ITU Zone when it's unambiguous
Browse files Browse the repository at this point in the history
  • Loading branch information
flwyd committed Jan 30, 2025
1 parent 5c88469 commit c896898
Show file tree
Hide file tree
Showing 7 changed files with 810 additions and 25 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,8 @@ Inferable fields:
* `MY_DXCC` from `MY_COUNTRY`
* `CQZ` from `COUNTRY`/`DXCC`
* `MY_CQ_ZONE` from `MY_COUNTRY`/`MY_DXCC`
* `ITUZ` from `COUNTRY`/`DXCC`
* `MY_ITU_ZONE` from `MY_COUNTRY`/`MY_DXCC`
* `CONT` from `COUNTRY`/`DXCC`
* `CNTY` from `USACA_COUNTIES` (unless multiple county-line counties)
* `MY_CNTY` from `MY_USACA_COUNTIES` (unless multiple county-line counties)
Expand Down
631 changes: 631 additions & 0 deletions adif/spec/itu_zones.go

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions adif/spec/itu_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 TestAllCountriesITUZone(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(ITUZoneFor(c.EntityName)) == 0 {
t.Errorf("ITUZoneFor(%q) is missing", c.EntityName)
}
if len(ITUZoneFor(c.EntityCode)) == 0 {
t.Errorf("ITUZoneFor(%q) is missing", c.EntityCode)
}
}
})
}
}
22 changes: 18 additions & 4 deletions adif/spec/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,21 +248,35 @@ func ValidateNumber(val string, f Field, ctx ValidationContext) Validation {
}
}

if f.Name == CqzField.Name || f.Name == MyCqZoneField.Name {
iscq := f.Name == CqzField.Name || f.Name == MyCqZoneField.Name
isitu := f.Name == ItuzField.Name || f.Name == MyItuZoneField.Name
if iscq || isitu {
var dxcc string
if f.Name == CqzField.Name {
if f.Name == CqzField.Name || f.Name == ItuzField.Name {
dxcc = ctx.FieldValue(DxccField.Name)
} else if f.Name == MyCqZoneField.Name {
if dxcc == "" {
dxcc = ctx.FieldValue(CountryField.Name)
}
} else if f.Name == MyCqZoneField.Name || f.Name == MyItuZoneField.Name {
dxcc = ctx.FieldValue(MyDxccField.Name)
if dxcc == "" {
dxcc = ctx.FieldValue(MyCountryField.Name)
}
}
if dxcc != "" {
zs := CQZoneFor(dxcc)
var zs []int
if iscq {
zs = CQZoneFor(dxcc)
} else if isitu {
zs = ITUZoneFor(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
// and incomplete/inaccurate for several ITU zones
}
return valid()
}
Expand Down
62 changes: 54 additions & 8 deletions adif/spec/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -879,15 +879,20 @@ func TestValidateWWFFRef(t *testing.T) {
func TestValidateCQZone(t *testing.T) {
tests := []struct {
validateTest
dxcc, mydxcc string
dxcc, mydxcc, country, mycountry 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: ""},
{validateTest: validateTest{field: CqzField, value: "38", want: Valid}},
{validateTest: validateTest{field: CqzField, value: "9", want: Valid}, dxcc: CountryTrinidadTobago.EntityCode},
{validateTest: validateTest{field: CqzField, value: "09", want: Valid}, dxcc: CountryTrinidadTobago.EntityCode},
{validateTest: validateTest{field: CqzField, value: "29", want: InvalidError}, dxcc: CountryTrinidadTobago.EntityCode},
{validateTest: validateTest{field: MyCqZoneField, value: "40", want: Valid}, mydxcc: CountryIceland.EntityCode},
{validateTest: validateTest{field: MyCqZoneField, value: "14", want: InvalidError}, mydxcc: CountryIceland.EntityCode},
{validateTest: validateTest{field: CqzField, value: "24", want: Valid}, dxcc: CountryChina.EntityCode},
{validateTest: validateTest{field: CqzField, value: "19", want: InvalidError}, dxcc: CountryChina.EntityCode},
{validateTest: validateTest{field: CqzField, value: "37", want: Valid}, country: CountryYemen.EntityName},
{validateTest: validateTest{field: MyCqZoneField, value: "39", want: InvalidError}, mycountry: CountryYemen.EntityName},
{validateTest: validateTest{field: MyCqZoneField, value: "28", want: Valid}, mycountry: CountryAntarctica.EntityName},
{validateTest: validateTest{field: MyCqZoneField, value: "1", want: InvalidError}, mycountry: CountryAntarctica.EntityName},
}
for _, tc := range tests {
ctx := ValidationContext{FieldValue: func(name string) string {
Expand All @@ -896,6 +901,47 @@ func TestValidateCQZone(t *testing.T) {
return tc.dxcc
case MyDxccField.Name:
return tc.mydxcc
case CountryField.Name:
return tc.country
case MyCountryField.Name:
return tc.mycountry
default:
return ""
}
}}
testValidator(t, tc.validateTest, ctx, "TestValidateCQZone")
}
}

func TestValidateITUZone(t *testing.T) {
tests := []struct {
validateTest
dxcc, mydxcc, country, mycountry string
}{
{validateTest: validateTest{field: ItuzField, value: "90", want: Valid}},
{validateTest: validateTest{field: ItuzField, value: "1", want: Valid}, dxcc: CountryAlaska.EntityCode},
{validateTest: validateTest{field: ItuzField, value: "02", want: Valid}, dxcc: CountryAlaska.EntityCode},
{validateTest: validateTest{field: ItuzField, value: "12", want: InvalidError}, dxcc: CountryAlaska.EntityCode},
{validateTest: validateTest{field: MyItuZoneField, value: "45", want: Valid}, mydxcc: CountryJapan.EntityCode},
{validateTest: validateTest{field: MyItuZoneField, value: "25", want: InvalidError}, mydxcc: CountryJapan.EntityCode},
{validateTest: validateTest{field: ItuzField, value: "33", want: Valid}, dxcc: CountryAsiaticRussia.EntityCode},
{validateTest: validateTest{field: ItuzField, value: "42", want: InvalidError}, dxcc: CountryAsiaticRussia.EntityCode},
{validateTest: validateTest{field: ItuzField, value: "36", want: Valid}, country: CountryCanaryIslands.EntityName},
{validateTest: validateTest{field: MyItuZoneField, value: "37", want: InvalidError}, mycountry: CountryCanaryIslands.EntityName},
{validateTest: validateTest{field: MyItuZoneField, value: "69", want: Valid}, mycountry: CountryAntarctica.EntityName},
{validateTest: validateTest{field: MyItuZoneField, value: "75", want: InvalidError}, mycountry: CountryAntarctica.EntityName},
}
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
case CountryField.Name:
return tc.country
case MyCountryField.Name:
return tc.mycountry
default:
return ""
}
Expand Down
41 changes: 28 additions & 13 deletions cmd/infer.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ var inferrers = map[string]inferrer{
spec.MyCountryField.Name: inferCountry,
spec.DxccField.Name: inferDXCC,
spec.MyDxccField.Name: inferDXCC,
spec.CqzField.Name: inferCQZone,
spec.MyCqZoneField.Name: inferCQZone,
spec.CqzField.Name: inferZone,
spec.MyCqZoneField.Name: inferZone,
spec.ItuzField.Name: inferZone,
spec.MyItuZoneField.Name: inferZone,
spec.ContField.Name: inferContinent,
spec.GridsquareField.Name: inferGridsquare,
spec.GridsquareExtField.Name: inferGridsquare,
Expand Down Expand Up @@ -78,19 +80,19 @@ func helpInfer() string {
res := &strings.Builder{}
res.WriteString("Inferable fields:\n")
fromfmt := " %s from %s\n"
pairfmt := " %s from %s/%s\n"
fmt.Fprintf(res, fromfmt, spec.BandField.Name, spec.FreqField.Name)
fmt.Fprintf(res, fromfmt, spec.BandRxField.Name, spec.FreqRxField.Name)
fmt.Fprintf(res, fromfmt, spec.ModeField.Name, spec.SubmodeField.Name)
fmt.Fprintf(res, fromfmt, spec.CountryField.Name, spec.DxccField.Name)
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.CountryField.Name)
fmt.Fprintf(res, fromfmt, spec.CqzField.Name, spec.DxccField.Name)
fmt.Fprintf(res, fromfmt, spec.MyCqZoneField.Name, spec.MyCountryField.Name)
fmt.Fprintf(res, fromfmt, spec.MyCqZoneField.Name, spec.MyDxccField.Name)
fmt.Fprintf(res, fromfmt, spec.ContField.Name, spec.MyCountryField.Name)
fmt.Fprintf(res, fromfmt, spec.ContField.Name, spec.MyDxccField.Name)
fmt.Fprintf(res, pairfmt, spec.CqzField.Name, spec.DxccField.Name, spec.CountryField.Name)
fmt.Fprintf(res, pairfmt, spec.MyCqZoneField.Name, spec.DxccField.Name, spec.MyCountryField.Name)
fmt.Fprintf(res, pairfmt, spec.ItuzField.Name, spec.DxccField.Name, spec.CountryField.Name)
fmt.Fprintf(res, pairfmt, spec.MyItuZoneField.Name, spec.DxccField.Name, spec.MyCountryField.Name)
fmt.Fprintf(res, pairfmt, spec.ContField.Name, spec.DxccField.Name, spec.CountryField.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 @@ -395,7 +397,7 @@ func inferUSCounty(r *adif.Record, name string) bool {
return true
}

func inferCQZone(r *adif.Record, name string) bool {
func inferZone(r *adif.Record, name string) bool {
if f, ok := r.Get(name); ok && f.Value != "" {
return false
}
Expand All @@ -413,22 +415,35 @@ func inferCQZone(r *adif.Record, name string) bool {
} else {
return false
}
zs := spec.CQZoneFor(c)
iscq := name == spec.CqzField.Name || name == spec.MyCqZoneField.Name
isitu := name == spec.ItuzField.Name || name == spec.MyItuZoneField.Name
var zs []int
if iscq {
zs = spec.CQZoneFor(c)
} else if isitu {
zs = spec.ITUZoneFor(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
// Multiple 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 != "" {
var zone string
if iscq {
zone = a.CqZone
} else if isitu {
zone = a.ItuZone
}
if a.DxccEntityCode == dxcc && zone != "" {
// Some Canadian provinces/territories have comma-separated zones
if z, err := strconv.Atoi(a.CqZone); err == nil {
if z, err := strconv.Atoi(zone); err == nil {
r.Set(adif.Field{Name: name, Value: strconv.Itoa(z)})
return true
}
Expand Down
26 changes: 26 additions & 0 deletions cmd/infer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,32 @@ func TestInfer(t *testing.T) {
want: []adif.Field{{Name: "DXCC", Value: spec.CountryCanada.EntityCode}, {Name: "STATE", Value: "NL"}, {Name: "MY_DXCC", Value: "230"}, {Name: "STATE", Value: "BY"}},
},

{
name: "ituz + my_itu_zone India + Armenia",
infer: FieldList{"ITUZ", "MY_ITU_ZONE"},
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: "ITUZ", Value: "41"}, {Name: "MY_ITU_ZONE", Value: "29"}},
},
{
name: "my_itu_zone Mexico + Madagascar DXCC",
infer: FieldList{"MY_ITU_ZONE", "ITUZ"},
start: []adif.Field{{Name: "MY_DXCC", Value: spec.CountryMexico.EntityCode}, {Name: "DXCC", Value: spec.CountryMadagascar.EntityCode}},
want: []adif.Field{{Name: "MY_DXCC", Value: spec.CountryMexico.EntityCode}, {Name: "DXCC", Value: spec.CountryMadagascar.EntityCode}, {Name: "MY_ITU_ZONE", Value: "10"}, {Name: "ITUZ", Value: "53"}},
},
{
name: "can't infer multi-zone states",
infer: FieldList{"ITUZ", "MY_ITU_ZONE"},
start: []adif.Field{{Name: "DXCC", Value: spec.CountryUnitedStatesOfAmerica.EntityCode}, {Name: "STATE", Value: "MT"}, {Name: "MY_DXCC", Value: spec.CountryCanada.EntityCode}, {Name: "MY_STATE", Value: "ON"}},
want: []adif.Field{{Name: "DXCC", Value: spec.CountryUnitedStatesOfAmerica.EntityCode}, {Name: "STATE", Value: "MT"}, {Name: "MY_DXCC", Value: spec.CountryCanada.EntityCode}, {Name: "MY_STATE", Value: "ON"}},
},
{
// ADIF spec doesn't have multi-zone data for Russia, so check wholly-contained federal subjects
name: "my_itu_zone Russia",
infer: FieldList{"MY_ITU_ZONE", "ITUZ"},
start: []adif.Field{{Name: "MY_COUNTRY", Value: spec.CountryAsiaticRussia.EntityName}, {Name: "MY_STATE", Value: "EA"}, {Name: "COUNTRY", Value: spec.CountryEuropeanRussia.EntityName}, {Name: "STATE", Value: "IN"}},
want: []adif.Field{{Name: "MY_COUNTRY", Value: spec.CountryAsiaticRussia.EntityName}, {Name: "MY_STATE", Value: "EA"}, {Name: "COUNTRY", Value: spec.CountryEuropeanRussia.EntityName}, {Name: "STATE", Value: "IN"}, {Name: "MY_ITU_ZONE", Value: "33"}, {Name: "ITUZ", Value: "29"}},
},

{
name: "continent Panama",
infer: FieldList{"CONT"},
Expand Down

0 comments on commit c896898

Please sign in to comment.