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
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
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
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
(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
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.
(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
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
(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
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
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
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
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
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
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
I have zero-prefixed number groups sorting consistently now. Web page
updated.
http://www.elegantcode.org/source/XPStyleSorting.html
-Gary
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