在Delphi中对Tstringlist中的数字数据进行数字排序

Posted

技术标签:

【中文标题】在Delphi中对Tstringlist中的数字数据进行数字排序【英文标题】:Sorting numeric data in Tstringlist numerically in Delphi 【发布时间】:2021-07-23 09:45:56 【问题描述】:

大量数字数据(来自数据库)存储在 Tstringlist 变量中。数据可能如下所示:

4
4 1/2
12.006
13 3/8
1.05
13.25
5 1/2
2.25
13 5/8

通过将 Tstringlist 的 sort 属性设置为 true,它们被排序为文本,因此 13.25 出现在 4 之前。

如何对这些数据进行数字化(高效)排序?

(德尔福,Rad Studio 10.4)

【问题讨论】:

您可以使用自定义排序过程来执行此操作。在我对this question 的回答中可以找到使用自定义排序的示例。编码将非常复杂,因为您的字符串很难解析为数字。例如,StrToFloat 不可能与 4 1/2 一起使用。 你说“可能看起来像”。您是否有所有可能性的完整列表或规范?该规范对于构建将您的数据作为输入并生成浮点数然后使用它进行排序的翻译器是必要的。 如果您可以评估条目(以便 fi 4 1/2 评估为 4.5)并将结果值替换为原始值,那么您可以通过左填充每个列表以准数字方式排序以 0(零)为固定长度的条目。 是的。您的问题与排序无关。这是关于将文本解析为数值。一旦你能做到这一点,排序就变得微不足道了。 虽然@KenWhite 的评论(基本上)是正确的,但它确实不是“非常复杂”。我会称之为“几乎是微不足道的”,但这只是在吹毛求疵。 【参考方案1】:

这里有一个解决方案:

// Conversion accepting input like '4', '4.003', '4 1/2' or '1/4'
// No leading or trailing space allowed (use trim if required).
function MyStrToFloat(const S : String) : Double;
var
    I, J : Integer;
    FS   : TFormatSettings;
begin
    I := Pos('/', S);
    if I > 0 then begin
        // We have a fractional part
        J := Pos(' ', S);
        if J > 0 then
            // Both integer and fractional parts
            Result := StrToInt(Trim(Copy(S, 1, J - 1)))
        else
            Result := 0;
        Result := Result + StrToInt(Trim(Copy(S, J + 1, I - J - 1))) /
                           StrToInt(Trim(Copy(S, I + 1)));
    end
    else begin
        FS.DecimalSeparator := '.';
        Result := StrToFloat(S, FS);
    end;
end;

function StringListSortProc(
    List   : TStringList;
    Index1 : Integer;
    Index2 : Integer): Integer;
var
    N1, N2: Double;
begin
    N1 := MyStrToFloat(List[Index1]);
    N2 := MyStrToFloat(List[Index2]);
    if N1 > N2 then
        Result := 1
    else if N1 < N2 then
        Result := -1
    else
    Result := 0;
end;


procedure TForm1.Button1Click(Sender: TObject);
var
    SL : TStringList;
    S  : String;
begin
    SL := TStringList.Create;
    try
        SL.Add('4');
        SL.Add('1/2');
        SL.Add('4 1/2');
        SL.Add('12.006');
        SL.Add('13 3/8');
        SL.Add('1.05');
        SL.Add('13.25');
        SL.Add('5 1/2');
        SL.Add('2.25');
        SL.Add('13 5/8');

        Memo1.Lines.Add('Raw:');
        for S in SL do
            Memo1.Lines.Add(S);

        SL.CustomSort(StringListSortProc);

        Memo1.Lines.Add('Sorted:');
        for S in SL do
            Memo1.Lines.Add(S);
    finally
        SL.Free;
    end;
end;

转换是在排序中完成的,效率不是很高。最好用转换后的值创建一个新列表,然后对其进行排序。或者在从数据库加载列表时进行转换。你有这个想法......

【讨论】:

是的,这就是您的大致做法。但是你不应该使用全局变量FormatSettings(这会导致大型项目出现问题),你不应该硬编码32(跳过Copy的最后一个参数)。 @AndreasRejbrand 好的,老板!编辑了我的答案:-) @hmdknight 我的回答对您有帮助吗?您会将其标记为已接受吗? (使用我答案左侧的勾号)。【参考方案2】:

两个组件解决您的问题:

    StrCmpLogical 是你的朋友。它使用自然排序 - 字符串中的数字被视为数字内容而不是文本。 还有这个帖子How do I enter fractions in Delphi?

我把它放在下面:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type

  TForm1 = class(TForm)
    Memo1: TMemo;
    ButtonSort: TButton;
    Memo2: TMemo;
    procedure ButtonSortClick(Sender: TObject);
  private
     Private declarations 
  public
     Public declarations 
  end;

  //Natural sorting - Digits in the strings are considered as numerical content rather than text. This test is not case-sensitive.
  function StrCmpLogicalW(P1, P2: PWideChar): Integer;  stdcall; external 'Shlwapi.dll';
  function StrCmpLogical(const S1, S2: string): Integer;

var
  Form1: TForm1;

implementation

$R *.dfm

function StrCmpLogical(const S1, S2: string): Integer;
begin
  result := StrCmpLogicalW(PChar(S1), PChar(S2));
end;

function CustomNbrSort(List: TStringList; Index1, Index2: Integer): Integer;
begin
  result := StrCmpLogical(List[index1], List[index2]);
end;

procedure DoCustomSort(const List : TStringList);
begin
  List.CustomSort(CustomNbrSort);
end;

// https://***.com/questions/18082644/how-do-i-enter-fractions-in-delphi
function FractionToFloat(const S: string): real;
var
  BarPos: integer;
  numStr, denomStr: string;
  num, denom: real;
begin
  BarPos := Pos('/', S);
  if BarPos = 0 then
    Exit(StrToFloat(S));
  numStr := Trim(Copy(S, 1, BarPos - 1));
  denomStr := Trim(Copy(S, BarPos + 1, Length(S)));
  num := StrToFloat(numStr);
  denom := StrToFloat(denomStr);
  result := num/denom;
end;

function FullFractionToFloat(S: string): real;
var
  SpPos: integer;
  intStr: string;
  frStr: string;
  int: real;
  fr: real;
begin
  S := Trim(S);
  SpPos := Pos(' ', S);
  if SpPos = 0 then
    Exit(FractionToFloat(S));
  intStr := Trim(Copy(S, 1, SpPos - 1));
  frStr := Trim(Copy(S, SpPos + 1, Length(S)));
  int := StrToFloat(intStr);
  fr := FractionToFloat(frStr);
  result := int + fr;
end;

procedure TForm1.ButtonSortClick(Sender: TObject);
begin
  var MyList : TStrings := TStringList.Create;
  try
    //MyList.Assign(Memo1.Lines);
    var MyFormatSettings : TFormatSettings := FormatSettings; //Make a copy of the global variable;
    MyFormatSettings.DecimalSeparator := '.'; //The system's seporator may be a comma ','

    for var i : integer := 0 to Memo1.Lines.Count - 1 do begin
      var s : string := Memo1.Lines[i];
      if not s.Contains('.') then
         s := FloatToStr(FullFractionToFloat(s), MyFormatSettings);
      MyList.AddObject(s, TObject(i));
    end;

    TStringList(MyList).CustomSort(CustomNbrSort);

    Memo2.Lines.Clear;
    for var i : integer := 0 to Memo1.Lines.Count - 1 do
        Memo2.Lines.Add(Memo1.Lines[Integer(MyList.Objects[i])]);
  finally
    MyList.Free;
  end;

end;

end.

【讨论】:

它几乎可以正常工作,但有一些例外情况,例如:1.9 的排序低于1.662.063 的排序高于2 3/82 7/8 @hmdknight StrCmpLogical() 不是你的朋友。只需使用左填充格式化数字并使用常规字符串排序。

以上是关于在Delphi中对Tstringlist中的数字数据进行数字排序的主要内容,如果未能解决你的问题,请参考以下文章

Delphi TStringList的用法

delphi TStringList 用法详解

delphi TStringList 用法详解

Delphi - TStringList 用法详解

delphi字符串分割和比大小

delphi编程 文本数字排序