Как сделать сортировку по типу Excel по A, затем по B в TObjectList <> с использованием нескольких компараторов - PullRequest
19 голосов
/ 30 декабря 2011

Я только что начал использовать дженерики, и у меня сейчас проблема с сортировкой по нескольким полям.

Корпус:
У меня есть 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 ▲   Age
---------------------
Jones        26
Jones        24
Lincoln      34
Smith        26

Тогда, если я сортирую по возрасту, я хочу это:

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

Для этого я создал два устройства сравнения - один 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> и 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;

Ответы [ 3 ]

16 голосов
/ 30 декабря 2011

Поместите критерии сортировки в список, который включает в себя направление сортировки и функцию, используемую для сравнения элементов. Такая запись может помочь:

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;
6 голосов
/ 30 декабря 2011

Ваша проблема в том, что вы выполняете два отдельных вида.Вам нужно выполнить одну сортировку и использовать так называемый лексический порядок.Вам нужно использовать компаратор, который сравнивает первичное поле, и затем, только если первичный ключ сравнивается равным, продолжает сравнивать вторичный ключ.Например:

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 голосов
/ 30 декабря 2011

Если у вас есть стабильный алгоритм сортировки, то вы можете применить каждый компаратор в обратном порядке, и результатом будет список, отсортированный в нужном вам порядке.Классы списков Delphi используют быструю сортировку, которая не является стабильной сортировкой.Вам нужно применить собственную процедуру сортировки вместо встроенных.

...