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

StackOverflow https://stackoverflow.com/questions/21150183

質問

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);    
}
役に立ちましたか?

解決

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.

  • in Pascal there are two division operators: float and integer; 3 div 2 = 1 and 3 / 2 = 1.5. Choosing wrong one causes compilation errors at best and data precision losses at worst.
  • I'd prefer explicit typecast from integer Length to float, but Delphi does not support it. So i multiply by 1.0 to cast. Or i may add 0.0.
  • Division takes a lot longer than multiplication - just do it with pen and paper to see. When you have a data-crunching loop, where all elements are divided by the same number, it is good idea to cache 1 / value into a temp variable, and then mutiply each element by it instead. Since GP is of fixed size, it is compiler that calculates (1.0 / Length(GP)) and substitutes this constant. If you would allow different clans to have different amount of games - and turn GP into being dynamic arrays of different sizes - you would be to explicitly add a variable inside the function and to calc coeff := 1.0 / Length(GP); before loop started.

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.

  • FindComponent is not too fast. Okay, you may find them once, cache in array of TLabel and be done. But why bother with extra workarounds?
  • user may resize the window, making it taller or shorter. Now there are 3 labels visible, in a minute there would be 30 labels visible, in a minute there will be 10 labels... How would you re-generate them in runtime ? So there would be enough of those always and in proper positions ? Actually just put them into the grid itself.
  • VCL sucks at form scaling. Now that Winodws 8.1 is out the fonts resolution might be different on different displays. There would be usually 96DPI on you main display, but as you would drag the window onto your secondary display there would be 120DPI, and on your mate's laptop (examples: Lenovo ThinkPad Yoga Pro and Lenovo IdeaPad Yoga 2) there might be like 200DPI or Retina-grade 300DPI. Still you would have to control your labels so their text would be shown exactly to the right of grid rows text, no matter what value would be rows of each height and each font.

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;
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top