How to sort alphanumeric strings numerically?

Inkhornism

New member
Joined
Nov 18, 2024
Messages
1
Programming Experience
Beginner
Hi.
I am writing a program that retrieves the names of files from a selected folder then puts the names of those files into an array as well as a combobox. The files are named identical with the the exception that they are numbered IE

XXXd1-1
XXXd1-2
XXXd1-3
XXXd1-4
etc
etc

There may also be

XXXd2-1
XXXd2-2
XXXd2-3
XXXd2-4
etc
etc

The test folder I have been using has twelve files so they are numbered 1 - 12 and are listed in order 1 - 12 in the folder. However in both the array and combobox they are listed as

XXXd2-1
XXXd2-10
XXXd2-11
XXXd2-12
XXXd2-2
XXXd2-3
XXXd2-4
tc
etc


How can I get the files listed as

XXXd1-1
XXXd1-2
XXXd1-3
XXXd1-4

.
.
.
.

XXXd2-10
XXXd2-11
XXXd2-12

I have sorted neither the array or the combobox.
 
Here's a reproduction of a post I made many years ago:

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:
VB.NET:
File1Test.txt
File2Test.txt
File3Test.txt
File4Test.txt
File5Test.txt
File10Test.txt
File20Test.txt
File30Test.txt
File40Test.txt
File50Test.txt
whereas a standard String sort would yield the following order:
VB.NET:
File1Test.txt
File10Test.txt
File2Test.txt
File20Test.txt
File3Test.txt
File30Test.txt
File4Test.txt
File40Test.txt
File5Test.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:
VB.NET:
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
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:
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
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:
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
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:
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
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.

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:
Dim folder As New DirectoryInfo(My.Computer.FileSystem.SpecialDirectories.MyDocuments)
Dim files = folder.EnumerateFiles("*.*", SearchOption.AllDirectories).
                   OrderBy(Function(fi) fi.Name,
                           New LogicalStringComparer).
                   ToArray()
 
The code above defines a dedicated class to do the comparing and that's what you should do if you want something reusable. If it's a one-off though, you may prefer to forgo the LogicalStringComparer class. You still need to declare the API function, which you can do in a module, as I have done, or somewhere else convenient, e.g. in the form in which you intend to perform the comparison. You can then use the Comparison(Of T) delegate instead, e.g.
VB.NET:
Dim folderPath = My.Computer.FileSystem.SpecialDirectories.MyDocuments
Dim filePaths = Directory.GetFiles(folderPath)

Array.Sort(files, AddressOf StrCmpLogicalW)
 
Back
Top