5

I have a TStringList of Name/Value pairs. The names are all integer values 9stored as strings of course) and the values are all strings (comma separated).

e.g.

5016=Catch the Fish!,honeyman,0
30686=Ozarktree1 Goes to town,ozarktreel,0

. . .

I would like to call the add routine and add new lines in the TStringlist, but need a way to sort the list afterwards.

e.g.

Tags.Add(frmTag.edtTagNo.Text + '=' +
         frmTag.edtTitle.Text + ',' +
         frmTag.edtCreator.Text + ',' +
         IntToStr(ord(frmTag.cbxOwned.Checked)));
Tags.Sort;

Here is what I tried:

Tags:= TStringList.Create;
 Tags.CustomSort(StringListSortComparefn);
 Tags.Sorted:= True;

my custom sort routine:

function StringListSortComparefn(List: TStringList; Index1, Index2: Integer): Integer;
var
 i1, i2 : Integer;
begin
 i1 := StrToIntDef(List.Names[Index1], 0);
 i2 := StrToIntDef(List.Names[Index2], 0);
 Result:= CompareValue(i1, i2);
end;

However, it still seems to be sorting them like strings instead of integers.

I even tried creating my own class:

type
 TXStringList = class(TStringList)
 procedure Sort;override;
end;

implementation

function StringListSortComparefn(List: TStringList; Index1, Index2: Integer): Integer;
var
i1, i2 : Integer;
begin
i1 := StrToIntDef(List.Names[Index1], 0);
i2 := StrToIntDef(List.Names[Index2], 0);
Result:= CompareValue(i1, i2);
end;

procedure TXStringList.Sort;
begin
 CustomSort(StringListSortComparefn);
end;

I even tried some examples on SO (e.g. Sorting TSTringList Names property as integers instead of strings)

Can someone tell me what I am doing wrong? Everytime, the list gets sorted as strings and not as integers.

30686=Ozarktree1 Goes to town,ozarktreel,0
5016=Catch the Fish!,honeyman,0
Community
  • 1
  • 1
JakeSays
  • 1,898
  • 6
  • 27
  • 39

3 Answers3

6

You can do a simple integer subtraction:

function StringListSortComparefn(List: TStringList; Index1, Index2: Integer): Integer;
var
 i1, i2 : Integer;
begin
 i1 := StrToIntDef(List.Names[Index1], 0);
 i2 := StrToIntDef(List.Names[Index2], 0);
 Result := i1 - i2
end;

To reverse the sort order, simply reverse the operands in the subtraction:

Result := i2 - i1;

Here's a quick, compiilable console example:

program Project2;

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes;

function StringListSortProc(List: TStringList; Index1, Index2: Integer): Integer;
var
  i1, i2: Integer;
begin
  i1 := StrToIntDef(List.Names[Index1], -1);
  i2 := StrToIntDef(List.Names[Index2], -1);
  Result := i1 - i2;
end;

var
  SL: TStringList;
  s: string;
begin
  SL := TStringList.Create;
  SL.Add('3456=Line 1');
  SL.Add('345=Line 2');
  SL.Add('123=Line 3');
  SL.Add('59231=Line 4');
  SL.Add('545=Line 5');
  WriteLn('Before sort');
  for s in SL do
    WriteLn(#32#32 + s);
  SL.CustomSort(StringListSortProc);
  WriteLn('');
  WriteLn('After sort');
  for s in SL do
    WriteLn(#32#32 + s);
  ReadLn;
  SL.Free;
end.

And the resulting output:

Before sort
  3456=Line 1
  345=Line 2
  123=Line 3
  59231=Line 4
  545=Line 5

After sort
  123=Line 3
  345=Line 2
  545=Line 5
  3456=Line 1
  59231=Line 4
Ken White
  • 117,855
  • 13
  • 197
  • 405
  • 1
    That isn't really a valid test. You'd get the same results even without the custom sorting function because `'1' < '3' < '5'`. Furthermore, your answer seems to implicitly say that the problem in the original code is `CompareValue`. Are you willing to say that explicitly? Can you *show* that that's really the problem? – Rob Kennedy Apr 02 '14 at 13:42
  • @Rob: Added two additional items to the stringlist that demonstrates that the sort does work properly. Thanks for pointing out that better datum was needed. While I can't definitively say that `CompareValue` is the problem, I can definitively say that a) a function call is not needed here to do the comparison, and b) I've successfully used the above method of sorting textual representations of numbers in TStringList for years now without issues. – Ken White Apr 02 '14 at 13:48
  • 1
    @KenWhite, I tried your answer. and these are the results: My first item in list is "10000=Aarshark Man,Zanty,0", when in fact, the first item should be "5016=Catch the Fish!,honeyman,0" and the last one should be "30771=zombie-sonar,tfrog316,0" but instead, my last one is: "9999=Happy Campers,zicky,0". So, it seems that it is still sorting as Strings: Here is my code: Tags:= TStringList.Create; Tags.CustomSort(StringListSortProc); Tags.Sorted:= True; Tags.LoadFromFile(AppPath + 'PTags.dat'); Tags.Sort; ShowMessage(Tags[0]); - Is any of this code wrong? – JakeSays Apr 02 '14 at 14:16
  • Un, no. It can't be, unless your values are not in fact numbers. Please edit your question to provide a specific list of items that I can just copy and paste from there. You do know that you have to call `CustomSort` after each and every item is added to the list if you want things sorted "on the fly", right? It's typical to add all items and then call CustomSort once at the end; it doesn't replace the default sort simply by setting the TStringList.Sorted property to True. – Ken White Apr 02 '14 at 14:18
  • The items in the list are irrelevant. The method of comparison is, too. It's the surrounding code that's leading to this problem. – Rob Kennedy Apr 02 '14 at 14:19
  • I found the issue! I was calling the custom compare method before the load when it should have been called after the load! – JakeSays Apr 02 '14 at 14:20
  • @KenWhite CompareValue works perfectly well. In fact it is better than subtraction because it uses the comparison operators. The reason being that subtraction can lead to overflow. The comparison operators never do. This answer, whilst accepted, does not actually explain the reported problem. – David Heffernan Apr 02 '14 at 15:00
  • @David: I've already upvoted Rob's answer (see my comment to it). There's no need for CompareValue here (I'd be interested in two values that are likely to occur that would cause the overflow, BTW, that wouldn't also be issues for the two preceeding StrToIntDef calls), but the issue is indeed in the surrounding code. I'd prefer that the poster award the accept to Rob instead, as he found the actual issue. – Ken White Apr 02 '14 at 15:02
  • `i1-i2` when `i1` is `MaxInt` and `i2` is `-1`. – David Heffernan Apr 02 '14 at 15:25
  • @David: I said "likely to occur" in the context of the requirements here. Which items in this list are either negative or very near to maxint? I can come up with highly unlikely examples of when almost anything can fail; doing so doesn't make it realistically probable. "I need to store two digit numbers, and will not need to ever store three because the standard I'm writing code to comply with allows only two." "Yeah, but what if 25 years from now they change the standard? Better code against it now." – Ken White Apr 02 '14 at 15:29
  • @KenWhite So you think that subtraction which can fail due to overflow, is preferable to `CompareValue` which works for all possible arguments? I don't. I think `CompareValue` is simply better. – David Heffernan Apr 02 '14 at 15:30
  • @David: I think the function call on every comparison when it's not necessary is wasteful of CPU cycles. You're entitled to your opinion, as am I to mine. – Ken White Apr 02 '14 at 15:33
  • @KenWhite Perhaps you should add that perf reason to your answer. – David Heffernan Apr 02 '14 at 15:36
  • @David: I'm waiting now for the poster to move the accept to one of the other questions so I can delete my answer. (I've already conceded that it wasn't relevant to the actual problem here at least twice.) There's no point in editing in any content when I intend to delete the answer anyway. – Ken White Apr 02 '14 at 15:38
  • @KenWhite OK, I understand. Thanks. – David Heffernan Apr 02 '14 at 15:44
4

The question is, do you require the list to remain sorted? Or is it sufficient to sort it at the end, after all the items have been added.

If you just need to be able to sort the list as needed, you're first example is almost correct. You just need to call CustomSort at the end, after your items have been added.

  Tags := tStringList . Create;
  Tags . Add ( '5016=Catch the Fish!,honeyman,0' );
  Tags . Add ( '30686=Ozarktree1 Goes to town,ozarktreel,0' );
  Tags.CustomSort(StringListSortComparefn);

If you need the list to stay sorted, then you need to override CompareStrings.

type
 TXStringList = class(TStringList)
    function CompareStrings(const S1, S2: string): Integer; override;
end;

function NumberOfNameValue ( const S : string ) : integer;
begin
  Result := StrToIntDef(copy(S,1,pos('=',S)-1), 0);
end;

function txStringList . CompareStrings ( const S1, S2 : string ) : integer;
var
 i1, i2 : Integer;
begin
 i1 := NumberOfNameValue ( S1 );
 i2 := NumberOfNameValue ( S2 );
 Result:= CompareValue(i1, i2);
end;


begin
  Tags := txstringlist . Create;
  Tags . Sorted := true;
  Tags . Add ( '5016=Catch the Fish!,honeyman,0' );
  Tags . Add ( '30686=Ozarktree1 Goes to town,ozarktreel,0' );
 // List will be correctly sorted at this point. 
end;
David Dubois
  • 3,692
  • 3
  • 15
  • 35
4

The CustomSort command is a one-time operation. You appear to be using it as though you're setting a property so that further sorting will use the custom comparison function, but that's not really how it works. It sorts the (newly created, empty) list once. Then, when you set the Sorted property, you re-sort the list using the default comparison, and you specify that any further additions to the list should be inserted using that default sort order.

When you override the Sort method, you're a little closer to a solution, but insertions to a sorted list (where Sorted=True) do not actually call Sort! Instead, they perform a binary search for the correct insertion location and then insert there. Instead of overriding Sort, you could try overriding CompareStrings:

type
  TXStringList = class(TStringList)
  protected
    function CompareStrings(const S1, S2: string): Integer; override;
  end;

function TXStringList.CompareStrings(const S1, S2: string): Integer;
var
  i1, i2, e1, e2: Integer;
begin
  Val(S1, i1, e1);
  Assert((e1 = 0) or (S1[e1] = NameValueSeparator));
  Val(S2, i2, e2);
  Assert((e2 = 0) or (S2[e2] = NameValueSeparator));
  Result := CompareValue(i1, i2);
end;

Beware that this will break the IndexOf method, though. It might also break Find, but you might want that, depending on how you want to treat elements with the same numeric key. (Find is what's used to locate the correct insertion point of a sorted list, and with the above code, it would treat all elements with the same key as equal.) They all use CompareStrings just like Sort does.

David Heffernan
  • 572,264
  • 40
  • 974
  • 1,389
Rob Kennedy
  • 156,531
  • 20
  • 258
  • 446
  • +1. Nice catch. I missed the setting of the `Sorted` property immediately after the call to `CustomSort` in the question's code. – Ken White Apr 02 '14 at 14:21
  • Just to be clear, because @David seems to have missed it: I upvoted Rob's answer because it correctly identifies the issue (as I stated in my previous comment). Rob should be the one getting the accept here, instead of me. – Ken White Apr 02 '14 at 15:06
  • @Ken I've not missed that. The answer from user is also good. – David Heffernan Apr 02 '14 at 15:13
  • @David: Apparently, you did based on the comment you added to my answer. I wanted to make it clear to you (and others) that I agree Rob's answer is more appropriate to the problem than mine. – Ken White Apr 02 '14 at 15:20
  • @KenWhite No. My comment to your answer was purely about the content of that answer. It makes no reference to the other answers. – David Heffernan Apr 02 '14 at 15:21