Как сделать Excel-подобный сортировать по A, затем по B в TObjectList <> с использованием нескольких сопоставлений
Я только начал использовать generics, и в настоящее время у меня проблема с сортировкой по нескольким полям.
Case:
У меня есть PeopleList как TObjectList<TPerson>
, и я хочу, чтобы иметь возможность сортировки по Excel, путем выбора одного поля сортировки за раз, но сохраняя предыдущую сортировку как можно больше.
EDIT: Должна быть возможность изменить последовательность сортировки полей во время выполнения. (То есть в одном сценарии пользователь хочет, чтобы порядок сортировки A, B, C - в другом сценарии он хотел, чтобы B, A, C - в еще одном A, C, D)
Допустим, у нас есть несортированный список людей:
Lastname Age
---------------------
Smith 26
Jones 26
Jones 24
Lincoln 34
Теперь, если я сортирую по LastName:
Lastname ▲ Age
---------------------
Jones 26
Jones 24
Lincoln 34
Smith 26
Тогда, если я сортирую по возрасту, я хочу это:
Lastname ▲ Age ▲
---------------------
Jones 24
Jones 26
Smith 26
Lincoln 34
Чтобы сделать это, я сделал два Comparers - один TLastNameComparer и один TAgeComparer.
Теперь я вызываю
PeopleList.Sort(LastNameComparer)
PeopleList.Sort(AgeComparer)
Теперь моя проблема в том, что это не дает результат, который я хочу, но
Lastname ? Age ?
---------------------
Jones 24
Smith 26
Jones 26
Lincoln 34
где Смит, 26 появляется перед Джонсом, 26. Поэтому кажется, что он не сохраняет предыдущую сортировку.
Я знаю, что могу сделать только один сравнитель, который сравнивает как LastName, так и Age - но проблема в том, что мне тогда приходится делать сравнения для каждой комбинации полей, присутствующих в TPerson.
Можно ли делать то, что я хочу, используя несколько TComparers или как я могу выполнить то, что хочу?
Новогоднее обновление
Просто для ссылки на будущих посетителей, это (почти) код, который я использую сейчас.
Сначала я создал базовый класс TSortCriterion<T>
и a TSortCriteriaComparer<T>
, чтобы иметь возможность использовать их в нескольких классах в будущем.
Я изменил Критерий и список на TObject
и TObjectList
соответственно, так как мне стало легче, если объектный список автоматически обрабатывает разрушение Критерия.
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;
Наконец, чтобы использовать критерии сортировки:
(это просто для примера, так как логика создания порядка сортировки действительно зависит от приложения):
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;
Ответы
Ответ 1
Поместите критерии сортировки в список, который включает в себя направление сортировки и функцию, используемую для сравнения элементов. Подобная запись может помочь:
type
TSortCriterion<T> = record
Ascending: Boolean;
Comparer: IComparer<T>;
end;
Когда пользователь настраивает нужный порядок, заполните список экземплярами этой записи.
var
SortCriteria: TList<TSortCriterion>;
Член Comparer
будет ссылаться на функции, которые вы уже написали, для сравнения в зависимости от имени и возраста. Теперь напишите единственную функцию сравнения, которая относится к этому списку. Что-то вроде этого:
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;
Ответ 2
Ваша проблема в том, что вы выполняете два отдельных типа. Вам нужно выполнить один вид и использовать то, что известно как лексическое упорядочение. Вам нужно использовать компаратор, который сравнивает основное поле, а затем, только если первичный ключ сравнивается с равным, продолжает сравнивать вторичный ключ. Вот так:
Result := CompareStr(Left.Name, Right.Name);
if Result=0 then
Result := Left.Age-Right.Age;
Этот подход может быть расширен для обслуживания произвольного количества ключей.
В вашем обновлении вопроса вы добавляете требование о том, чтобы приоритет ключа определялся во время выполнения. Вы можете сделать это с помощью такой функции сравнения:
function TMyClass.Comparison(const Left, Right: TPerson): Integer;
var
i: Integer;
begin
for i := low(FSortField) to high(FSortField) do begin
Result := CompareField(Left, Right, FSortField[i]);
if Result<>0 then begin
exit;
end;
end;
end;
Здесь FSortField
- массив, содержащий идентификаторы для полей, в порядке убывания точности. Таким образом, FSortField[0]
идентифицирует первичный ключ, FSortField[1]
идентифицирует вторичный ключ и так далее. Функция CompareField
сравнивает поле, идентифицированное по его третьему параметру.
Таким образом, функция CompareField
может быть такой:
function CompareField(const Left, Right: TPerson; Field: TField): Integer;
begin
case Field of
fldName:
Result := CompareStr(Left.Name, Right.Name);
fldAge:
Result := Left.Age-Right.Age;
//etc.
end;
end;
Ответ 3
Если у вас есть алгоритм сортировки стабильный, вы можете применить каждый сравнитель в обратном порядке, и результат будет списком, отсортированным в желаемом порядке. Классы списка Delphi используют быструю сортировку, которая не является стабильной. Вам нужно будет применить свою собственную процедуру сортировки вместо встроенных.