How do I sort a generic list using a custom comparer?

37,832

Solution 1

The Sort overload you should be using is this one:

procedure Sort(const AComparer: IComparer<TMyRecord>);

Now, you can create an IComparer<TMyRecord> by calling TComparer<TMyRecord>.Construct. Like this:

var
  Comparison: TComparison<TMyRecord>;
....
Comparison := 
  function(const Left, Right: TMyRecord): Integer
  begin
    Result := Left.intVal-Right.intVal;
  end;
List.Sort(TComparer<TMyRecord>.Construct(Comparison));

I've written the Comparison function as an anonymous method, but you could also use a plain old style non-OOP function, or a method of an object.

One potential problem with your comparison function is that you may suffer from integer overflow. So you could instead use the default integer comparer.

Comparison := 
  function(const Left, Right: TMyRecord): Integer
  begin
    Result := TComparer<Integer>.Default.Compare(Left.intVal, Right.intVal);
  end;

It might be expensive to call TComparer<Integer>.Default repeatedly so you could store it away in a global variable:

var
  IntegerComparer: IComparer<Integer>;
....
initialization
  IntegerComparer := TComparer<Integer>.Default;

Another option to consider is to pass in the comparer when you create the list. If you only ever sort the list using this ordering then that's more convenient.

List := TList<TMyRecord>.Create(TComparer<TMyRecord>.Construct(Comparison));

And then you can sort the list with

List.Sort;

Solution 2

The concise answer:

uses
  .. System.Generics.Defaults // Contains TComparer

myList.Sort(
  TComparer<TMyRecord>.Construct(
    function(const Left, Right: TMyRecord): Integer
    begin
      Result := Left.intVal - Right.intVal;
    end
  )
);

Solution 3

I want to share my solution (based on the input I have gathered here).

It's a standard setup. A filedata class that holds data of a single file in a generic TObjectList. The list has the two private attributes fCurrentSortedColumn and fCurrentSortAscending to control the sort order. The AsString-method is the path and filename combined.

function TFileList.SortByColumn(aColumn: TSortByColums): boolean;
var
  Comparison: TComparison<TFileData>;
begin
  result := false;
  Comparison := nil;

  case aColumn of
    sbcUnsorted   : ;
    sbcPathAndName: begin
                      Comparison := function(const Left, Right: TFileData): integer
                                    begin
                                      Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
                                    end;
                    end;
    sbcSize       : begin
                      Comparison := function(const Left, Right: TFileData): integer
                                    begin
                                      Result := TComparer<int64>.Default.Compare(Left.Size,Right.Size);
                                      if Result = 0 then
                                        Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
                                    end;
                    end;
    sbcDate       : begin
                      Comparison := function(const Left, Right: TFileData): integer
                                    begin
                                      Result := TComparer<TDateTime>.Default.Compare(Left.Date,Right.Date);
                                      if Result = 0 then
                                        Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
                                    end;
                    end;
    sbcState      : begin
                      Comparison := function(const Left, Right: TFileData): integer
                                    begin
                                      Result := TComparer<TFileDataTestResults>.Default.Compare(Left.FileDataResult,Right.FileDataResult);
                                      if Result = 0 then
                                        Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
                                    end;
                     end;
  end;

  if assigned(Comparison) then
  begin
    Sort(TComparer<TFileData>.Construct(Comparison));

    // Control the sort order
    if fCurrentSortedColumn = aColumn then
      fCurrentSortAscending := not fCurrentSortAscending
    else begin
      fCurrentSortedColumn := aColumn;
      fCurrentSortAscending := true;
    end;

    if not fCurrentSortAscending then
      Reverse;

    result := true;
  end;
end;
Share:
37,832
p1.e
Author by

p1.e

Updated on September 01, 2020

Comments

  • p1.e
    p1.e almost 4 years

    I'm kinda a Delphi-newbie and I don't get how the Sort method of a TList of Records is called in order to sort the records by ascending integer value. I have a record like the following:

     type
       TMyRecord = record
         str1: string;
         str2: string;
         intVal: integer;
       end;
    

    And a generic list of such records:

    TListMyRecord = TList<TMyRecord>;
    

    Have tried to find a code-example in the help files and found this one:

    MyList.Sort(@CompareNames);
    

    Which I can't use, since it uses classes. So I tried to write my own compare function with a little different parameters:

    function CompareIntVal(i1, i2: TMyRecord): Integer;
    begin
      Result := i1.intVal - i2.intVal;
    end;
    

    But the compiler always throws a 'not enough parameters' - error when I call it with open.Sort(CompareIntVal);, which seems obvious; so I tried to stay closer to the help file:

    function SortKB(Item1, Item2: Pointer): Integer;
    begin
      Result:=PMyRecord(Item1)^.intVal - PMyRecord(Item2)^.intVal;
    end;
    

    with PMyRecord as PMyRecord = ^TMyRecord;

    I have tried different ways of calling a function, always getting some error...

  • p1.e
    p1.e over 11 years
    Thanks verry much! Do I need to include anything in 'uses' besides the uses Generics.Collections,..., 'cause I get an 'undeclared' for TComparisonand IComparerin var Comparison: TComparison<TKanteRecord>; IntegerComparer: IComparer<Integer>;?
  • David Heffernan
    David Heffernan over 11 years
    You also need Generics.Defaults. Have you found the RTL source code yet. That would help you.
  • TLama
    TLama almost 11 years
    @David, are you sure TComparer is a good choice for a code you provided ? TComparer is meant to be the abstract base class. I'd suggest to use TDelegatedComparer for your code.
  • David Heffernan
    David Heffernan over 10 years
    @TLama Yes I am sure of that: TComparer<T>.Construct(Comparison) is implemented with a call to TDelegatedComparer<T>.Create(Comparison).
  • Kromster
    Kromster over 9 years
    You might want to clean up your code sample a bit. Remove unused variables. Add usage example. Correct the syntax.
  • ByteArts
    ByteArts almost 9 years
    The original question was about sorting a generic list, while this example is using the standard TList (list of pointers), which is a different scenario.
  • SOLID Developper
    SOLID Developper almost 6 years
    @DavidHeffernan The TList<T> does not have a constructor that accept a TComparer<T> as an input parameter in Delphi 10.2. Could you give compilable examples?
  • David Heffernan
    David Heffernan almost 6 years
    @Bitman The code here does compile. It does not pass a TComparer<T> to any constructor.
  • SOLID Developper
    SOLID Developper almost 6 years
    @DavidHeffernan Definitely not! I interested in your last "solution" that uses a TList<T>.create( IComparer<T> ) constructor and a parameterless TList<T>.Sort method call.
  • David Heffernan
    David Heffernan almost 6 years
    @bitman I'm pretty sure it compiles but I can't check right now
  • David Heffernan
    David Heffernan almost 6 years
    @bitman As I thought, the code in the answer compiles. You must have transcribed it incorrectly.
  • SOLID Developper
    SOLID Developper almost 6 years
    @DavidHeffernan As I wrote. I was interested in the second solution. The section after "Another option...". There is no example for this solution.
  • David Heffernan
    David Heffernan almost 6 years
    @bitman yes there is, that's the code I checked, and it compiles just fine. Instead of asking what's wrong with my code you'll need to study yours. You made a mistake somewhere.