1

Huge amount of numeric data (from a database) is stored in a Tstringlist variable. the Data may look like:

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

By setting the sort property of the Tstringlist to true, they are sorted as texts and as a result, 13.25 shows up before 4.

How can these data be sorted numerically (and efficiently)?

(Delphi, Rad Studio 10.4)

hsh_Ar
  • 93
  • 7
  • 2
    You can use a custom sort procedure to do this. An example of using a custom sort can be found in my answer to [this question](https://stackoverflow.com/q/22812881/62576). It's going to be very complex to code, because your strings are difficult to parse to numbers. There's no way that `StrToFloat` will work with `4 1/2`, for example. – Ken White Apr 30 '21 at 03:41
  • You said "may look like". Do you have a complete list or specification of all possibilities? This specification is necessary to build a translator taking your data as input and producing a floating point number and then use that for sorting. – fpiette Apr 30 '21 at 06:17
  • If you can evaluate the entries (so that f.i. 4 1/2 evaluates to 4.5) and substitute the resulting values for the originals, then you can make the list sortable quasi-numerically by left-padding each entry with 0 (zero) to a fixed length. – MartynA Apr 30 '21 at 06:46
  • 1
    Yes. Your question is not about sorting. It's about parsing text to numeric values. Once you can do that the sort is trivial. – David Heffernan Apr 30 '21 at 06:48
  • And although @KenWhite's comment is (essentially) correct, it really isn't "very complex". I'd call it "almost trivial", but this is just nitpicking about words. – Andreas Rejbrand Apr 30 '21 at 07:04
  • Are the values stored like this in the database? – Olivier Apr 30 '21 at 07:23
  • @fpiette As mentioned, huge amount of data, between 1 and 15 in fraction forms and decimal forms. – hsh_Ar Apr 30 '21 at 14:02
  • @Olivier Yes, they are. – hsh_Ar Apr 30 '21 at 14:03

2 Answers2

3

Here is a solution:

// 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;

The conversion is done in the sorting which is not very efficient. It is probably better to create a new list with converted values and then sort it. Or convert when loading the list from database. You've got the idea...

fpiette
  • 11,983
  • 1
  • 24
  • 46
  • 1
    Yes, this is roughly how you'd do it. But you shouldn't use the global `FormatSettings` variable (that will lead to problems in large projects), and you shouldn't hardcode `32` (just skip the last argument of `Copy`). – Andreas Rejbrand Apr 30 '21 at 07:02
  • @AndreasRejbrand OK, boss! Edited my answer :-) – fpiette Apr 30 '21 at 07:33
1

enter image description here

Two components solve your problem:

  1. StrCmpLogical is your friend. It uses Natural sorting - Digits in the strings are considered as numerical content rather than text.
  2. And this SO post How do I enter fractions in Delphi?

I put it together below:

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://stackoverflow.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.
Lars
  • 1,428
  • 1
  • 13
  • 22
  • It almost works with a few exceptions like: the `1.9` is sorted as lower than `1.66` and `2.063` is sorted higher than `2 3/8` and `2 7/8` – hsh_Ar Apr 30 '21 at 14:39
  • 1
    @hmdknight `StrCmpLogical()` is not your friend here. Just format the number with left padding and use regular string sort. – Olivier Apr 30 '21 at 15:32