Search code examples
.netvb.netsortingicomparable

Sort Missing and Duplicate Ordinal Numbers Using IComparable


I am tasked with writing code to use IComparable with our custom type (Product).

The Product type has a property called OrdinalNumber which is the property being compared for the sort operation. An example of the ordinals can be something like this: 1 2 3 3 5 6 7 7 9 etc. The routine must replace duplicate or missing ordinals and sort the results.

I have found this example and have it working. https://msdn.microsoft.com/en-us/library/w56d4y5z(v=vs.110).aspx

Can someone please help me?

I don't want to sort a DataTable. We currently have code that sorts a DataTable, we want to rewrite that code to use a custom type called Product. The example code I have pasted here uses the new Product class.

Here is what I have so far:

Imports System.Collections.Generic

Public Class Product
    Implements IEquatable(Of Product)
    Implements IComparable(Of Product)
    Public Property ProductName() As String
        Get
            Return m_ProductName
        End Get
        Set(value As String)
            m_ProductName = value
        End Set
    End Property
    Private m_ProductName As String

    Public Property ProductId() As Integer
        Get
            Return m_ProductId
        End Get
        Set(value As Integer)
            m_ProductId = value
        End Set
    End Property
    Private m_ProductId As Integer

    Public Property OrdinalNumber() As Integer
        Get
            Return m_OrdinalNumber
        End Get
        Set(value As Integer)
            m_OrdinalNumber = value
        End Set
    End Property
    Private m_OrdinalNumber As Integer

    Public Overrides Function ToString() As String
        Return "Ordinal Number: " & OrdinalNumber & "   ID: " & ProductId & "   Name: " & ProductName
    End Function

    Public Overrides Function Equals(obj As Object) As Boolean
        If obj Is Nothing Then
            Return False
        End If
        Dim objAsProduct As Product = TryCast(obj, Product)
        If objAsProduct Is Nothing Then
            Return False
        Else
            Return Equals(objAsProduct)
        End If
    End Function

    Public Function SortByNameAscending(name1 As String, name2 As String) As Integer

        Return name1.CompareTo(name2)
    End Function

    Public Function CompareTo(compareProduct As Product) As Integer _
            Implements IComparable(Of Product).CompareTo

        If compareProduct Is Nothing Then
            Return 1
        Else

            Return Me.OrdinalNumber.CompareTo(compareProduct.OrdinalNumber)
        End If
    End Function

    Public Overrides Function GetHashCode() As Integer
        Return OrdinalNumber
    End Function

    Public Overloads Function Equals(other As Product) As Boolean Implements IEquatable(Of Product).Equals
        If other Is Nothing Then
            Return False
        End If
        Return (Me.OrdinalNumber.Equals(other.OrdinalNumber))
    End Function

End Class

Solution

  • I am not sure why there is such focus on the Bingo variation (now redacted via edit) of a SelectionSort. The Bingo version is slightly faster version of SelectionSort when there are many duplicate values but only marginally so. It is not a sorter specialized for cases when there are some dupes. They are both slow.

    How would I implement a selection or "bingo" sort using List.Sort
    Thats not how it works. The actual sort mechanism(s) is built into the NET Framework. In the case of List.Sort() it will select one of 3 sorts as it sees fit. You can supply the comparison mechanism.

    Given a data set of {1,3,5,5,7,9,15} The elements with 1 and 3 will always sort lower than 5 or 1024, so missing elements are not an issue. How you want the duplicate values to compare, might be.

    There are several alternatives:

    Linq's OrderBy

    Dim sortedProds = myProducts.OrderBy(Function (j) j.Ordinal).ToList()
    

    Time: ~6 ms for 20k items
    If you want to do something about dupes, such as sort them by the Id:

    Dim ProdsL = myProducts.OrderBy(Function(j) j.Ordinal).
                       ThenBy(Function(k) k.Id).ToList()
    

    Time: ~8 ms for 20k items

    List.Sort using a comparer

    Make the class implement IComparable:

    Public Function CompareTo(other As ProductItem) As Integer _ 
                             Implements IComparable(Of ProductItem).CompareTo
        If Ordinal < other.Ordinal Then Return -1
        If Ordinal > other.Ordinal Then Return 1
    
        ' equal, return the lower ID or:
        'Return 0
    
        If Id < other.Id Then Return -1
        Return 1
    End Function
    

    Usage:

    myProducts.Sort(Function(x, y) x.CompareTo(y))
    

    Time: ~28 ms for 20k items

    You can also use a method which performs the comparison which means you dont have to implement IComparable unless it has value in other cases.

    Private Function ProductComparer(x As ProductItem, y As ProductItem) As Integer
        If x.Ordinal < y.Ordinal Then Return -1
        If x.Ordinal > y.Ordinal Then Return 1
    
        ' equal, return the lower ID or:
        'Return 0
    
        If x.Id < y.Id Then Return -1
        Return 1
    End Function
    

    Usage:

    myProducts.Sort(AddressOf ProductComparer)
    

    Time: 11 ms

    Bingo Sort

    SelectionSort (~3650 ms) and the Bingo variation (~3590 ms) are so far behind the others, they do not seem worth considering. Bingo is included just to satisfy the original question (even though there is already one in use elsewhere).

      Private Sub BingoSort(items As List(Of ProductItem))
        ' converted from https://en.wikipedia.org/wiki/Selection_sort#Variants
        ' for http://stackoverflow.com/q/42303395/1070452
    
        Dim max As Int32 = items.Count - 1
        Dim nextVal = items(max).Ordinal
        Dim value As Int32
        Dim tmp As ProductItem
    
        For i As Int32 = max - 1 To 0 Step -1
            If items(i).Ordinal > nextVal Then
                nextVal = items(i).Ordinal
            End If
        Next
        While (max > 0) And (items(max).Ordinal = nextVal)
            max -= 1
        End While
        While (max > 0)
            value = nextVal
            nextVal = items(max).Ordinal
            For i As Int32 = max - 1 To 0 Step -1
                If items(i).Ordinal = value Then
                    tmp = items(i)
                    items(i) = items(max)
                    items(max) = tmp
                    max -= 1
                ElseIf items(i).Ordinal > nextVal Then
                    nextVal = items(i).Ordinal
                End If
            Next
            While (max > 0) And (items(max).Ordinal = nextVal)
                max -= 1
            End While
        End While
    End Sub
    

    Usage:

    BingoSort(myProducts)
    

    Time: ~3590 ms for 20k items.

    Note that when there are no dupes (or the original list starts in Id order), the result is the same as linq's OrderBy

    Dim Valid = ProdsLinq.SequenceEqual(ProdsBingo)
    

    Given the poor performance there doesn't seem to be a reason to use it here. IComparable lets you decide how to tie-break duplicates and both Linq and List.Sort(IComparable) are gobs and gobs faster.

    Note that something similar exists for the DataTable:

    myDT.DefaultView.Sort = "Ordinal ASC"
    ' or
    myDT.DefaultView.Sort = "Ordinal ASC, Id ASC"