Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

string sorting: emulating the change in how Explorer sorts files by name

21 views
Skip to first unread message

Gary Williams

unread,
Nov 14, 2003, 10:23:43 AM11/14/03
to
I've noticed a change in Explorer's sorting algorithm.

Under Windows 2000, one would see files sorted by name this way:

A100
A20
A3
B100
B20
B3

Under Windows XP, one would see the same files sorted by name this way:

A3
A20
A100
B3
B20
B100

Does anyone know of a string sort-compare function that uses this new
sorting algorithm? I would prefer to not rely on an API call that doesn't
exist in prior versions of Windows.

-Gary


Jim Kueneman

unread,
Nov 14, 2003, 9:35:50 AM11/14/03
to

Hi,

>
> Does anyone know of a string sort-compare function that uses this new
> sorting algorithm? I would prefer to not rely on an API call that
> doesn't exist in prior versions of Windows.

http://support.microsoft.com/default.aspx?kbid=319827


--
Jim

Posted with XanaNews 1.15.6.3

Gary Williams

unread,
Nov 14, 2003, 10:57:35 AM11/14/03
to

Jim Kueneman wrote:

> I wrote:
> > Does anyone know of a string sort-compare function that uses this new
> > sorting algorithm? I would prefer to not rely on an API call that
> > doesn't exist in prior versions of Windows.
>
> http://support.microsoft.com/default.aspx?kbid=319827
>

Thanks for the link, however the article's content doesn't get me any closer
to my goal of emulating this behavior for strings in Delphi code.

-Gary


Marc Rohloff

unread,
Nov 14, 2003, 11:34:34 AM11/14/03
to
On Fri, 14 Nov 2003 09:57:35 -0600, Gary
Williams<2FC5...@garywilliams.us> said ...

> Thanks for the link, however the article's content doesn't get me any closer
> to my goal of emulating this behavior for strings in Delphi code.

Maybe:

[untested]
function XPCompare(const s1, s2:string):integer;
procedure splitstring(const s:string;
out left:string; out right:Int64);
var i:integer;
begin
left := '';
right := 0;

for i := length(s) downto 1 do
if not s[i] in ['0'..'9'] then
begin
if i<length(s) then
right := strtoint64(copy(s,i+1,length(s));
left := copy(s,1,i);
end;

//All numbers
right := strtoint64(s);
end;

var l1,l2:string; r1,r2:int64;
begin
splitstring(s1,l1,r1);
splitstring(s2,l2,r2);
result := comparetext(l1,l2);
if result=0
then result := r1-r2;
end;

This will fail for very big numbers (more than 18 digits) and doesn't
handle negatives.

Marc

Gary Williams

unread,
Nov 14, 2003, 12:01:51 PM11/14/03
to

Marc Rohloff wrote:

> Gary Williams wrote:
> > Thanks for the link, however the article's content doesn't get me any
closer
> > to my goal of emulating this behavior for strings in Delphi code.
>
> Maybe:
>
> [untested]

(code removed)

This solution doesn't appear to be suited to the general case, which may
have multiple groups of digits and non-digits.

However, I have a solution in mind.

-Gary


Jim Kueneman

unread,
Nov 14, 2003, 12:07:06 PM11/14/03
to

Hi,


> Thanks for the link, however the article's content doesn't get me any
> closer to my goal of emulating this behavior for strings in Delphi
> code.


Yes I knew you were going to ask <G>. This is the "right" way to do it.


unit Unit1;

interface

uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
StdCtrls;

type
TForm1 = class(TForm)
Button1: TButton;
ListBox1: TListBox;
Edit1: TEdit;
Label1: TLabel;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

type
TFolderContent = (
fcFiles, // Include all Files
fcFolders, // Include all Folders
fcHidden // Include all hidden objects
);
TFolderContents = set of TFolderContent;

TFileResult = (
FileName, // Return a list of filenames
Path // Return a list of complete file paths
);

const
AllFolderContent = [fcFiles, fcFolders, fcHidden];

var
Form1: TForm1;

implementation

uses
ShellAPI, ShlObj, ActiveX;

{$R *.dfm}

var
SortFolder: IShellFolder;
SortColumn: Integer;

function ShellCompare(Item1, Item2: Pointer): Integer;
begin
Result := 0;
if Assigned(SortFolder) then
Result := ShortInt(SortFolder.CompareIDs(SortColumn, Item1, Item2))
end;

function PathToPIDL(APath: WideString): PItemIDList;
// Takes the passed Path and attempts to convert it to the equavalent
PIDL
var
Desktop: IShellFolder;
pchEaten, dwAttributes: ULONG;
begin
Result := nil;
SHGetDesktopFolder(Desktop);
dwAttributes := 0;
if Assigned(Desktop) then
Desktop.ParseDisplayName(0, nil, PWideChar(APath), pchEaten,
Result, dwAttributes)
end;

function StrRetToStr(StrRet: TStrRet; APIDL: PItemIDList; const Malloc:
IMalloc): WideString;
{ Extracts the string from the StrRet structure.
}
var
P: PChar;
// S: string;
begin
case StrRet.uType of
STRRET_CSTR:
begin
SetString(Result, StrRet.cStr, lStrLen(StrRet.cStr));
// Result := S
end;
STRRET_OFFSET:
begin
if Assigned(APIDL) then
begin
{$R-}
P := PChar(@(APIDL).mkid.abID[StrRet.uOffset -
SizeOf(APIDL.mkid.cb)]);
{$R+}
SetString(Result, P, StrLen(P));
// Result := S;
end else
Result := '';
end;
STRRET_WSTR:
begin
Result := StrRet.pOleStr;
if Assigned(StrRet.pOleStr) then
Malloc.Free(StrRet.pOLEStr);
end;
end;
end;

function GetDirectoryFolder(Directory: WideString): IShellFolder;
var
Desktop: IShellFolder;
pchEaten, dwAttributes: ULONG;
PIDL: PItemIDList;
begin
SHGetDesktopFolder(Desktop);
if Assigned(Desktop) then
begin
PIDL := nil;
Desktop.ParseDisplayName(0, nil, PWideChar(Directory), pchEaten,
PIDL, dwAttributes);
if Assigned(PIDL) then
begin
Desktop.BindToObject(PIDL, nil, IShellFolder, Result);
CoTaskMemFree(PIDL);
end
end
end;

procedure EnumFolder(Folder: IShellFolder; Contents: TFolderContents;
PIDLList: TList);
var
Flags: Longword;
EnumList: IEnumIDList;
Fetched: ULONG;
PIDL: PItemIDList;
begin
Flags := 0;
if fcFiles in Contents then
Flags := Flags or SHCONTF_NONFOLDERS;
if fcFolders in Contents then
Flags := Flags or SHCONTF_FOLDERS;
if fcHidden in Contents then
Flags := Flags or SHCONTF_INCLUDEHIDDEN;

Folder.EnumObjects(0, Flags, EnumList);
if Assigned(EnumList) then
begin
while EnumList.Next(1, PIDL, Fetched) <> S_FALSE do
PIDLList.Add(PIDL)
end
end;

procedure GetDirectoryContents(Directory: WideString; Contents:
TFolderContents;
FileResult: TFileResult; SortOnColumn: Integer; FileList:
TStringList);
// Parameters:
// Directory: Path of the directory to get the contents of
// Contents: What type of objects on the folder to include
// FileResult: Return only the file names or the complete path for
each file
// SortOnColumn: What column (in Explorer report view) to sort the
item on, 0 is the name
// FileList: The resulting file list user allocated
var
Folder: IShellFolder;
PIDLList: TList;
i: Integer;
Malloc: IMalloc;
Flags: Longword;
StrRet: TStrRet;
begin
Assert(Assigned(FileList), 'User must allocate the FileString List in
GetDirectoryContents');
Folder := GetDirectoryFolder(Directory);
if Assigned(Folder) then
begin
SHGetMalloc(Malloc);
PIDLList := TList.Create;
try
EnumFolder(Folder, Contents, PIDLList);
SortFolder := Folder;
SortColumn := SortOnColumn;
PIDLList.Sort(ShellCompare);
// Release the count on the interface
SortFolder := nil;
FileList.Capacity := PIDLList.Count;
if FileResult = FileName then
Flags := SHGDN_NORMAL
else
Flags := SHGDN_FORPARSING;

for i := 0 to PIDLList.Count - 1 do
begin
FillChar(StrRet, SizeOf(StrRet), #0);
if Folder.GetDisplayNameOf(PIDLList[i], Flags, StrRet) =
NOERROR then
FileList.Add(StrRetToStr(StrRet, PIDLList[i], Malloc));
end
finally
for i := 0 to PIDLList.Count - 1 do
Malloc.Free(PIDLList[i]);
PIDLList.Free
end
end;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
Files: TStringList;
begin
Files := TStringList.Create;
GetDirectoryContents(Edit1.Text, AllFolderContent, Path, 0, Files);
ListBox1.Items.Assign(Files);
Files.Free;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
Label1.Caption := 'Enter a Directory';
Edit1.Text := 'c:\';
end;

end.

Gary Williams

unread,
Nov 14, 2003, 1:30:30 PM11/14/03
to

Jim Kueneman wrote:
> > Thanks for the link, however the article's content doesn't get me any
> > closer to my goal of emulating this behavior for strings in Delphi
> > code.
>
> Yes I knew you were going to ask <G>. This is the "right" way to do it.

(code cut)

I appreciate this, however I devised my own solution which I think is
cleaner, not Windows-specific, and not dependent on the strings representing
files. I will upload my code to a web page and post the URL here.

-Gary


Marc Rohloff

unread,
Nov 14, 2003, 1:10:08 PM11/14/03
to
On Fri, 14 Nov 2003 11:01:51 -0600, Gary
Williams<2FC5...@garywilliams.us> said ...

> This solution doesn't appear to be suited to the general case, which may
> have multiple groups of digits and non-digits.
Does XP handle the general case or only the trailing digits case?

Marc

Rob Kennedy

unread,
Nov 14, 2003, 1:56:02 PM11/14/03
to
Marc Rohloff wrote:
> Does XP handle the general case or only the trailing digits case?

It handles the general case, as illustrated in the Knowledge Base
article Jim mentioned.

The basic algorithm I imagine goes like this:

function CompareFileNames(const FileName1, FileName2: string): Integer;
var
Segments1, Segments2: array of string;
Chunks1, Chunks2: array of string;
N1, N2: Integer;
i, j: Integer;
begin
Result := 0;
Segments1 := Split(FileName1, '.');
Segments2 := Split(FileName2, '.');

for i := 0 to High(Segments1) do begin
Chunks1 := NumericSplit(Segments1[i]);
Chunks2 := NumericSplit(Segments2[i]);

for j := 0 to High(Chunks1) do begin
if IsNumeric(Chunks1[j]) and IsNumeric(Chunks2[j]) then begin
N1 := StrToInt(Chunks1[j]);
N2 := StrToInt(Chunks2[j]);
Result := N2 - N1;
end else begin
Result := Windows.CompareString(Chunks1[j], Chunks2[j]);
end;
if Result <> 0 then exit;
end;
end;
end;

In the code above, Split would create an array of strings representing
the segments of the file name. Most of the time, it would be equivalent
to using ExtractFileName and ExtractFileExt.

NumericSplit would be a little more complicated. From the KB example,
"Ie401sp2" would have four chunks: Ie, 401, sp, and 2. Each chunk would
be compared separately.

The function would have account for the case when the arrays aren't the
same length, in which case the file name with the shorter corresponding
array would be placed first. I indicate to use the CompareString API
function because it handles nonalphanumeric characters in a special way.
(It has lots more parameters, and its return value is funny; just
imagine that I called it correctly.)

--
Rob

Gary Williams

unread,
Nov 14, 2003, 8:43:36 PM11/14/03
to

My solution is available at this URL:

http://www.elegantcode.org/source/XPStyleSorting.html

-Gary


Gary Williams

unread,
Nov 14, 2003, 8:52:19 PM11/14/03
to

"Rob Kennedy" <.> wrote in message news:3fb52539$1...@newsgroups.borland.com...

> Marc Rohloff wrote:
> > Does XP handle the general case or only the trailing digits case?
>
> It handles the general case, as illustrated in the Knowledge Base
> article Jim mentioned.
>
> The basic algorithm I imagine goes like this:

(algorithm snipped)

One limitation of your algorithm and Marc's is that they will both fail for
long runs of digits. My solution first compares the lengths of the runs of
digits; if they are of different lengths then the larger one is obvious. If
they are the same length, then a text comparison will suffice.

> The function would have account for the case when the arrays aren't the
> same length, in which case the file name with the shorter corresponding
> array would be placed first.

Actually, XP will sort 'A1' before 'A'. Believe it or not! My solution has
a boolean constant that can toggle this behavior on or off.

> I indicate to use the CompareString API
> function because it handles nonalphanumeric characters in a special way.
> (It has lots more parameters, and its return value is funny; just
> imagine that I called it correctly.)

I'll look into that.

My solution is at the following URL if you are interested.

http://www.elegantcode.org/source/XPStyleSorting.html

-Gary


Dr John Stockton

unread,
Nov 15, 2003, 2:10:04 PM11/15/03
to
JRS: In article <3fb586ce$3...@newsgroups.borland.com>, seen in news:borla
nd.public.delphi.language.objectpascal, Gary Williams
<0808...@garywilliams.us> posted at Fri, 14 Nov 2003 19:52:19 :-

>
>One limitation of your algorithm and Marc's is that they will both fail for
>long runs of digits. My solution first compares the lengths of the runs of
>digits; if they are of different lengths then the larger one is obvious. If
>they are the same length, then a text comparison will suffice.

Test whether 00003 should be greater than 24.

I would prefer to need all digit fields to be space-padded to some
maximum length, then sorted "alphabetically"; there should be consistent
sorting of 3, 03, 003, ..., which that will give.

--
© John Stockton, Surrey, UK. ?@merlyn.demon.co.uk Delphi 3 Turnpike 4 ©
<URL:http://www.merlyn.demon.co.uk/> TP/BP/Delphi/&c., FAQqy topics & links;
<URL:http://www.bancoems.com/CompLangPascalDelphiMisc-MiniFAQ.htm> clpdmFAQ;
<URL:http://www.borland.com/newsgroups/guide.html> news:borland.* Guidelines

Jim Kueneman

unread,
Nov 15, 2003, 8:31:00 PM11/15/03
to

Hi,


> I appreciate this, however I devised my own solution which I think is
> cleaner, not Windows-specific, and not dependent on the strings
> representing files. I will upload my code to a web page and post the

If you want it not windows specific why care how XP sorts it?

--
Jim

Posted with XanaNews 1.15.7.4

Gary Williams

unread,
Nov 15, 2003, 9:27:47 PM11/15/03
to

Dr John Stockton wrote:

> Gary Williams wrote:
> >One limitation of your algorithm and Marc's is that they will both fail
for
> >long runs of digits. My solution first compares the lengths of the runs
of
> >digits; if they are of different lengths then the larger one is obvious.
If
> >they are the same length, then a text comparison will suffice.
>
> Test whether 00003 should be greater than 24.

In XP's Windows Explorer, 00003 sorts after 24. I have a boolean constant
in my sort routine that controls whether or not leading zeroes should be
ignored so this counterintuitive behavior can be avoided if desired.

-Gary


Rob Kennedy

unread,
Nov 15, 2003, 11:18:29 PM11/15/03
to
Gary Williams wrote:
> In XP's Windows Explorer, 00003 sorts after 24.

That's not the order I've seen. On my computer, 00003 comes before 24.
But that's consistent with the classic sort order, too. The key is that
00003 sorts after 2, which is not how a classic sort would put it.

--
Rob

Gary Williams

unread,
Nov 15, 2003, 11:48:41 PM11/15/03
to

Ack. You're right.

XP sorts thusly:

01
1
02
2
00003
24

It is easy to modify my function to ignore the zeroes, but then extra code
will be needed to guarantee that 01 will sort before 1.

-Gary


Gary Williams

unread,
Nov 16, 2003, 2:25:07 PM11/16/03
to

Jim Kueneman wrote:
> > I appreciate this, however I devised my own solution which I think is
> > cleaner, not Windows-specific, and not dependent on the strings
> > representing files. I will upload my code to a web page and post the
>
> If you want it not windows specific why care how XP sorts it?

I'm not using this code for sorting files, but for something else entirely.

One of the applications I help to maintain allows users to define objects
(which represent physical hardware specified by an architect) and these
objects are typically named something like XXX-### where XXX is an
alphabetic string and ### is a numeric string. Our users want XXX-9 to sort
before XXX-10. They do not want to use XXX-09, since the leading zero
wouldn't serve any purpose in the real world and would it be silly to force
the user to enter it just for sorting purposes.

-Gary


Gary Williams

unread,
Nov 18, 2003, 7:34:26 PM11/18/03
to

Dr John Stockton wrote:
> Test whether 00003 should be greater than 24.
>
> I would prefer to need all digit fields to be space-padded to some
> maximum length, then sorted "alphabetically"; there should be consistent
> sorting of 3, 03, 003, ..., which that will give.

I have zero-prefixed number groups sorting consistently now. Web page
updated.

http://www.elegantcode.org/source/XPStyleSorting.html

-Gary


Pajamas

unread,
Nov 28, 2003, 3:07:00 PM11/28/03
to

Thanks guys.

I was quite content to believe that noone would ever
want to sort a list like I've seen in this thread.
I'll try to keep a more open mind in the future.

Jim L

0 new messages