Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions data/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,33 @@ type Location struct {
Latitude float64 `json:"lat,omitempty"`
Longitude float64 `json:"lon,omitempty"`
}

// CallRedirections tracks mappings for former callsigns to current callsigns
type CallRedirections struct {
// FRNToCurrentCall maps FRN to the most recent active callsign for that person
FRNToCurrentCall map[string]string
// FormerCallToFRN maps former callsigns to their FRN for redirection lookup
FormerCallToFRN map[string]string
}

// NewCallRedirections creates a new CallRedirections instance
func NewCallRedirections() *CallRedirections {
return &CallRedirections{
FRNToCurrentCall: make(map[string]string),
FormerCallToFRN: make(map[string]string),
}
}

// ResolveCallsign returns the current active callsign for a given callsign
// If the callsign is current/active, returns it unchanged
// If the callsign is former, returns the current callsign for that FRN
func (cr *CallRedirections) ResolveCallsign(callsign string) string {
// Check if this is a former callsign that needs redirection
if frn, isFormer := cr.FormerCallToFRN[callsign]; isFormer {
if currentCall, hasCurrentCall := cr.FRNToCurrentCall[frn]; hasCurrentCall {
return currentCall
}
}
// Return the original callsign (either it's current or we don't have redirection info)
return callsign
}
56 changes: 56 additions & 0 deletions data/data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package data

import (
"testing"
)

func TestCallRedirections(t *testing.T) {
redirections := NewCallRedirections()

// Set up test data - WW0CJ is the current call, KO4JZT is the former call
testFRN := "0012345678"
currentCall := "WW0CJ"
formerCall := "KO4JZT"

redirections.FRNToCurrentCall[testFRN] = currentCall
redirections.FormerCallToFRN[formerCall] = testFRN

// Test resolving a current callsign returns itself
resolved := redirections.ResolveCallsign(currentCall)
if resolved != currentCall {
t.Errorf("Expected current call %s to resolve to itself, got %s", currentCall, resolved)
}

// Test resolving a former callsign returns the current callsign
resolved = redirections.ResolveCallsign(formerCall)
if resolved != currentCall {
t.Errorf("Expected former call %s to resolve to current call %s, got %s", formerCall, currentCall, resolved)
}

// Test resolving an unknown callsign returns itself
unknownCall := "N0CALL"
resolved = redirections.ResolveCallsign(unknownCall)
if resolved != unknownCall {
t.Errorf("Expected unknown call %s to resolve to itself, got %s", unknownCall, resolved)
}
}

func TestNewCallRedirections(t *testing.T) {
redirections := NewCallRedirections()

if redirections.FRNToCurrentCall == nil {
t.Error("FRNToCurrentCall map should be initialized")
}

if redirections.FormerCallToFRN == nil {
t.Error("FormerCallToFRN map should be initialized")
}

if len(redirections.FRNToCurrentCall) != 0 {
t.Error("FRNToCurrentCall map should be empty initially")
}

if len(redirections.FormerCallToFRN) != 0 {
t.Error("FormerCallToFRN map should be empty initially")
}
}
41 changes: 31 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func main() {
}

calls := make(map[string]data.HamCall)
process(&calls)
redirections := process(&calls)
fmt.Printf("processing finished at %s\n", time.Since(start).String())

if *runMode == "b2" || *runMode == "stats" {
Expand All @@ -70,11 +70,11 @@ func main() {
}

if *runMode == "cli" || *runMode == "stats" {
cli(&calls)
cli(&calls, redirections)
}

if *runMode == "web" {
web(&calls, osSigExit)
web(&calls, redirections, osSigExit)
}

if *runMode == "sqlite" {
Expand All @@ -98,12 +98,13 @@ func downloadFiles() {
wg.Wait()
}

func process(calls *map[string]data.HamCall) {
uls.Process(calls)
func process(calls *map[string]data.HamCall) *data.CallRedirections {
redirections := uls.Process(calls)
ised.Process(calls, "ised_data/amateur_delim.txt")
radioid.Process(calls)
lotw.Process(calls)
geo.Process(calls)
return redirections
}

func writeToB2(calls *map[string]data.HamCall, keyID, applicationKey string, uploadWorkers int, osSigExit chan bool, dryRun bool) {
Expand All @@ -121,7 +122,7 @@ func writeToB2(calls *map[string]data.HamCall, keyID, applicationKey string, upl
}
}

func cli(calls *map[string]data.HamCall) {
func cli(calls *map[string]data.HamCall, redirections *data.CallRedirections) {
validate := func(input string) error {
var usCall = regexp.MustCompile(`^[AKNW][A-Z]{0,2}[0123456789][A-Z]{1,3}$`)

Expand All @@ -146,20 +147,40 @@ func cli(calls *map[string]data.HamCall) {
fmt.Printf("Prompt failed %v\n", err)
return
}
j, err := json.MarshalIndent((*calls)[strings.ToUpper(result)], "", " ")

requestedCall := strings.ToUpper(result)
actualCall := redirections.ResolveCallsign(requestedCall)
hamCall := (*calls)[actualCall]

// Add a note if this was a redirection
if requestedCall != actualCall {
hamCall.Callsign = actualCall + " (redirected from " + requestedCall + ")"
}

j, err := json.MarshalIndent(hamCall, "", " ")
if err != nil {
log.Fatalf("error marshaling JSON: %v", err)
}
fmt.Printf("%s\n", string(j))
}
}

func web(calls *map[string]data.HamCall, osSigExit chan bool) {
func web(calls *map[string]data.HamCall, redirections *data.CallRedirections, osSigExit chan bool) {

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
call := r.URL.Path[len("/") : len(r.URL.Path)-len(".json")]
requestedCall := r.URL.Path[len("/") : len(r.URL.Path)-len(".json")]
requestedCall = strings.ToUpper(requestedCall)
fmt.Printf("%s: %s\n", r.Method, r.URL.Path)
j, err := json.Marshal((*calls)[strings.ToUpper(call)])

actualCall := redirections.ResolveCallsign(requestedCall)
hamCall := (*calls)[actualCall]

// Add a note if this was a redirection
if requestedCall != actualCall {
hamCall.Callsign = actualCall + " (redirected from " + requestedCall + ")"
}

j, err := json.Marshal(hamCall)
if err != nil {
// log.Fatalf(err.Error())
}
Expand Down
127 changes: 127 additions & 0 deletions source/uls/redirect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package uls

import (
"testing"

"github.com/pcunning/hamcall/data"
)

func TestBuildCallRedirections(t *testing.T) {
calls := make(map[string]data.HamCall)
redirections := data.NewCallRedirections()

// Set up test data - same FRN with two different callsigns and grant dates
testFRN := "0012345678"

// KO4JZT is the older call (granted 2010-01-01)
calls["KO4JZT"] = data.HamCall{
Callsign: "KO4JZT",
FRN: testFRN,
Grant: "01/01/2010",
Name: "John Smith",
}

// WW0CJ is the newer call (granted 2020-01-01) - should be the current one
calls["WW0CJ"] = data.HamCall{
Callsign: "WW0CJ",
FRN: testFRN,
Grant: "12/31/2020",
Name: "John Smith",
}

// Add a call with different FRN to make sure it's not affected
calls["N0TEST"] = data.HamCall{
Callsign: "N0TEST",
FRN: "0087654321",
Grant: "01/01/2015",
Name: "Jane Doe",
}

// Build redirections
BuildCallRedirections(&calls, redirections)

// Test that the FRN maps to the most recent call
currentCall := redirections.FRNToCurrentCall[testFRN]
if currentCall != "WW0CJ" {
t.Errorf("Expected FRN %s to map to current call WW0CJ, got %s", testFRN, currentCall)
}

// Test that the former call maps to the FRN
formerFRN := redirections.FormerCallToFRN["KO4JZT"]
if formerFRN != testFRN {
t.Errorf("Expected former call KO4JZT to map to FRN %s, got %s", testFRN, formerFRN)
}

// Test that the current call is NOT in the former call map
if _, exists := redirections.FormerCallToFRN["WW0CJ"]; exists {
t.Error("Current call WW0CJ should not be in the former call map")
}

// Test that a call with different FRN is not affected
if _, exists := redirections.FormerCallToFRN["N0TEST"]; exists {
t.Error("N0TEST should not be in former call map as it has unique FRN")
}

// Test redirection functionality
resolved := redirections.ResolveCallsign("KO4JZT")
if resolved != "WW0CJ" {
t.Errorf("Expected KO4JZT to resolve to WW0CJ, got %s", resolved)
}

resolved = redirections.ResolveCallsign("WW0CJ")
if resolved != "WW0CJ" {
t.Errorf("Expected WW0CJ to resolve to itself, got %s", resolved)
}

resolved = redirections.ResolveCallsign("N0TEST")
if resolved != "N0TEST" {
t.Errorf("Expected N0TEST to resolve to itself, got %s", resolved)
}
}

func TestBuildCallRedirectionsEmptyFRN(t *testing.T) {
calls := make(map[string]data.HamCall)
redirections := data.NewCallRedirections()

// Add calls with empty FRN - should not create redirections
calls["W5TEST"] = data.HamCall{
Callsign: "W5TEST",
FRN: "",
Grant: "01/01/2020",
}

BuildCallRedirections(&calls, redirections)

// Should not create any redirections
if len(redirections.FRNToCurrentCall) != 0 {
t.Error("Expected no redirections for calls with empty FRN")
}

if len(redirections.FormerCallToFRN) != 0 {
t.Error("Expected no former call mappings for calls with empty FRN")
}
}

func TestBuildCallRedirectionsSingleCall(t *testing.T) {
calls := make(map[string]data.HamCall)
redirections := data.NewCallRedirections()

// Add single call with FRN - should not create redirections
testFRN := "0012345678"
calls["W5TEST"] = data.HamCall{
Callsign: "W5TEST",
FRN: testFRN,
Grant: "01/01/2020",
}

BuildCallRedirections(&calls, redirections)

// Should not create redirections for single call per FRN
if len(redirections.FRNToCurrentCall) != 0 {
t.Error("Expected no redirections for single call per FRN")
}

if len(redirections.FormerCallToFRN) != 0 {
t.Error("Expected no former call mappings for single call per FRN")
}
}
69 changes: 68 additions & 1 deletion source/uls/uls.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,14 @@ func DownloadApplications(wg *sync.WaitGroup) error {
return nil
}

func Process(calls *map[string]data.HamCall) {
func Process(calls *map[string]data.HamCall) *data.CallRedirections {
redirections := data.NewCallRedirections()
ProcessAM(calls)
ProcessEN(calls)
ProcessHD(calls)
LoadFileNumbers(calls)
BuildCallRedirections(calls, redirections)
return redirections
}

func ProcessAM(calls *map[string]data.HamCall) {
Expand Down Expand Up @@ -300,3 +303,67 @@ func LoadFileNumbers(calls *map[string]data.HamCall) {
fmt.Printf(" ... %s\n", time.Since(start).String())

}

// BuildCallRedirections creates mappings from former callsigns to current callsigns
func BuildCallRedirections(calls *map[string]data.HamCall, redirections *data.CallRedirections) {
start := time.Now()
fmt.Print("building call redirections")

// Build FRN to callsign mapping
frnToCallsigns := make(map[string][]string)

// Group callsigns by FRN
for callsign, hamCall := range *calls {
if hamCall.FRN != "" {
frnToCallsigns[hamCall.FRN] = append(frnToCallsigns[hamCall.FRN], callsign)
}
}

// Helper function to parse MM/DD/YYYY date format for comparison
parseDate := func(dateStr string) time.Time {
if dateStr == "" {
return time.Time{}
}
// Try MM/DD/YYYY format first
if t, err := time.Parse("01/02/2006", dateStr); err == nil {
return t
}
// Try MM/DD/YY format
if t, err := time.Parse("01/02/06", dateStr); err == nil {
return t
}
// Return zero time if parsing fails
return time.Time{}
}

// For each FRN that has multiple callsigns, determine current vs former
for frn, callsigns := range frnToCallsigns {
if len(callsigns) > 1 {
// Find the callsign with the most recent grant date
var currentCall string
var mostRecentGrant time.Time

for _, callsign := range callsigns {
grant := parseDate((*calls)[callsign].Grant)
if grant.After(mostRecentGrant) {
mostRecentGrant = grant
currentCall = callsign
}
}

if currentCall != "" {
// Map this FRN to its current callsign
redirections.FRNToCurrentCall[frn] = currentCall

// Map all other callsigns for this FRN as former callsigns
for _, callsign := range callsigns {
if callsign != currentCall {
redirections.FormerCallToFRN[callsign] = frn
}
}
}
}
}

fmt.Printf(" ... %s (found %d former calls)\n", time.Since(start).String(), len(redirections.FormerCallToFRN))
}