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;