delphi generics sorting tobjectlist

delphi - Cómo hacer una clasificación similar a Excel por A, luego por B en una TObjectList<> usando múltiples comparadores



generics sorting (3)

Acabo de comenzar a usar genéricos y actualmente tengo problemas para clasificar en varios campos.

Caso:
Tengo una PeopleList como TObjectList<TPerson> y quiero poder realizar una función de clasificación similar a Excel, seleccionando un campo de clasificación a la vez, pero manteniendo la clasificación anterior tanto como sea posible.

EDITAR: debe ser posible cambiar la secuencia de clasificación de campo en tiempo de ejecución. (Es decir, en un escenario, el usuario desea el ordenamiento A, B, C; en otro escenario, quiere B, A, C; en otro A, C, D)

Digamos que tenemos una lista de personas sin clasificar:

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

Ahora si lo ordeno por Apellido:

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

Entonces si ordeno por Edad, quiero esto:

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

Para hacer esto, he hecho dos comparadores: uno TLastNameComparer y uno TAgeComparer.

Ahora llamo

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

Ahora mi problema es que esto no produce la salida que quiero, pero

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

donde Smith, 26 aparece ante Jones, 26 en cambio. Así que parece que no mantiene la clasificación anterior.

Sé que puedo hacer un solo comparador que compare tanto Apellido como Edad, pero el problema es que luego tengo que hacer comparadores para cada combinación de los campos presentes en TPerson.

¿Es posible hacer lo que quiero usando varios TComparers o cómo puedo lograr lo que quiero?

Actualización de año nuevo

Solo para referencia a futuros visitantes, este es (casi) el código que estoy usando ahora.

Primero hice una clase base TSortCriterion<T> y un TSortCriteriaComparer<T> para poder usarlos en varias clases en el futuro. He cambiado el Criterio y la lista a TObject y TObjectList respectivamente, ya que me resultó más fácil si la lista de objetos controla automáticamente la destrucción del Criterio.

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;

Finalmente, para usar los criterios de clasificación: (esto es solo por el ejemplo, ya que la lógica de crear el orden de clasificación realmente depende de la aplicación):

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;


Ponga sus criterios de clasificación en una lista que incluya la dirección para ordenar y la función que se usará para comparar elementos. Un registro como este podría ayudar:

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

A medida que el usuario configura el orden deseado, rellene la lista con las instancias de ese registro.

var SortCriteria: TList<TSortCriterion>;

El miembro del Comparer se referirá a las funciones que ya ha escrito para comparar en función del nombre y la edad. Ahora escribe una única función de comparación que se refiera a esa lista. Algo como esto:

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;


Si tiene un algoritmo de clasificación estable , puede aplicar cada comparador en orden inverso , y el resultado será una lista ordenada en el orden que desee. Las clases de la lista de Delphi usan una ordenación rápida, que no es una ordenación estable. Necesitará aplicar su propia rutina de clasificación en lugar de las integradas.


Su problema es que está realizando dos clases separadas. Debe realizar una única ordenación y usar lo que se conoce como ordenamiento léxico . Debe usar un comparador que compare el campo primario y luego, solo si la clave primaria se compara igual, continúa comparando la clave secundaria. Me gusta esto:

Result := CompareStr(Left.Name, Right.Name); if Result=0 then Result := Left.Age-Right.Age;

Este enfoque puede extenderse para atender un número arbitrario de claves.

En su actualización de la pregunta, agrega el requisito de que la prioridad de la clave se determinará en el tiempo de ejecución. Puedes hacer esto con una función de comparación como esta:

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;

Aquí FSortField es una matriz que contiene identificadores para los campos, en orden decreciente de prioridad. Por FSortField[0] tanto, FSortField[0] identifica la clave primaria, FSortField[1] identifica la clave secundaria y así sucesivamente. La función CompareField compara el campo identificado por su tercer parámetro.

Así que la función CompareField podría ser así:

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;