如何在 TObjectList<> 中进行类似 Excel 的按 A 排序,然后按 B 排序使用多个比较器

发布于 2024-12-23 07:00:16 字数 3686 浏览 3 评论 0 原文

我刚刚开始使用泛型,目前在对多个字段进行排序时遇到问题。

案例:
我有一个 PeopleList 作为 TObjectList 并且我希望能够通过一次选择一个排序字段来创建类似 Excel 的排序功能,但尽可能保留先前的排序可能的。

编辑:必须可以在运行时更改字段排序顺序。 (即,在一种情况下,用户想要排序顺序 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)

Now,我的问题是这不会产生我想要的输出,但

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

Smith,26 出现在 Jones,26 之前。所以看起来它没有保留以前的排序。

我知道我可以只创建一个比较器来比较 LastName 和 Age - 但问题是,我必须为 TPerson 中存在的每个字段组合创建比较器。

是否可以使用多个 TComparer 来做我想做的事情,或者我怎样才能完成我想要的事情?

新年更新

仅供未来访客参考,这(几乎)是我现在使用的代码。

首先,我创建了一个基类 TSortCriterion 和一个 TSortCriteriaComparer ,以便将来能够在多个类中使用它们。 我已将 Criterion 和列表分别更改为 TObjectTObjectList,因为我发现如果对象列表自动处理 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;

最后,为了使用排序标准: (这只是为了示例,因为创建排序顺序的逻辑实际上取决于应用程序):

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;

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;

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

挽梦忆笙歌 2024-12-30 07:00:16

将排序标准放入列表中,其中包括排序方向和用于比较项目的函数。像这样的记录可能会有所帮助:

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;

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;
一笑百媚生 2024-12-30 07:00:16

你的问题是你正在执行两种不同的排序。您需要执行一次排序并使用所谓的词汇排序。您需要使用比较器来比较主字段,然后,仅当主键比较相等时,才继续比较辅助键。像这样:

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;

Your problem is that you are performing two separate sorts. You need to perform a single sort and use what is known as a lexical ordering. You need to use a comparer that compares the primary field and then, only if the primary key compares equal, goes on to compare the secondary key. Like this:

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

This approach can be extended to cater for an arbitrary number of keys.


In your update to the question you add the requirement that the key precedence will be determined at runtime. You can do this with a comparison function like this:

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;

Here FSortField is an array containing identifiers for the fields, in descending order of precendence. So FSortField[0] identifies the primary key, FSortField[1] identifies the secondary key and so on. The CompareField function compares the field identified by its third parameter.

So the CompareField function might be like this:

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;
嘿看小鸭子会跑 2024-12-30 07:00:16

如果您有稳定排序算法,那么您可以按相反顺序应用每个比较器,结果将是按照您想要的顺序排序的列表。 Delphi的列表类使用快速排序,这不是一种稳定的排序。您需要应用自己的排序例程而不是内置的排序例程。

If you have a stable sorting algorithm, then you can apply each comparer in reverse order, and the result will be a list sorted in the order you desire. Delphi's list classes use quick sort, which is not a stable sort. You'd need to apply your own sorting routine instead of the built-in ones.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文