diff --git a/data/data.go b/data/data.go index f8928e7..eb3ac47 100644 --- a/data/data.go +++ b/data/data.go @@ -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 +} diff --git a/data/data_test.go b/data/data_test.go new file mode 100644 index 0000000..076c560 --- /dev/null +++ b/data/data_test.go @@ -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") + } +} \ No newline at end of file diff --git a/main.go b/main.go index d8b7e8d..c3cffab 100644 --- a/main.go +++ b/main.go @@ -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" { @@ -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" { @@ -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) { @@ -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}$`) @@ -146,7 +147,17 @@ 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) } @@ -154,12 +165,22 @@ func cli(calls *map[string]data.HamCall) { } } -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()) } diff --git a/source/uls/redirect_test.go b/source/uls/redirect_test.go new file mode 100644 index 0000000..974ed94 --- /dev/null +++ b/source/uls/redirect_test.go @@ -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") + } +} \ No newline at end of file diff --git a/source/uls/uls.go b/source/uls/uls.go index c53b317..e7f8ddb 100644 --- a/source/uls/uls.go +++ b/source/uls/uls.go @@ -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) { @@ -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)) +}