C# version here.
People often ask how to sort alphanumeric Strings logically. It is usually, although not exclusively, in the context of sorting file names. That's because Windows File Explorer does just that. As an example, Windows File Explorer would sort the following file names in the following order:
whereas a standard String sort would yield the following order:Code:File1Test.txt File2Test.txt File3Test.txt File4Test.txt File5Test.txt File10Test.txt File20Test.txt File30Test.txt File40Test.txt File50Test.txt
Windows File Explorer achieves this by using the StrCmpLogicalW API function and you can do the same. The first step is to use Platform Invoke to declare this function in your .NET code so that you can invoke the unmanaged function:Code:File1Test.txt File10Test.txt File2Test.txt File20Test.txt File3Test.txt File30Test.txt File4Test.txt File40Test.txt File5Test.txt File50Test.txt
Sorting is done in various specific ways but they all come down to comparing pairs of values and swapping them if required. You can provide the code to perform those comparisons yourself and use the API function above:vb.net Code:
Imports System.Runtime.InteropServices Public Module NativeMethods <DllImport("shlwapi.dll", CharSet:=CharSet.Unicode)> Public Function StrCmpLogicalW(x As String, y As String) As Integer End Function End Module
That class follows the convention of implementing both when there is a generic and non-generic version of an interface. As is done above, you should declare the non-generic method Private and have it call the generic method. That way, those who use the class directly will only have access to the generic method but code that uses a reference of the non-generic interface type will still have access to the non-generic method. More on that later. Note that you may prefer to declare the API function inside that class if that's the only place you plan to use it:vb.net Code:
Public Class LogicalStringComparer Implements IComparer, IComparer(Of String) Private Function Compare(x As Object, y As Object) As Integer Implements IComparer.Compare Return Compare(CStr(x), CStr(y)) End Function Public Function Compare(x As String, y As String) As Integer Implements IComparer(Of String).Compare Return NativeMethods.StrCmpLogicalW(x, y) End Function End Class
You can then use that class to perform the comparisons wherever an IComparer or IComparer(Of String) is expected. Here is some example code that uses it to sort a String array, an ArrayList, a List(Of String) and a LINQ query producing an IEnumerable(Of String):vb.net Code:
Imports System.Runtime.InteropServices Public Class LogicalStringComparer Implements IComparer, IComparer(Of String) <DllImport("shlwapi.dll", CharSet:=CharSet.Unicode)> Private Shared Function StrCmpLogicalW(x As String, y As String) As Integer End Function Private Function Compare(x As Object, y As Object) As Integer Implements IComparer.Compare Return Compare(CStr(x), CStr(y)) End Function Public Function Compare(x As String, y As String) As Integer Implements IComparer(Of String).Compare Return StrCmpLogicalW(x, y) End Function End Class
I've included the ArrayList because it demonstrates that, even though the method that implements IComparer.Compare is Private, it is still accessible where a reference of type IComparer is used. The parameter of the ArrayList.Sort method is type IComparer so, inside that method, it is actually that Private overload of LogicalStringComparer.Compare that is being invoked.vb.net Code:
Module Module1 Sub Main() Dim fileNames = {"File1Test.txt", "File2Test.txt", "File3Test.txt", "File4Test.txt", "File5Test.txt", "File10Test.txt", "File20Test.txt", "File30Test.txt", "File40Test.txt", "File50Test.txt"} Dim rng As New Random SortArray(fileNames.OrderBy(Function(s) rng.NextDouble()).ToArray()) SortArrayList(New ArrayList(fileNames.OrderBy(Function(s) rng.NextDouble()).ToArray())) SortList(New List(Of String)(fileNames.OrderBy(Function(s) rng.NextDouble()))) SortQuery(fileNames.OrderBy(Function(s) rng.NextDouble()).ToArray()) Console.ReadLine() End Sub Private Sub SortArray(arr As String()) Console.WriteLine("Array before:") For Each s In arr Console.WriteLine(s) Next Console.WriteLine() Array.Sort(arr, New LogicalStringComparer) Console.WriteLine("Array after:") For Each s In arr Console.WriteLine(s) Next Console.WriteLine() End Sub Private Sub SortArrayList(al As ArrayList) Console.WriteLine("ArrayList before:") For Each s In al Console.WriteLine(s) Next Console.WriteLine() al.Sort(New LogicalStringComparer) Console.WriteLine("ArrayList after:") For Each s In al Console.WriteLine(s) Next Console.WriteLine() End Sub Private Sub SortList(lst As List(Of String)) Console.WriteLine("List before:") For Each s In lst Console.WriteLine(s) Next Console.WriteLine() lst.Sort(New LogicalStringComparer) Console.WriteLine("List after:") For Each s In lst Console.WriteLine(s) Next Console.WriteLine() End Sub Private Sub SortQuery(arr As String()) Console.WriteLine("Query before:") For Each s In arr Console.WriteLine(s) Next Console.WriteLine() Dim qry = arr.OrderBy(Function(s) s, New LogicalStringComparer) Console.WriteLine("Query after:") For Each s In qry Console.WriteLine(s) Next Console.WriteLine() End Sub End Module
Note that the LINQ OrderBy method that accepts an IComparer(Of T) as an argument also requires you to specify a selector for the sort key of type T. In the example above, we're sorting Strings so the key is the item itself. In other cases, the sort key might be a property of the item, e.g. the Name property of a FileInfo object:
vb.net Code:
Dim folder As New DirectoryInfo(My.Computer.FileSystem.SpecialDirectories.MyDocuments) Dim files = folder.EnumerateFiles("*.*", SearchOption.AllDirectories). OrderBy(Function(fi) fi.Name, New LogicalStringComparer). ToArray()


Reply With Quote