VB.NET code to implement Excel Rank()

| | No TrackBacks

Recently, I had to implement the Excel function Rank() for a project. I came up with this templatized LINQ code:

Public Class ReversedComparer(Of T As IComparable)
    Implements IComparer(Of T)
    Public Function Compare(ByVal x As T, ByVal y As T) As Integer _
    Implements System.Collections.Generic.IComparer(Of T).Compare
       Return -x.CompareTo(y)
    End Function
End Class

Private Shared Function BinarySearch(Of T As IComparable)(ByRef x() As T, _
                                     ByVal val As T, _
                                     ByVal comparer As IComparer(Of T)) As Integer
    Dim lo As Integer = 0, hi As Integer = x.Count - 1
    While lo <= hi
        Dim mid As Integer = (hi + lo) \ 2
        If comparer.Compare(val, x(mid)) <> 1 Then
            hi = mid - 1
        Else
            lo = mid + 1
        End If
    End While
    Return CInt(If(comparer.Compare(x(lo), val) = 0, lo, -1))
End Function

Public Shared Function Rank(ByRef X() As Double) As Integer()
    Dim sorted() As Double = (From xx In X Order By xx Descending Select xx).ToArray()
     Dim comparer As IComparer(Of Double) = New ReversedComparer(Of Double)
     Return X.Select(Function(val) 1 + BinarySearch(Of Double)(sorted, val, comparer)).ToArray()
End Function

And it's pretty good as far as it goes. It takes a copy of the data in reverse-sorted order and then does a binary search to find the 0-based position in the array for each element in the original array. And that's fine, but I wanted to implement something more like my python code:

import collections
def rank(arr):
	"""
	>>> a = list(range(10))
	>>> print rank(a)
	[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
	>>> b = list(range(10))
	>>> b.reverse()
	>>> print rank(b)
	[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
	>>> c = [5] * 10
	>>> print rank(c)
	[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
	>>> d = ([5] * 5) + ([1] * 5)
	>>> d
	[5, 5, 5, 5, 5, 1, 1, 1, 1, 1]
	>>> print rank(d)
	[1, 1, 1, 1, 1, 6, 6, 6, 6, 6]
	"""
	d = collections.defaultdict(list)
	for i, v in enumerate(arr):
		d[v].append(i)
	result = [0] * len(arr)
	i = 1
	for k in sorted(d, reverse = True):
		for j in d[k]:
			result[j] = i
		i += len(d[k])
	return result

if __name__ == '__main__':
	import doctest
	doctest.testmod()
 

So I worked through the LINQ issues using the LinqPad interpreter and came up with this:

Function Rank(Of T)(Byref x() as T) as Integer()
    Dim original_pos = x.Select(Function(xx, index) New With {.Val = xx, .Index = index}) _
             .ToLookup(Function(xxx) xxx.Val)
    Dim keys = original_pos.OrderByDescending(Function(yy) yy.Key)    
    Dim result(x.Count-1) as Integer
    Dim i as Integer = 1
    For Each item in keys
        For Each v in item
            result(v.Index) = i
        Next
        i = i + item.Count
    Next
    Return result
End Function

And I think that's pretty good code: templatized LINQ code that uses lambda functions and implements the algorithm as quickly as I know how.

No TrackBacks

TrackBack URL: http://www.iwebthereforeiam.com/cgi-bin/mt/mt-tb.cgi/1512

Leave a comment

Verification (needed to reduce spam):

Pages

OpenID accepted here Learn more about OpenID
Powered by Movable Type 4.32-en

About this Entry

This page contains a single entry by Hugh Brown published on October 13, 2009 1:13 PM.

The return of CoWhereIs was the previous entry in this blog.

MoreLinq is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.