How do I sort objects in VB.net with custom comparer elegantly? - vb.net

I want to sort a bunch of domainsdata object. I want to sort first based on countryCode then I want to sort based on revenue.
First I created a private comparer.
Private Function CompareDomainCountry(ByVal x As domainsData, ByVal y As domainsData) As Integer
If x.countryCode < y.countryCode Then
Return -1
ElseIf y.countryCode < x.countryCode Then
Return 1
ElseIf x.revenue < y.revenue Then
Return 1
ElseIf y.revenue < x.revenue Then
Return -1
Else
Return 0
End If
End Function
This has several problem.
The comparer returns 1,-1,0. I think there should be a normal enum for that.
Also I think my comparer should simply call standard vb.net comparer.
And after that, how do I sort list (of domainsdata)?
comparer?

You can use the CompareTo method to compare the values. If the first comparison is zero, then do the other comparison:
Private Function CompareDomainCountry(ByVal x As domainsData, ByVal y As domainsData) As Integer
Dim result As Integer = x.countryCode.CompareTo(y.countryCode)
If result = 0 Then
result = x.revenue.CompareTo(y.revenue)
End If
Return result
End Function
To sort the list using the comparison you use it in the Sort method call:
myList.Sort(AddressOf CompareDomainCountry)

First, note that your Revenue compare code is inconsistent: a smaller X should return -1. You also don't absolutely need that method. This gives the same result:
Dim sorted = DomainList.OrderBy(Function(x) x.CountryCode).
ThenBy(Function(y) y.Revenue).
ToList()
If you want to rely on a standard NET method, your method can be a class member:
Public Class DomainComparer
Implements IComparer(Of Domain)
Public Function Compare(x As Domain, y As Domain) As Integer _
Implements IComparer(Of Domain).Compare
' all your code
End Function
End Class
Then to use it:
Dim dSorter = New DomainComparer
DomainList.Sort(dSorter)
' or simply:
DomainList.Sort(New DomainComparer)
Mr Guffa's AddressOf method is simpler and more concise; I like the class method when there are other variables/properties such as a SortOrder.
The results are the same either way (when the revenue result is changed) unless a sort member is mixed alpha-numeric string (which seemed not to be the case based on the names and comparison).
If you were hoping to use your method with OrderBy(), I dont think you can - the signature doesn't match Func(Of T)(TKey). The return however is uniform with most all Compare() methods to indicate the larger value (DateTime indicates the lesser/earlier date; there may be others).

Related

In a generic VB.NET structure, how do I access its explicitly provided constructor?

First of all, what I want to achieve:
I want to extend a value datatype by providing additional properties, especially to validate ranges provided at declaration time. I want the new datatype to be a value type as well.
Compare with Ada:
subtype Day_Number is Integer range 1 .. 31;
Ideal, but obviously not implementable, would be:
Dim DayNumber As Int64 Range 1 To 31
However, I would be happy with:
Dim DayNumber As RangeInt64(1, 31)
It is of no concern, if initialization takes its time. Once ranges are provided, they are considered to be immutable. The datatype from then on is only used to set/get values like with ordinary value types, only that they are subject of being validated against the initially provided range.
My attempt:
Since I cannot inherit from structures in order to expand on them, I tried to incorporate a structure into a structure as a member.
In a module, I have this structure:
Friend Structure SRangeValueType(Of T)
Private lMinimum As T
Private lMaximum As T
Friend Property Minimum As T
Get
Return lMinimum
End Get
Set(tValue As T)
lMinimum = tValue
End Set
End Property
Friend Property Maximum As T
Get
Return lMaximum
End Get
Set(tValue As T)
lMaximum = tValue
End Set
End Property
Friend Sub New(Minimum As T, Maximum As T)
lMinimum = Minimum
lMaximum = Maximum
End Sub
End Structure
I attempt to use this generic structure as a member of another structure (of concrete type Int64):
Public Structure RangeInt64
Private Range As SRangeValueType(Of Int64)
End Structure
However, this is not using the constructor with the two arguments.
Say I want to initialize Range (the only member of the structure RangeInt64) with the values 100 and 200 for Minimum and Maximum, resp.
I am not allowed to use something like:
Private Range As SRangeValueType(Of Int64)(100,200)
What is the correct syntax to provide my values to the generic constructor?
Normally, if you add a constructor to a structure, you can call it using the New keyword:
Dim x As SRangeValueType(Of Int64) ' Calls the default, infered, parameter-less constructor
Dim y As New SRangeValueType(Of Int64)(100, 200) ' Calls the explicitly defined constructor
However, that's not really the problem. The problem is that you are trying to set the default value of a non-shared field in a structure. That is something which is never allowed. All non-shared fields in structures must default to their default value (i.e. Nothing). For instance:
Public Structure RangeInt64
Private x As Integer = 5 ' Error: Initializers on structure members are valid only for 'Shared' members and constants
Private y As New StringBuilder() ' Error: Non-shared members in a structure cannot be declared 'New'
End Structure
And, as you may already know, you cannot override the default, inferred, parameter-less constructor on a structure either:
Public Structure RangeInt64
Public Sub New() ' Error: Structures cannot declare a non-shared 'Sub New' with no parameters
x = 5
y = New StringBuilder()
End Sub
Private x As Integer
Private y As StringBuilder
End Structure
As such, you are stuck. By design, when the default constructor is used, all fields in the structure must always default to Nothing. However, if you really, really need it to be a structure, and you can't just convert it to a class, and you really need to change it's default value, you could theoretically fake it into working by using a property to wrap the field:
Public Structure RangeInt64
Private _Range As SRangeValueType(Of Int64)
Private _RangeInitialized As Boolean
Private Property Range As SRangeValueType(Of Int64)
Get
If Not _RangeInitialized Then
_Range = New SRangeValueType(Of Int64)(100, 200)
_RangeInitialized = True
End If
Return _Range
End Get
Set(value As SRangeValueType(Of Int64))
_Range = value
End Set
End Property
End Structure
It should go without saying, though, that it's pretty gross and should be avoided if possible.
Update
Now that you've provided more details about what you are trying to accomplish, I think I may have a better solution for you. You're right that structures do not support inheritance, but what they do support is interfaces. So, if all you need is a bunch of range types, one per value-type, all having the same minimum and maximum properties, but all returning different predetermined values, then you could do something like this:
Private Interface IRangeValueType(Of T)
ReadOnly Property Minimum As T
ReadOnly Property Maximum As T
End Interface
Private Structure RangeInt64
Implements IRangeValueType(Of Int64)
Public ReadOnly Property Minimum As Long Implements IRangeValueType(Of Int64).Minimum
Get
Return 100
End Get
End Property
Public ReadOnly Property Maximum As Long Implements IRangeValueType(Of Int64).Maximum
Get
Return 200
End Get
End Property
End Structure

Sorting a List of Integers and Strings by integer descending order in VB

I have to make this program that sorts the high scores of a game and then displays them biggest to smallest with the username USING LISTS. So far i have written:
Public highscore As New List(Of HighScores)
highscore.Add(New HighScores("Jeremias", 6))
highscore.Add(New HighScores("Tom", 1))
highscore.Add(New HighScores("sdf", 5))
highscore.Add(New HighScores("asfd", 1))
highscore.Sort()
highscore.Reverse()
Console.WriteLine("------High Scores-----")
For Each scores In highscore
Console.WriteLine(scores)
Next
Console.WriteLine("----------------------")
And the HighScores Class:
Public Class HighScores
Public name As String
Public score As Integer
Public Sub New(ByVal name As String, ByVal score As Integer)
Me.name = name
Me.score = score
End Sub
Public Overrides Function ToString() As String
Return String.Format("{0}, {1}", Me.name, Me.score)
End Function
End Class
Usually i would just use .Sort() and .Reverse() to sort the list, but in this case i don't think i can do this. Any ideas how i can rewrite this/just sort the list easily?
You can specify how to sort a List(Of T) in various ways. The simplest would be like so:
highscore.Sort(Function(x, y) y.score.CompareTo(x.score))
That uses the overload of Sort that takes a Comparison(Of T) delegate and uses a Lambda expression for that delegate. Note that the Lambda parameters are x and y and the body calls CompareTo on the score of y. That is critical because that's what makes the sort happen in descending order and negates the need to call Reverse.
Note that you could use a named method instead of a Lambda. Such a method would look like this:
Private Function CompareHighScoresByScoreDescending(x As HighScores, y As HighScores) As Integer
Return y.score.CompareTo(x.score)
End Function
The code to sort would then look like this:
highscore.Sort(AddressOf CompareHighScoresByScoreDescending)
When comparing objects for sorting purposes, the convention is to use -1, 0 and 1 to represent relative positions. That's what CompareTo does and thus that's what our comparison method does here. If the object you call CompareTo on is conceptually less the object you pass in then the result is -1. 1 means the first object is greater than the second and 0 means they are equal. That method could be rewritten like so:
Private Function CompareHighScoresByScoreDescending(x As HighScores, y As HighScores) As Integer
If y.score < x.score Then
Return -1
ElseIf y.score > x.score Then
Return 1
Else
Return 0
End If
End Function
It's obviously more succinct to use the existing IComparable implementation of the Integer type though, i.e. that CompareTo method.
By the way, your code could use some improvements in other areas. Firstly, HighScores is not an appropriate name for that class. It represent a single thing so the name should not be plural and it doesn't actually represent a high score in and of itself. A more appropriate name would be PlayerScore as that more accurately describes what it represents.
Secondly, your List variable actually does represent more than one object, i.e. a list that contains multiple items, so it's name should be plural. It also does actually represent high scores so it should be named highScores.
Finally, it is almost universally bad practice to expose member variables publicly. You should absolutely be using properties in that class:
As a bonus, if you're using VS 2015 or later then you can also replace String.Format with string interpolation.
Public Class PlayerScore
Public Property Name As String
Public Property Score As Integer
Public Sub New(name As String, score As Integer)
Me.Name = name
Me.Score = score
End Sub
Public Overrides Function ToString() As String
Return $"{Name}, {Score}"
End Function
End Class

vb.net function branching based on optional parameters performance

So I was coding a string search function and ended up with 4 since they needed to go forwards or backwards or be inclusive or exclusive. Then I needed even more functionality like ignoring certain specific things and blah blah.. I figured it would be easier to make a slightly bigger function with optional boolean parameters than to maintain the 8+ functions that would otherwise be required.
Since this is the main workhorse function though, performance is important so I devised a simple test to get a sense of how much I would lose from doing this. The code is as follows:
main window:
Private Sub testbutton_Click(sender As Object, e As RoutedEventArgs) Handles testbutton.Click
Dim rand As New Random
Dim ret As Integer
Dim count As Integer = 100000000
Dim t As Integer = Environment.TickCount
For i = 0 To count
ret = superfunction(rand.Next, False)
Next
t = Environment.TickCount - t
Dim t2 As Integer = Environment.TickCount
For i = 0 To count
ret = simplefunctionNeg(rand.Next)
Next
t2 = Environment.TickCount - t2
MsgBox(t & " " & t2)
End Sub
The functions:
Public Module testoptionality
Public Function superfunction(a As Integer, Optional b As Boolean = False) As Integer
If b Then
Return a
Else
Return -a
End If
End Function
Public Function simpleFunctionPos(a As Integer)
Return a
End Function
Public Function simplefunctionNeg(a As Integer)
Return -a
End Function
End Module
So pretty much as simple as it gets. The weird part is that the superfunction is consistently twice faster than either of the simple functions (my test results are "1076 2122"). This makes no sense.. I tried looking for what i might have done wrong but I cant see it. Can anybody explain this?
You didn't set a return type for simple function. So they return Object type.
So when you using simpleFunctionNeg function application convert Integer to Object type when returning value, and then back from Object to Integer when assigning returning value to your variable
After setting return value to Integer simpleFunctionNeg was little bid faster then superfunction

How to sort an object in a list on a non-unique value?

I'm trying to categorize articles by stored keywords. I have a list of keywords for a category, and I want an article to get assigned a category that has the most keyword count.
For Each keyword As String In category.Keywords
category.tempCount += Regex.Matches(article.Item("title").InnerXml, Regex.Escape(keyword)).Count
category.tempCount += Regex.Matches(article.Item("description").InnerXml, Regex.Escape(keyword)).Count
Next
And this is done for each category, ran for each article. I'm trying to sort the list in order to tell which category is the best one for this article. However it is possible more than one category is the best, and that none of the categories fit. So running this did not help me:
Categories.Sort(
Function(article1 As ArticleCategory, article2 As ArticleCategory)
Return article1.tempCount.CompareTo(article2.tempCount)
End Function)
Maybe I'm doing this all wrong, but so far I think I'm on the right path. (I also have a default compare in the Category class, it just wasn't working either.)
I get an exception on the sorting most likely caused because they are not unique.
The exception I get is an InvalidOperationException: Failed to compare two elements in the array. That's with using the comparer I built in the ArticleClass
Imports System.Xml
Class ArticleCategory
Implements IComparer(Of ArticleCategory)
Public ReadOnly key As Int32
Public ReadOnly Name As String
Public ReadOnly Keywords As List(Of String)
Public tempCount As Integer = 0
Public Sub New(ByVal category As XmlElement)
key = System.Web.HttpUtility.UrlDecode(category.Item("ckey").InnerXml)
Name = System.Web.HttpUtility.UrlDecode(category.Item("name").InnerXml)
Dim tKeywords As Array = System.Web.HttpUtility.UrlDecode(category.Item("keywords").InnerXml).Split(",")
Dim nKeywords As New List(Of String)
For Each keyword As String In tKeywords
If Not keyword.Trim = "" Then
nKeywords.Add(keyword.Trim)
End If
Next
Keywords = nKeywords
End Sub
'This should be removed if your using my solution.
Public Function Compare(ByVal x As ArticleCategory, ByVal y As ArticleCategory) As Integer Implements System.Collections.Generic.IComparer(Of ArticleCategory).Compare
Return String.Compare(x.tempCount, y.tempCount)
End Function
End Class
You need to implement IComparable instead of IComparer.
IComparer would be implemented by the class performing the sorting (such as a List class) while IComparable would be implemented by the class being sorted.
For example:
Public Function CompareTo(other As ArticleCategory) As Integer Implements System.IComparable(Of ArticleCategory).CompareTo
Return Me.tempCount.CompareTo(other.tempCount)
End Function
The best solution I found was using the Microsoft LINQ (a query language for objects) it works very well and quickly produces the right result.
Dim bestCat As ArticleCategory
bestCat = (From cat In Categories
Order By cat.tempCount Descending, cat.Name
Select cat).First
Completing my solution:
For Each category As ArticleCategory In Categories
category.tempCount = 0
For Each keyword As String In category.Keywords
category.tempCount += Regex.Matches(System.Web.HttpUtility.UrlDecode(article.Item("title").InnerXml), Regex.Escape(keyword)).Count
category.tempCount += Regex.Matches(System.Web.HttpUtility.UrlDecode(article.Item("description").InnerXml), Regex.Escape(keyword)).Count
Next
Next
Dim bestCat As ArticleCategory
Try
bestCat = (From cat In Categories
Order By cat.tempCount Descending, cat.Name
Select cat).First
Catch ex As Exception
ReportStatus(ex.Message)
End Try
So this is my preferred method to do a sort or a query on a list object or an array. It produces the best result, in the fastest time without having to add the IComparer implementations to your class.
Check it out at Microsoft.com

IComparable - Call different sorts?

I have a DTO that I am using to process transactions. To ensure that it is processing in the correct order, I am using iComparable and sorting the List(of T) of the DTO. That works great. However I just got another requirement that the customer wants the output in a different order... is there a way to allow me to have two different sorts for the same object, or do I need to copy the current class, save the output as a new List of that type and sort using the new way for that object? Seems like an awful way to do it, but cannot find anything that allows me to do it.
Here is an example I ripped from a recent project. Works like a charm. Just have to remember to call SORT with the appropriate function. This is outside the scope fo the IComparable interface, so you might want to drop that from your class declaration.
Public Class Purchaser
....
Public Shared Function CompareByGroup( _
ByVal x As Purchaser, ByVal y As Purchaser) As Integer
If x Is Nothing Then
If y Is Nothing Then
' If x is Nothing and y is Nothing, they're equal.
Return 0
Else
' If x is Nothing and y is not Nothing, y is greater.
Return -1
End If
Else
If y Is Nothing Then
' If x is not Nothing and y is Nothing, x is greater.
Return 1
Else
' ...and y is not Nothing, compare by GroupName.
Return x.GroupName.CompareTo(y.GroupName)
End If
End If
End Function
Public Shared Function CompareByName( _
ByVal x As Purchaser, ByVal y As Purchaser) As Integer
... 'you get the idea
End Function
And call them like this...
tempList.Sort(AddressOf Classes.Purchaser.CompareByGroup)
or
tempList.Sort(AddressOf Classes.Purchaser.CompareByName)
Or you can use linq if you are on .Net 3.5 or above.
dim orderedlistofdtos = (from e in listofdtos order by e.whatever select e).Tolist