Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
like image 932
TechnoCowboy Avatar asked Dec 29 '11 20:12

TechnoCowboy


People also ask

How to sort multiple levels of data in Excel?

Excel Sort Multiple Levels 1 Select the entire data set that you want to sort. 2 Click the Data tab. 3 Click on the Sort Icon (the one shown below). This will open the Sort dialog box. 4 In the Sort Dialogue box, make the following selections. 5 Click on Add Level (this will add another level of sorting options). 6 ... (more items) See More....

How to use simple sort in Excel?

1. A Simple Sort in Excel Sorting can be a very simple, two-click process to reorganize the data in your spreadsheet. Let's learn how. In an Excel workbook, start off by clicking in a cell of the column you want to sort. Now, make sure that you're on the Home tab of Excel's ribbon, and find the Sort and Filter button on the far right side of it.

What is the difference between sortby and custom sort in Excel?

A big advantage of the SORTBY formula over Excel's Custom Sort feature is that the formula updates automatically whenever the original data changes, while the feature requires cleaning up and re-sorting with each change. How this formula works:

What are the different data sorting techniques in Excel?

Here are three Excel data sorting techniques you'll learn in this tutorial: Sort data simply with just a couple of clicks. Set multiple, cascading sorting rules, such as sorting alphabetically by state, and then by county.


1 Answers

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;
like image 161
Rob Kennedy Avatar answered Oct 07 '22 22:10

Rob Kennedy