Search code examples
delphigenericssortingtobjectlist

How to make an Excel-Like Sort By A, Then By B in a TObjectList<> using multiple comparers


I have just started to use generics, and I am currently having a problem doing sorting on multiple fields.

Case:
I have a PeopleList as a TObjectList<TPerson> and I want to be able to make an Excel-like sorting function, by selecting one sort-field at a time, but keeping the previous sorting as much as possible.

EDIT: It must be possible to change the field sort sequence at runtime. (Ie. in one scenario, the user wants the sort order A,B,C - in another scenario he wants B,A,C - in yet another A,C,D)

Lets say we have an unsorted list of people :

Lastname     Age
---------------------
Smith        26
Jones        26
Jones        24
Lincoln      34

Now if I sort by LastName :

Lastname ▲   Age
---------------------
Jones        26
Jones        24
Lincoln      34
Smith        26

Then if I sort by Age, I want this :

Lastname ▲   Age ▲
---------------------
Jones        24
Jones        26
Smith        26
Lincoln      34

In order to do this, I have made two Comparers - One TLastNameComparer and one TAgeComparer.

I now call

PeopleList.Sort(LastNameComparer)
PeopleList.Sort(AgeComparer)

Now my problem is that this does not produce the output I want, but

Lastname ?   Age ?
---------------------
Jones        24
Smith        26
Jones        26
Lincoln      34

where Smith,26 appears before Jones,26 instead. So it seems like it doesn't keep the previous sorting.

I know that I can make just one comparer that compares both LastName and Age - but the problem is, that I then have to make comparers for each combination of the fields present in TPerson.

Is it possible to do what I want using multiple TComparers or how can I accomplish what I want?

New Years Update

Just for reference to future visitors, this is (almost) the code I am using now.

First I made a base class TSortCriterion<T> and a TSortCriteriaComparer<T> in order to be able to use these in multiple classes in the future. I have changed the Criterion and the list to TObject and TObjectList respectively, as I found it easier if the objectlist automatically handles destruction of the Criterion.

  TSortCriterion<T> = Class(TObject)
    Ascending: Boolean;
    Comparer: IComparer<T>;
  end;

  TSortCriteriaComparer<T> = Class(TComparer<T>)
  Private
    SortCriteria : TObjectList<TSortCriterion<T>>;
  Public
    Constructor Create;
    Destructor Destroy; Override;
    Function Compare(Const Right,Left : T):Integer; Override;
    Procedure ClearCriteria; Virtual;
    Procedure AddCriterion(NewCriterion : TSortCriterion<T>); Virtual;
  End;

implementation

{ TSortCriteriaComparer<T> }

procedure TSortCriteriaComparer<T>.AddCriterion(NewCriterion: TSortCriterion<T>);
begin
  SortCriteria.Add(NewCriterion);
end;

procedure TSortCriteriaComparer<T>.ClearCriteria;
begin
  SortCriteria.Clear;
end;

function TSortCriteriaComparer<T>.Compare(Const Right, Left: T): Integer;
var
  Criterion: TSortCriterion<T>;
begin
  for Criterion in SortCriteria do begin
    Result := Criterion.Comparer.Compare(Right, Left);
    if not Criterion.Ascending then
      Result := -Result;
    if Result <> 0 then
      Exit;
  end;
end;

constructor TSortCriteriaComparer<T>.Create;
begin
  inherited;
  SortCriteria := TObjectList<TSortCriterion<T>>.Create(True);
end;

destructor TSortCriteriaComparer<T>.Destroy;
begin
  SortCriteria.Free;
  inherited;
end;

Finally, in order to use the sort criteria : (this is just for the sake of the example, as the logic of creating the sort order really depends on the application) :

Procedure TForm1.SortList;
Var
  PersonComparer : TSortCriteriaComparer<TPerson>; 
  Criterion : TSortCriterion<TPerson>;
Begin
  PersonComparer := TSortCriteriaComparer<TPerson>.Create;
  Try
    Criterion:=TSortCriterion<TPerson>.Create;
    Criterion.Ascending:=True;
    Criterion.Comparer:=TPersonAgeComparer.Create
    PersonComparer.AddCriterion(Criterion);
    Criterion:=TSortCriterion<TPerson>.Create;
    Criterion.Ascending:=True;
    Criterion.Comparer:=TPersonLastNameComparer.Create
    PersonComparer.AddCriterion(Criterion);
    PeopleList.Sort(PersonComparer);
    // Do something with the ordered list of people.
  Finally
    PersonComparer.Free;  
  End;  
End;

Solution

  • Put your sort criteria in a list that includes the direction to sort and the function to use to compare items. A record like this could help:

    type
      TSortCriterion<T> = record
        Ascending: Boolean;
        Comparer: IComparer<T>;
      end;
    

    As the user configures the desired ordering, populate the list with instances of that record.

    var
      SortCriteria: TList<TSortCriterion>;
    

    The Comparer member will refer to the functions you've already written for comparing based on name and age. Now write a single comparison function that refers to that list. Something like this:

    function Compare(const A, B: TPerson): Integer;
    var
      Criterion: TSortCriterion<TPerson>;
    begin
      for Criterion in SortCriteria do begin
        Result := Criterion.Comparer.Compare(A, B);
        if not Criterion.Ascending then
          Result := -Result;
        if Result <> 0 then
          Exit;
      end;
    end;