Skip to content

Commit

Permalink
62 add support for ARRL DX Contest (#138)
Browse files Browse the repository at this point in the history
Fixes #62. Implements ARRL DX Contest. Introduces TDualExchangeContest
as a base class to provide common behaviors to contests with variable
exchanges between callers.
  • Loading branch information
w7sst authored Nov 30, 2022
2 parents 616c8e2 + 1d2bc87 commit 95122de
Show file tree
Hide file tree
Showing 11 changed files with 17,366 additions and 20 deletions.
16,877 changes: 16,877 additions & 0 deletions ARRLDXCW_USDX.txt

Large diffs are not rendered by default.

336 changes: 336 additions & 0 deletions ArrlDx.pas
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
unit ARRLDX;

{$ifdef FPC}
{$MODE Delphi}
{$endif}

interface

uses
Generics.Defaults, Generics.Collections, Classes, DualExchContest, DxStn;

type
TArrlDxCallRec = class
public
Call: string; // call sign
Name: string; // Name
State: string; // State/Province (US/Canada)
Power: string; // Power (DX Stations)
Section: string; // ARRL/RAC section (e.g. OR)
UserText: string; // club name
function GetString: string; // returns 3A OR [club name]
class function compareCall(const left, right: TArrlDxCallRec) : integer; static;
end;

TArrlDx = class(TDualExchContest)
private
ArrlDxCallList: TObjectList<TArrlDxCallRec>;
Comparer: IComparer<TArrlDxCallRec>;

public
constructor Create;
destructor Destroy; override;
function LoadCallHistory(const AUserCallsign : string) : boolean; override;
function OnSetMyCall(const AUserCallsign : string; out err : string) : boolean; override;
function PickStation(): integer; override;
procedure DropStation(id : integer); override;
function GetCall(id:integer): string; override; // returns station callsign
procedure GetExchange(id : integer; out station : TDxStation); override;

function getExch1(id:integer): string; // returns default RST value
function getExch2(id:integer): string; // returns State/Prov (US/Canada) or Power (DX)
function getUserText(id:integer): string; // returns optional club name
function IsNum(Num: String): Boolean;
function FindCallRec(out dxrec: TArrlDxCallRec; const ACall: string): Boolean;
function GetStationInfo(const ACallsign : string) : string; override;
end;


implementation

uses
SysUtils, PerlRegEx, pcre, log, ARRL, CallLst,
Ini, Main;


constructor TArrlDx.Create;
begin
inherited Create(etRST, etStateProv, // US/CA station exchange
etRST, etPower); // DX station exchange
ArrlDxCallList:= TObjectList<TArrlDxCallRec>.Create;
Comparer := TComparer<TArrlDxCallRec>.Construct(TArrlDxCallRec.compareCall);
end;


destructor TArrlDx.Destroy;
begin
FreeAndNil(ArrlDxCallList);
inherited;
end;


// load call history file iff user's callsign has changed.
// for US/CA calls, load DX callsigns; for DX calls, load US/CA calls.
function TArrlDx.LoadCallHistory(const AUserCallsign: string) : boolean;
const
DelimitChar: char = ',';
var
slst, tl: TStringList;
i: integer;
rec: TArrlDxCallRec;
CallInx, NameInx, StateInx, PowerInx, UserTextInx: integer;
begin
slst:= TStringList.Create;
tl:= TStringList.Create;
tl.Delimiter := DelimitChar;
tl.StrictDelimiter := True;
CallInx := -1;
NameInx := -1;
StateInx := -1;
PowerInx := -1;
UserTextInx := -1;
rec := nil;

try
ArrlDxCallList.Clear;

slst.LoadFromFile(ParamStr(1) + 'ARRLDXCW_USDX.txt');

for i:= 0 to slst.Count-1 do begin
tl.DelimitedText := slst.Strings[i];

if (tl.Count < 4) then continue;
if (tl.Strings[0] = '!!Order!!') then
begin
// !!Order!!,Call,Name,State,Power,UserText,
tl.Delete(0); // shifts others down by one
CallInx := tl.IndexOf('Call');
NameInx := tl.IndexOf('Name');
StateInx := tl.IndexOf('State');
PowerInx := tl.IndexOf('Power');
UserTextInx := tl.IndexOf('UserText');
assert(CallInx <> -1);
assert(NameInx <> -1);
assert(StateInx <> -1);
assert(PowerInx <> -1);
assert(UserTextInx <> -1);
continue;
end;

if rec = nil then
rec := TArrlDxCallRec.Create;

// Using .Trim() to remove unexpected spaces in some records
rec.Call := UpperCase(tl.Strings[CallInx].Trim);
rec.Name := UpperCase(tl.Strings[NameInx].Trim);
rec.State := UpperCase(tl.Strings[StateInx].Trim);
rec.Power := UpperCase(tl.Strings[PowerInx].Trim);
if tl.Count > UserTextInx then
rec.UserText := tl.Strings[UserTextInx].Trim
else
rec.UserText := '';

if rec.Call.IsEmpty then continue;

// a well-formed entry will have either State or Power, but not both.
if not (rec.State.IsEmpty xor rec.Power.IsEmpty) then
continue;

// W/VE stations work only DX stations (those with non-empty Power field);
// DX Stations work only W/VE stations (those with non-empty State field)
if ( HomeCallIsLocal and not rec.Power.IsEmpty) or
(not HomeCallIsLocal and not rec.State.IsEmpty) then
begin
ArrlDxCallList.Add(rec);
rec := nil;
end;
end;

Result := True;

finally
if rec <> nil then rec.Free;
slst.Free;
tl.Free;

end;
end;


{
OnSetMyCall is overriden for ARRL DX Contest to determine whether user's
callsign is within US/CA. In other words, is the user's station within the
home region of this contest?
Sets TDualExchContest.HomeCallIsLocal. Used by GetExchangeTypes() to
determine sent messages types (US/CA send State/Province; DX sends Power).
}
function TArrlDx.OnSetMyCall(const AUserCallsign : string;
out err : string) : boolean;
var
dxcc: TDxCCRec;
begin
Result:= True;
err:= '';

// select calls based on location of user's station (US/CA work only DX)
if gDxCCList.FindRec(dxcc, AUserCallsign) then
// Is home call local to contest (i.e. a W/VE Station)?
HomeCallIsLocal := dxcc.Entity.Equals('United States of America') or
dxcc.Entity.Equals('Canada')
else
begin
// report an error
err := Format('Error: ''%s'' is not recognized as a valid DXCC callsign.',
[AUserCallsign]);

// for the error case, make a best-guess effort to determine US/VE
HomeCallIsLocal := AUserCallsign.StartsWith('A') or
AUserCallsign.StartsWith('K') or
AUserCallsign.StartsWith('N') or
AUserCallsign.StartsWith('W') or
AUserCallsign.StartsWith('VE');
Result := False;
end;

// call baseclass to update Me.MyCall and Me.SentExchTypes
if not inherited OnSetMyCall(AUserCallsign, err) then
Result:= False;
end;


function TArrlDx.PickStation(): integer;
begin
result := random(ArrlDxCallList.Count);
end;


procedure TArrlDx.DropStation(id : integer);
begin
ArrlDxCallList.Delete(id)
end;


function TArrlDx.FindCallRec(out dxrec: TArrlDxCallRec; const ACall: string): Boolean;
var
rec: TArrlDxCallRec;
{$ifdef FPC}
index: int64;
{$else}
index: integer;
{$endif}
begin
rec := TArrlDxCallRec.Create();
rec.Call := ACall;
dxrec:= nil;
try
if ArrlDxCallList.BinarySearch(rec, index, Comparer) then
dxrec:= ArrlDxCallList.Items[index];
finally
rec.Free;
end;
Result:= dxrec <> nil;
end;


// return status bar information string from call history file.
// for DX stations, their Entity and Continent is also included.
// this string is used in MainForm.sbar.Caption (status bar).
// Format: '<call> - <user text from ArrlDxCallHistoryFile> [- Entity/Continent]'
function TArrlDx.GetStationInfo(const ACallsign: string) : string;
var
dxrec : TArrlDxCallRec;
dxccrec : TDXCCRec;
userText : string;
dxEntity : string;
begin
dxrec := nil;
dxccrec := nil;
userText := '';
dxEntity := '';
result:= '';

if FindCallRec(dxrec, ACallsign) then
begin
userText:= dxrec.UserText;

// if caller is DX station, include its Continent/Entity
if Self.HomeCallIsLocal and gDXCCList.FindRec(dxccrec, ACallsign) then
dxEntity:= dxccrec.Continent + '/' + dxccrec.Entity;
end;

if (userText <> '') or (dxEntity <> '') then
begin
result:= ACallsign;
if userText <> '' then
result:= result + ' - ' + userText;
if dxEntity <> '' then
result:= result + ' - ' + dxEntity;
end;
end;


function TArrlDx.getCall(id:integer): string; // returns station callsign
begin
result := ArrlDxCallList.Items[id].Call;
end;


procedure TArrlDx.GetExchange(id : integer; out station : TDxStation);
begin
station.Exch1 := getExch1(id);
station.Exch2 := getExch2(id);
station.UserText := getUserText(id);
end;

function TArrlDx.getExch1(id:integer): string; // returns default RST value
begin
result := '599';
end;


function TArrlDx.getExch2(id:integer): string; // returns State/Prov (US/Canada) or Power (DX)
begin
result := ArrlDxCallList.Items[id].State +
ArrlDxCallList.Items[id].Power;
end;


function TArrlDx.getUserText(id:integer): string; // returns optional club name
begin
result := ArrlDxCallList.Items[id].UserText;
end;


function TArrlDx.IsNum(Num: String): Boolean;
var
X : Integer;
begin
Result := Length(Num) > 0;
for X := 1 to Length(Num) do begin
if Pos(copy(Num,X,1),'0123456789') = 0 then begin
Result := False;
Exit;
end;
end;
end;


class function TArrlDxCallRec.compareCall(const left, right: TArrlDxCallRec) : integer;
begin
Result := CompareStr(left.Call, right.Call);
end;


function TArrlDxCallRec.GetString: string; // returns <State>|<Power> [UserText]
begin
Result := Format(' - %s%s', [State, Power]);
if UserText <> '' then
Result := Result + ' ' + UserText;
end;


end.



5 changes: 2 additions & 3 deletions Contest.pas
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ TContest = class
const ADxCallsign : string) : TExchTypes;
function GetExchangeTypes(
const AStationKind : TStationKind;
const AMsgType : TRequestedMsgType;
const ARequestedMsgType : TRequestedMsgType;
const ADxCallsign : string) : TExchTypes; virtual;
function Minute: Single;
function GetAudio: TSingleArray;
Expand Down Expand Up @@ -246,14 +246,13 @@ function TContest.GetRecvExchTypes(
const AMyCallsign : string;
const ADxCallsign : string) : TExchTypes;
begin
// perhaps need to pass in AUserCallsign instead of using TArrlDx.HomeCallIsWVE
Result:= Self.GetExchangeTypes(AStationKind, mtRecvMsg, ADxCallsign);
end;


function TContest.GetExchangeTypes(
const AStationKind : TStationKind;
const AMsgType : TRequestedMsgType;
const ARequestedMsgType : TRequestedMsgType;
const ADxCallsign : string) : TExchTypes;
begin
Result.Exch1 := ActiveContest.ExchType1;
Expand Down
Loading

0 comments on commit 95122de

Please sign in to comment.