Sort filenames intelligently for numeric tokens
Closed, ResolvedPublic

Description

Add intelligent numeric-token sorting for file names instead of (or in addition to) character/byte order.

Jørgen wrote:

When I use your program, I expect a text-file sorted like here. But I get something else:

1 - File 1.txt
11 - File 11.txt
12 - File 12.txt
13 - File 13.txt
2 - File 2.txt

In Windows the files are sorted like numbers, and here are sorted as characters. Also the output files are sorted in a different manner.

Confirmed:

COMMENT	Karen's Directory Printer v5.3.2
COMMENT	Prepared: 09:33 9/26/2018
FILE	1.txt
FILE	11.txt
FILE	2.txt
FILE	New Text Document 1.txt
FILE	New Text Document 11.txt
FILE	New Text Document 2.txt

Joe created this task.Sep 26 2018, 9:37 AM
Joe created this object in space S5 Public.
Joe triaged this task as Normal priority.
Joe created this object with visibility "Public (No Login Required)".
Joe added a comment.Oct 28 2018, 7:32 PM

This sort ordering is done by a .NET function not easily available from a Visual Basic 6.0 application. So, I'll need to write a custom sort function, I think.

Joe added a comment.Oct 28 2018, 7:33 PM

OR OR!!! Bingo?

From: https://social.msdn.microsoft.com/Forums/vstudio/en-US/94f38774-e224-430f-a129-3037eef3d15e/sort-files-exactly-like-windows-explorer-by-name?forum=csharpgeneral

That order (Numerical, then Alphabetic; also called "Natural") is a special case. And invovles an uggly call to the old API function "StrCmpLogicalW":

Joe added a comment.Oct 28 2018, 7:45 PM
' 2018-10-28 JMW
Private Declare Function StrCmpLogicalW Lib "shlwapi.dll" ( _
    ByVal psz1 As String, _
    ByVal psz2 As String) As Long
Joe added a comment.Oct 28 2018, 8:04 PM

Almost there.

Joe added a comment.EditedOct 28 2018, 8:53 PM

StrCmpLogicalW does not work.

Next, try CompareStringEx function.

https://docs.microsoft.com/en-us/windows/desktop/api/stringapiset/nf-stringapiset-comparestringex

int CompareStringEx(
  LPCWSTR                          lpLocaleName,
  DWORD                            dwCmpFlags,
  _In_NLS_string_(cchCount1)LPCWCH lpString1,
  int                              cchCount1,
  _In_NLS_string_(cchCount2)LPCWCH lpString2,
  int                              cchCount2,
  LPNLSVERSIONINFO                 lpVersionInformation,
  LPVOID                           lpReserved,
  LPARAM                           lParam
);
Joe added a comment.EditedOct 29 2018, 10:53 AM

Choosing to try CompareStringW function next.

https://docs.microsoft.com/en-us/windows/desktop/api/stringapiset/nf-stringapiset-comparestringw

int CompareStringW(
  LCID                              Locale,
  DWORD                             dwCmpFlags,
  _In_NLS_string_(cchCount1)PCNZWCH lpString1,
  int                               cchCount1,
  _In_NLS_string_(cchCount2)PCNZWCH lpString2,
  int                               cchCount2
);

VB6 declaration should be:

Private Const LOCALE_SYSTEM_DEFAULT As Long = &H0080 ' Locale
Private Const SORT_DIGITSASNUMBERS As Long = 8 ' dwCmpFlags

Private Declare Function CompareStringW Lib "kernel32.dll" ( _
    Long Locale, _
    Long dwCmpFlags, _
    ByVal lpString1 As String, _
    Long cchCount1, _
    ByVal lpString2 As String, _
    Long cchCount2 ) As Long
Joe added a comment.EditedOct 29 2018, 11:41 AM

Switching to CompareStringEx

Note For compatibility with Unicode, your applications should prefer CompareStringEx or the Unicode version of CompareString. Another reason for preferring CompareStringEx is that Microsoft is migrating toward the use of locale names instead of locale identifiers for new locales, for interoperability reasons. Any application that will be run only on Windows Vista and later should use CompareStringEx.

int CompareStringEx(
  LPCWSTR                          lpLocaleName,
  DWORD                            dwCmpFlags,
  _In_NLS_string_(cchCount1)LPCWCH lpString1,
  int                              cchCount1,
  _In_NLS_string_(cchCount2)LPCWCH lpString2,
  int                              cchCount2,
  LPNLSVERSIONINFO                 lpVersionInformation,
  LPVOID                           lpReserved,
  LPARAM                           lParam
);
Joe added a comment.EditedOct 29 2018, 12:17 PM

Doh!!! I didn't read the instructions and assumed it returned the old c standard library signed value for the comparison results... This has been working.

Return Value

Returns one of the following values if successful. To maintain the C runtime convention of comparing strings, the value 2 can be subtracted from a nonzero return value. Then, the meaning of <0, ==0, and >0 is consistent with the C runtime.

The function returns 0 if it does not succeed.

Joe closed this task as Resolved.Oct 29 2018, 12:31 PM
Private Declare Function CompareStringEx Lib "kernel32.dll" ( _
    ByRef lpLocaleName As String, _
    ByVal dwCmpFlags As Long, _
    ByRef lpString1 As Byte, _
    ByVal cchCount1 As Long, _
    ByRef lpString2 As Byte, _
    ByVal cchCount2 As Long, _
    ByVal lpVersionInformation As Long, _
    ByVal lpReserved As Long, _
    ByVal lParam As Long) As Long

' Compares two strings and signed result like StrComp does
' 2019-10-29 JmW
Public Function ApiCompareStringDigitsAsNumbers(String1 As String, String2 As String) As Integer
    Const LOCALE_NAME_USER_DEFAULT = vbNullString
    Const SORT_DIGITSASNUMBERS As Long = 8 ' dwCmpFlags
        

    If CSW_OK Then
        Dim lpString1() As Byte
        Dim lpString2() As Byte
        Dim result As Long
        
        lpString1 = String1 & vbNullChar
        lpString2 = String2 & vbNullChar
        
        result = CompareStringEx( _
            LOCALE_NAME_USER_DEFAULT, _
            SORT_DIGITSASNUMBERS, _
            lpString1(0), -1, _
            lpString2(0), -1, _
            0, 0, 0)
        ' The function returns 0 if it does not succeed.
        ' Returns one of the following values if successful:
        ' CSTR_LESS_THAN, CSTR_EQUAL, CSTR_GREATER_THAN
        ' To maintain the C runtime convention of comparing strings, the value 2
        ' can be subtracted from a nonzero return value.
        ' Then, the meaning of <0, ==0, and >0 is consistent with the C runtime.
        If result <> 0 Then
            ApiCompareStringDigitsAsNumbers = result - 2
            Exit Function
        End If
       
    End If
    
    ' If something dailed or API wasn't actually there, then default to
    ' calling StrComp
    ApiCompareStringDigitsAsNumbers = StrComp(String1, String2, vbTextCompare)
End Function