delphisortingexternal-sorting

Sort several arrays together and return the ranking number in the all-arrays combined score


I have 2 tables like this

enter image description here

As you can see, if you look at Total you can see the score of each player in 3 rounds. I have to do a list (from the 1st to the 12th) indicating the highest score.

Here the player with 28 points, must have the number 1 (instead of that 8 which is generated by default), the player with 22 must have the number 2 instead of 11... So I have to sort the TOTAL columns and return the position in the correct label.

When I click the button I underlined, the procedure is called:

var vettore:array[1..12] of integer;
    indici:array[1..12] of integer;
    i:smallint;
begin
 for i := 1 to 6 do
  begin
   vettore[i]:= StrToInt(StringGrid1.Cells[5,i]); //col,row
   indici[i] := i;
  end;
 for i := 6 to 12 do
  begin
   vettore[i]:= StrToInt(StringGrid2.Cells[5,i]); //col,row
   indici[i] := i;
  end;

In this way I load inside vettore all the TOTAL numbers in the rows of both tables, and in indici you can find the number of the label on the right of the table (they indicates the position). Now I thought I could use any sorting method since I have only 12 elements (like the Quick Sort).

My problem is this: how can I change the labels texts (the ones on right of the tables) according with the sorted array? It's like the picture above shows.

Every label is called (starting from 1) mvp1, mvp2, mvp3, mvp4... I think this can be helpful because if (maybe) I will have to do a for loop for change the text of each label, I can use a TFindComponent.


If it could be helpful, here there is the function I wrote with javascript on my website (it works):

var totals = [],   //array with the scores
    indices = [];  //array with the indices

for (var i=0; i<6; i++) {
    totals[i] = parseInt(document.getElementById('p'+i).value, 10);
    indices[i] = i;
}

for (var i=6; i<12; i++) {
    totals[i] = parseInt(document.getElementById('p'+i).value, 10);
    indices[i] = i;
}

indices.sort(function(a, b) {
    return totals[b]- totals[a];
});

for (var i=0; i<indices.length; i++) {
    document.getElementById('mvp'+(indices[i]+1)).value = (i+1);    
}

Solution

  • AS. Since only is listed in tags, that means that any Delphi version is okay. I'd refer to .

    1st we would use Advanced Records to hold the data for a single participant. Some links are below, google for more.

    .

    type
      TClanResults = record
      public
         type All_GPs = 1..3;
         var GP: array [All_GPs] of Cardinal;
         var Players: string;
         var Clan_ID: integer;
      private
         function CalcTotal: Cardinal;
         function CalcAverage: single; inline;
      public
         property Total: Cardinal read CalcTotal;
         property AVG: single read CalcAverage;
      end;
    
    { TClanResults }
    
    function TClanResults.CalcAverage: single;
    begin
      Result := Self.Total * ( 1.0 / Length(GP) );
    end;
    
    function TClanResults.CalcTotal: Cardinal;
    var score: cardinal;
    begin
      Result := 0;
      for score in GP do
        Inc(Result, score);
    end;
    

    The expression Self.Total * ( 1.0 / Length(GP) ); can be also written as Self.Total / Length(GP). However i'd like to highlight some Delphi quirks here.

    Now we should make a container to hold results and sort them. There can be several approaches, but we'd use generics-based TList<T>.

    The TList is an object, so you would have to CREATE it and to FREE it. I think you can make it a PUBLIC property of your MainForm, then create the list in TMainForm.OnCreate event and free it in TMainForm.OnDestroy event.

    Another, lazier approach, would be using a regular dynamic array and its extensions.

    However, i'll use TList below. Again, i assume that other routines in you program already and correctly create and destroy the given var ClanData: TList<TClanResults>; object instance.

    type
      TClansTable = TList<TClanResults>;
    
    procedure TMainForm.Input;
    var row: TClanResults
    begin
      Self.ClanData.Clear;
    
      row.Clan_ID := 1;
      row.Players := JclStringList.Add(['John', 'James', 'Jenny']).Join(' and ');
      row.GP[1]   := 2;
      row.GP[1]   := 5;
      row.GP[1]   := 7;
      Self.ClanData.Add(row);
    
      row.Clan_ID := 2;
      row.Players := JclStringList.Add(['Mary', 'Mark', 'Marge']).Join(' and ');
      row.GP[1]   := 3;
      row.GP[1]   := 6;
      row.GP[1]   := 2;
      Self.ClanData.Add(row);
    
      ...
    end;
    
    procedure SortOnTotal(const Table: TClansTable);
    begin
       Table.Sort(
          TComparer<TClanResults>.Construct(
             function(const Left, Right: TClanResults): Integer
             begin Result := - (Left.Total - Right.Total) end
             // negating since we need reversed order: large to little
          )
       );
    end;
    

    Now finally we need to know how to show that table on the screen. I would use typical TStringGrid as the most simplistic widget. I suggest you to look some advanced string grid from JediVCL or something from Torry.net so you would be able to specify columns styles. It is obvious that integers should be right-aligned on the screen and averages should be comma-aligned. However stock TStringGrid does not have kind of GetCellStyle event, so you would need some advanced grid derivative to add it. It is left as your home-task.

    .

    procedure TMainForm.DumpTableToGrid(const Data: TClansTable; const grid: TStringGrid);
    const TableFields = 8;
    var row: integer;
        ss: array of string;
        res: TClanResults;
      procedure DumpTheRow; var col: integer;
      begin
        for col := 0 to TableFields - 1 do begin
            grid.Cells[ col, row ] := ss[ col ]; 
      end;
    begin
      grid.Options := [ goFixedVertLine, goVertLine, goHorzLine, goColSizing, goColMoving, goThumbTracking ];
      grid.ColCount := TableFields;
      SetLength( ss, TableFields );
      grid.RowCount := 1 + Data.Count;
      grid.FixedRows := 1;
      grid.FixedColumns := 1;    
      row := 0; // headers
        ss[0] := ''; // number in the row, self-evident
        ss[1] := 'Players';
        ss[2] := 'GP 1';
      ....
        ss[7] := 'Clan ID';
      DumpTheRow;
    
      for res in Data do begin // we assume Data already sorted before calling this
        Inc(row);
          ss[0] := IntToStr( row );
          ss[1] := res.Players;
          ss[2] := IntToStr( res.GP[1] );
        ...
          ss[6] := FloatToStrF( res.AVG, ffFixed, 4, 2);
          ss[7] := IntToStr( res.Clan_ID );
        DumpTheRow;
      end;
    end;
    

    Now, it is unclear what you mean by those labels. I can guess, that you want to show there ranks according to both your two clans combined positions. The externals labels are a bad idea for few reasons.

    So, i think they should be INSIDE the row. If you want to highlight them - use bold font, or coloured, or large, or whatever inside the grid.

    TRanks = record min, max: word; end;
    TClanResults = record
    ...
      RanksCombined: TRanks;
    ...
    end;
    

    You correctly shown that some clans might have the same results and share the rank.


    Before continuing you, as a JS user, have to notice a basis difference between record and class datatypes. record is operated by value while class is operated by reference. That means for class instances and variables you have to manually allocate memory for new elements and to dispose it for no longer used ones. Since class variable is a reference to some anonymous class instance(data). Hence the different containers of class-type elements can point to the single real element(data, instance), providing for easy data changing and cheaper sorting. Then for record instances (and record variable IS record data) you don't care about memory allocation and life times, yet would have copying data between different record instances, and if you change the one instance, to apply it to other containers you would have to copy it back. This difference is very visible in for element in container loops, whether we can change element.field or not.


    So let us have few more data structures for sorting and calculating. For example

    TAvgAndRanks = class 
       avg: single; rank: TRanks; 
       table: TClansTable; idx: integer; 
    end;
    

    We'll have then modification for the data dumper:

    procedure TMainForm.DumpTableToGrid(const Data: TClansTable; const grid: TStringGrid);
    const TableFields = 9;
    ...
      row := 0; // headers
      ....
        ss[7] := 'Clan ID'; 
        ss[8] := 'Rank';
      DumpTheRow;
    ...
          ss[7] := IntToStr( res.Clan_ID );
          with res.RanksCombined do
            if min = max 
               then ss[9] := IntToStr(min)
               else ss[9] := IntToStr(min) + ' - ' + IntToStr(max);
        DumpTheRow;
    

    Another approach would be to keep ranks externally using something like

      TClanPtr = record table: TClansTable; idx: integer;  end;
      TClanSortData = record avg: single; rank: TRanks;  end;
      TClanRanksCombined = TDictionary<TClanPtr, TClanSortData>;
    

    This approach is more extensible (allows in different window "attach" different extended data to the clans), but would require much more boilerplate. If you liek it more, your homework would be to implement it.

    procedure MakeRanks(const clans: array of TClansTable);
    var tab: TClansTable; idx: integer;
        total: TObjectList<TAvgAndRanks>;
        ar : TAvgAndRanks;
        res: TClanResults;
    
        // for spanning ranks with same avg
        r_curr, r_min: word;
        r_span, r_idx: integer; 
        r_avg: single; 
        r_chg: boolean;
    begin
      total := TObjectList<TAvgAndRanks>.Create( True ); // auto-free by container
      try
        for tab in clans do
          for idx := 0 to tab.Count - 1 do begin
            res := tab[ idx ];
    
            ar := TAvgAndRanks.Create; // but creation is still manual
            ar.table := tab;
            ar.idx := idx;
            ar.avg := res.AVG;
    
            total.Add(ar);
          end;
    
        if total.Count <= 0 then Abort;
    
        if total.Count = 1 then begin
           ar := total[0]; 
    
           res := ar.table[ ar.idx ];
           res.RanksCombined.min := 1;
           res.RanksCombined.max := 1;
           ar.table[ ar.idx ] := res; // copying back updated data
    
           Exit; // from procedure - nothing to do
        end;
    
        total.Sort(  
          TComparer<TAvgAndRanks>.Construct(
            function(const Left, Right: TAvgAndRanks): Integer
            begin Result := - (Left.avg - Right.avg) end
            // negating since we need reversed order: large to little
          )
        );
    
        (***** calculating ranks with spans ****)
    
        r_curr := 1;
        r_min := 1;
        r_span := 0;
        r_idx := 0;
        r_avg := total[0].avg;
    
        for idx := 1 to total.Count - 1 do begin
          ar := total[ idx ];
    
          inc(r_curr);
    
          if r_avg = ar.avg then inc(r_span);
    
          if (r_avg <> ar.avg) or (idx = total.Count - 1) then begin
    
             for r_idx := r_idx to r_idx + r_span do begin
                with total[ r_idx ] do begin // class == reference, can update directly
                  rank.min := r_min;
                  rank.max := r_min + r_span;
                end;
             end;        
    
             Assert( (r_curr = r_min + r_span + 1) or ( r_avg = ar.avg ) );
    
             r_min := r_curr;
             r_span := 0;
             r_idx := idx;
             r_avg := ar.avg;
          end;
        end;
    
        (*** saving calculated ranks  ***)
    
        for ar in total do begin
            res := ar.table[ ar.idx ];
            res.RanksCombined := ar.ranks;
            ar.table[ ar.idx ] := res; // copying back updated data
        end;
    
      finally
        Total.Destroy;
      end;
    end;