Home > Software design >  Delphi 10.4 - Sort a dynamic array of record by 2 values
Delphi 10.4 - Sort a dynamic array of record by 2 values

Time:10-08

I am writing a program in Delphi 10.4 that is reading multiple tables from a database into a dynamic array of records. The SQL query already sorts the values by the name during the initial load of the data.

These records are then displayed on a ListView under different columns. I want to give the user the option to click on a column to sort the values according to that column. Up to this point, everything works perfectly fine. I have the current code below, and you are welcome to point out any mistakes I made.

First, I declare the record type:

type
   TDDNS = record
     ID : Integer;      --the ID in the database
     Name  : String;    --the client name
     Alias : string;    --an alias for the client
     Domain : string;   --the DDNS address
     Login : String;    --DDNS login username
     Password: string;  --DDNS login password
     Renewed: TDate;    --Date DDNS account was renewed
     IsActive: Boolean; --Boolean if account is still active
   end;

Secondly, I create the dynamic array:

DDNSDetails : array of TDDNS;

The data is then read into the array.

The Login and Password data is not displayed in the ListView for obvious reasons.

For the sorting, I use the following code:

procedure lvDDNSColumnClick(Sender: TObject;
  Column: TListColumn);
begin
  SortList(Column.Index);
  ReloadLV();
end;

procedure SortList(Col : Integer);
var
 i, j : Integer;
begin
  if Length(DDNSDetails) > 0 then
  begin
    for i :=  0 to Length(DDNSDetails)-1 do
    begin
      for j := i 1 to Length(DDNSDetails)-1 do
      begin
        if Col = 0 then //Name
        begin
          if UpperCase(DDNSDetails[i].Name) > UpperCase(DDNSDetails[j].Name) then
            Resort(i, j);
        end else
        if Col = 1 then //Alias
        begin
          if UpperCase(DDNSDetails[i].Alias) > UpperCase(DDNSDetails[j].Alias) then
            Resort(i, j);
        end else
        if Col = 2 then //Domain
        begin
          if UpperCase(DDNSDetails[i].Domain) > UpperCase(DDNSDetails[j].Domain) then
            Resort(i, j);
        end else
        if (Col = 3) or (Col = 4) then //Renewal date
        begin
          if DDNSDetails[i].Renewed > DDNSDetails[j].Renewed then
            Resort(i, j);
        end;
      end;
    end;
    lvDDNS.Columns[0].Caption := 'Client Name';
    lvDDNS.Columns[1].Caption := 'Trading As';
    lvDDNS.Columns[2].Caption := 'Domain Address';
    lvDDNS.Columns[3].Caption := 'Renewed';
    lvDDNS.Columns[4].Caption := 'Active';
    lvDDNS.Columns[Col].Caption := '|| ' lvDDNS.Columns[Col].Caption ' ||';
  end;
end;

procedure Resort(var i, j : Integer);
var
 tempInt : Integer;
 temp : string;
 tempDate : TDate;
 tempBool : Boolean;
begin
  tempInt := DDNSDetails[i].ID;
  DDNSDetails[i].ID := DDNSDetails[j].ID;
  DDNSDetails[j].ID := tempInt;

  temp := DDNSDetails[i].Name;
  DDNSDetails[i].Name := DDNSDetails[j].Name;
  DDNSDetails[j].Name := temp;

  temp := DDNSDetails[i].Alias;
  DDNSDetails[i].Alias := DDNSDetails[j].Alias;
  DDNSDetails[j].Alias := temp;

  temp := DDNSDetails[i].Domain;
  DDNSDetails[i].Domain := DDNSDetails[j].Domain;
  DDNSDetails[j].Domain := temp;

  tempDate := DDNSDetails[i].Renewed;
  DDNSDetails[i].Renewed := DDNSDetails[j].Renewed;
  DDNSDetails[j].Renewed := tempDate;

  tempBool := DDNSDetails[i].IsActive;
  DDNSDetails[i].IsActive := DDNSDetails[j].IsActive;
  DDNSDetails[j].IsActive := tempBool;

  temp := DDNSDetails[i].Login;
  DDNSDetails[i].Login := DDNSDetails[j].Login;
  DDNSDetails[j].Login := temp;

  temp := DDNSDetails[i].Password;
  DDNSDetails[i].Password := DDNSDetails[j].Password;
  DDNSDetails[j].Password := temp;
end;

The purpose of this program is to display DDNS records and login credentials for different DDNS accounts and some clients have more than once account.

What happens is, for example, if you sort by the DDNS renewal date, there may be 50 entries for 23/07/2022 and client "f" has 5 entries under that day, however those 5 entries are not together. In the Name column you might see

z
w
g
x
f
z
a
f
.....

The result should be

a
f
f
f
f
f
g
w
x
z
z
.....

The sorting works perfectly for each column selected. I now need to sort the name column as a secondary if the user sorts any other column.

EDIT: As per a comment by dummzeuch, I changed procedure Resort to the following:

procedure SwapRecord(var i, j : Integer);
var
 temp : TDDNS;
begin
  temp := DDNSDetails[i];
  DDNSDetails[i] := DDNSDetails[j];
  DDNSDetails[j] := temp;
end;

CodePudding user response:

If you are using Delphi 10.4 – try to use generic types. Here what I recommend:

type
   //declare new type to store sort rule
   TSortRule = record
     ColumnID : byte; //number of column
     Desc : boolean;  //reverse sort direction
   end;

//change array to list for storing items, it's much esier to work with it
var
  xList : TList<TDDNS>;

//we need somehow passed few sort rules, i prefer TList, something like that:
var
  xSortOrder : TList<TSortRule>;

Here is procedure for sorting all this staff:

procedure TForm.SortRecords(AList : TList<TDDNS>; ASortOrder : TList<TSortRule>);
begin
  AList.Sort(TComparer<TDDNS>.Construct(
             function(const Left, Right: TDDNS): Integer
             var
               LeftValue, RightValue: TDDNS;
             begin
               //we go for all sorting rules
               for var xSortItem in ASortOrder do begin
                 //check if current rule is reverse
                 if not xSortItem.Desc then begin
                   LeftValue := Left;
                   RightValue := Right;
                 end else begin
                   //it's reverse - switch sides
                   LeftValue := Right;
                   RightValue := Left;
                 end{if..else};

                 //let's do comparation by correct property
                 case xSortItem.ColumnID of
                   0:  Result := CompareStr(Left.Name, Right.Name);
                   1:  Result := CompareStr(Left.Alias, Right.Alias);
                   2:  Result := CompareStr(Left.Domain, Right.Domain);
                   3, 4:  Result := TComparer<TDate>.Default.Compare(Left.Renewed, Right.Renewed);
                 end{case};

                 //if items not equval by this rule, we skip next rules
                 if Result <> 0 then
                   break;
               end{for};
              end
            ));
end;

More info about sorting of TList<> you can read in oficial doc or Here example

CodePudding user response:

Based on my initial code I managed to modify that and get it working in a much simpler way.

Firstly, I created a new class for the record type and sort procedure and I declared them as follows:

type
  TRec = record
    dbID : Integer;
    Name  : String;
    Alias : string;
    Domain : string;
    Login : String;
    Password: string;
    Renewed: TDate;
    IsActive: Boolean;
  end;

type
  TData = array of TRec;

procedure SortData(Data : TData; const Field : Integer);
procedure SwapRecords(var Data : TData; const i, j : Integer);

SortData performs the comparing for the sorting and SwapRecords swaps the entries during the sorting procedure. SortData uses Goto to go to the bottom of the loop once it finds the field it needs to sort in order for it to save time and start with the next cycle.

The procedures are scripted as follows:

procedure SortData(Data : TData; const Field : Integer);
var
  n, newn, i : integer;
label
  bottom;
begin
  n := length(Data);
  repeat
    newn := 0;
    for i := 1 to n-1 do
    begin
      if Field = 1 then //Name
      begin
        if UpperCase(Data[i-1].Name) > UpperCase(Data[i].Name) then
        begin
          SwapRecords(Data, i-1, i);
          newn := i;
          Goto bottom;
        end;
      end;

      if Field = 2 then //Alias
      begin
        if UpperCase(Data[i-1].Alias) > UpperCase(Data[i].Alias) then
        begin
          SwapRecords(Data, i-1, i);
          newn := i;
          Goto bottom;
        end;
      end;

      if Field = 3 then //Domain
      begin
        if UpperCase(Data[i-1].Domain) > UpperCase(Data[i].Domain) then
        begin
          SwapRecords(Data, i-1, i);
          newn := i;
          Goto bottom;
        end;
      end;

      if Field = 3 then //Login
      begin
        if UpperCase(Data[i-1].Login) > UpperCase(Data[i].Login) then
        begin
          SwapRecords(Data, i-1, i);
          newn := i;
          Goto bottom;
        end;
      end;

      if Field = 4 then //Password
      begin
        if UpperCase(Data[i-1].Password) > UpperCase(Data[i].Password) then
        begin
          SwapRecords(Data, i-1, i);
          newn := i;
          Goto bottom;
        end;
      end;

      if Field = 5 then //Renewed
      begin
        if Data[i-1].Renewed > Data[i].Renewed then
        begin
          SwapRecords(Data, i-1, i);
          newn := i;
          Goto bottom;
        end;
      end;

      if Field = 6 then  //IsActive
      begin
        if Data[i-1].IsActive > Data[i].IsActive then
        begin
          SwapRecords(Data, i-1, i);
          newn := i;
          Goto bottom;
        end;
      end;

      bottom:
    end;
    n := newn;
  until n < 1;
end;

procedure SwapRecords(var Data : TData; const i, j : Integer);
var
  temp : TRec;
begin
  temp := Data[i];
  Data[i] := Data[j];
  Data[j] := temp;
end;

Finally, in the main form I call this procedure after creating and filling a varialbe (DDNSInfo of TData).

procedure TfrmDDNS.lvDDNSColumnClick(Sender: TObject;
  Column: TListColumn);
var
  Field : Integer;
begin
  ColActive := Column.Index;//ColActive is a global Integer in the form to keep track of which column is selected in the TListView
  case ColActive of
    0 : Field := 1;//Client Name
    1 : Field := 2;//Trading As
    2 : Field := 3;//Domain Address
    3 : Field := 6;//Renewed
    4 : Field := 7;//Active
  else
    Field := 0;
  end;

  //Sort array
  if Field = 6 then
  begin
    SortData(DDNSInfo,1);//Sort according to Client Name
    SortData(DDNSInfo,Field);//Sort according to Renewal Date
  end else
    SortData(DDNSInfo,Field);//Sort only according to selected column

  //Output new array
  UpdateLV(lvDDNS,DDNSInfo);//This is another procedure for updating the data displayed in the TListView
end;

I set this up to accomodate sorting according to more than one column and this can be expanded to accomodate more scenarios, such as websites, other types of passwords, etc.

  • Related