Sort List(of Object) by object properties - vb.net

I'm trying to achieve something where the answer is already given for. But it's in c# and I don't have any knowledge what-so-ever over c# so I'm looking for a vb.net alternative.
I made a class called BomItem which has several properties like quantity, description etc.
I add these BomItems into a List(of BomItem) but now I would like to sort them according to a property. How can you sort the items based on the ItemNumber property?
Here is the link to the c# solution I found.
My class code
Public Class BomItem
Public Property ItemNumber As String
Public Property Description As String
Public Property Quantity As Double
Public Property Material As String
Public Property Certificate As String
End Class
How I add the BomRow objects
_NewBomList.Add(New BomItem() With {
.ItemNumber = oRow.ItemNumber,
.Description = oPropSet.Item("Description").Value,
.Quantity = oRow.TotalQuantity,
.Material = oPropSet.Item("Material").Value,
.Certificate = CustomPropertySet.Item("Cert.").Value})
Comparer
Public Class NaturalSort
Implements IComparer
Public Function Compare(ByVal x As Object,
ByVal y As Object) As Integer Implements IComparer.Compare
' [1] Validate the arguments.
Dim s1 As String = x
If s1 = Nothing Then
Return 0
End If
Dim s2 As String = y
If s2 = Nothing Then
Return 0
End If
Dim len1 As Integer = s1.Length
Dim len2 As Integer = s2.Length
Dim marker1 As Integer = 0
Dim marker2 As Integer = 0
' [2] Loop over both Strings.
While marker1 < len1 And marker2 < len2
' [3] Get Chars.
Dim ch1 As Char = s1(marker1)
Dim ch2 As Char = s2(marker2)
Dim space1(len1) As Char
Dim loc1 As Integer = 0
Dim space2(len2) As Char
Dim loc2 As Integer = 0
' [4] Collect digits for String one.
Do
space1(loc1) = ch1
loc1 += 1
marker1 += 1
If marker1 < len1 Then
ch1 = s1(marker1)
Else
Exit Do
End If
Loop While Char.IsDigit(ch1) = Char.IsDigit(space1(0))
' [5] Collect digits for String two.
Do
space2(loc2) = ch2
loc2 += 1
marker2 += 1
If marker2 < len2 Then
ch2 = s2(marker2)
Else
Exit Do
End If
Loop While Char.IsDigit(ch2) = Char.IsDigit(space2(0))
' [6] Convert to Strings.
Dim str1 = New String(space1)
Dim str2 = New String(space2)
' [7] Parse Strings into Integers.
Dim result As Integer
If Char.IsDigit(space1(0)) And Char.IsDigit(space2(0)) Then
Dim thisNumericChunk = Integer.Parse(str1)
Dim thatNumericChunk = Integer.Parse(str2)
result = thisNumericChunk.CompareTo(thatNumericChunk)
Else
result = str1.CompareTo(str2)
End If
' [8] Return result if not equal.
If Not result = 0 Then
Return result
End If
End While
' [9] Compare lengths.
Return len1 - len2
End Function
End Class

Use LINQ OrderBy:
_NewBomList.OrderBy(Function(bi) bi.ItemNumber)
and for descending:
_NewBomList.OrderByDescending(Function(bi) bi.ItemNumber)
If you want a numeric order in your string you have to convert it to an integer first:
_NewBomList.OrderBy(Function(bi) Integer.Parse(bi.ItemNumber))
Edit:
To provide a custom IComparer for the OrderBy extension you have to create a class which implements IComparer(Of String) where String are your ItemNumbers to compare:
Class ItemNumberComparer
Implements IComparer(Of String)
Public Function Compare(String x, String y)
Dim ix As String() = x.Split("."C)
Dim iy As String() = y.Split("."C)
Dim maxLen As Integer = Math.Max(ix.Length, iy.Length)
For i As Integer = 0 To maxLen - 2
If ix.Length >= i AndAlso iy.Length >= i Then
If Integer.Parse(ix(i)) < Integer.Parse(iy(i)) Then
Return -1 'If x.i is LT y.i it must be smaller at all
ElseIf Integer.Parse(ix(i)) > Integer.Parse(iy(i)) Then
Return 1 'If x.i is GT y.i it must be bigger all
End If
End If
Next
'This code is only executed if x and y differ at last number or have different ´number of dots
If ix.Length = iy.Length Then
Return Integer.Parse(ix(ix.Length - 1)).CompareTo(Integer.Parse(iy(iy.Length - 1))) 'Only last number differs
Else
Return ix.Length.CompareTo(iy.Length) 'The number with more dots is smaller
End If
End Function
End Class
Call syntax:
Dim comparer = new ItemNumberComparer()
_NewBomList.OrderByDescending(Function(bi) bi.ItemNumber, comparer)

This C# code from that other thread:
List<Order> SortedList = objListOrder.OrderBy(o=>o.OrderDate).ToList();
equates to this VB code:
List(Of Order) SortedList = objListOrder.OrderBy(Function(o) o.OrderDate).ToList()
As you can see, very little changes. You just have to know the syntax for generics and lambda expressions.
You should be aware, though, that this code does NOT sort your list. It sorts the items in the list and then adds them to a new list in that order. This is perfectly fine for many applications but if you're using that same list elsewhere then you won't see the new order there. While there are a few variations, one way to actually sort the list in place is like this:
objListOrder.Sort(Function(o1, o2) o1.OrderDate.CompareTo(o2.OrderDate))

Another solution would be to implement the IComparable (see MSDN ref) interface. This interface is designed to sort your objects on a custom way :
Public Class BomItem
Implements IComparable
Public Property ItemNumber As String
Public Property Description As String
Public Property Quantity As Double
Public Property Material As String
Public Property Certificate As String
Public Function CompareTo(obj As Object) As Integer
Dim bom = CType(obj, BomItem)
If Not bom Is Nothing then
Return Me.ItemNumber.CompareTo(bom.ItemNumber)
Else
Throw New ArgumentException("Object is not a BomItem")
End If
End Class
and you can sort the list this way :
Dim myList As New List(Of BomItem)
'Code to add your BomItems
myList.Sort()
This will actually sort your list, it does not create a new list.

Related

VB.net - Sorting only one column in datagridview

I'm populating a DataGridView from an Excel file, and trying to sort only ONE column of my choice, other columns should remain as-is. How can it be achieved? Should the component be changed to something else, in case it is not possible in DataGridView?
I Created a List of my custom class, and this class will handle the sorting based on my preference (Randomization in this case)
Public Class Mylist
Implements IComparable(Of Mylist)
Private p_name As String
Private r_id As Integer
Public Property Pname() As String 'This will hold the contents of DGV that I want sorted
Get
Return p_name
End Get
Set(value As String)
p_name = value
End Set
End Property
Public Property Rid() As Integer 'This will be the basis of sort
Get
Return r_id
End Get
Set(value As Integer)
r_id = value
End Set
End Property
Private Function IComparable_CompareTo(other As Mylist) As Integer Implements IComparable(Of Mylist).CompareTo
If other Is Nothing Then
Return 1
Else
Return Me.Rid.CompareTo(other.Rid)
End If
End Function
End Class
Then a Button which will sort the contents:
Dim selcol = xlView.CurrentCell.ColumnIndex
Dim rand = New Random()
Dim x As Integer = 0
Dim plist As New List(Of Mylist)
Do While x < xlView.Rows.Count
plist.Add(New Mylist() With {
.Pname = xlView.Rows(x).Cells(selcol).Value,
.Rid = rand.Next()
})
x += 1
Loop
plist.Sort()
x = 0
Do While x < xlView.Rows.Count
xlView.Rows(x).Cells(selcol).Value = plist.ElementAt(x).Pname
x += 1
Loop
xlView.Update()
plist.Clear()
I'm open to any changes to code, as long as it achieves the same result.
Here is the simpler version. Pass the column index will do like Call SortSingleColum(0)
Private Sub SortSingleColumn(x As Integer)
Dim DataCollection As New List(Of String)
For i = 0 To dgvImport.RowCount - 2
DataCollection.Add(dgvImport.Item(x, i).Value)
Next
Dim t As Integer = 0
For Each item As String In DataCollection.OrderBy(Function(z) z.ToString)
dgvImport.Item(x, t).Value = item
t = t + 1
Next
End Sub

ContainsValue with Dictionary(Of String, Items)

How to know if a dictionary with multiple values contains specific value?
'Create dictionary
Dim testDictionary As New Dictionary(Of String, Items)
'Code to fill dictionary
'.......................
'.......................
'.......................
'Test if a specific value is contained in dictionary
Dim testValue as String = "TEST"
testDictionary.ContainsValue(testValue) 'This doesn't work
Public Class Items
Public Property Property1 As String
Public Property Property2 As String
Public Sub New()
End Sub
End Class
If you can define how to determine whether the dictionary contains that string, pass that logic into Enumerable.Any
Dim testValue As String = "TEST"
Dim contains = testDictionary.Any(Function(kvp) kvp.Value.Property1 = testValue OrElse kvp.Value.Property2 = testValue)
If contains Then
Dim containsEntries = testDictionary.Where(Function(kvp) kvp.Value.Property1 = testValue OrElse kvp.Value.Property2 = testValue)
End If
Since you reuse it for Any and Where, you can declare the predicate once
Dim predicate =
Function(kvp As KeyValuePair(Of String, Items))
Return kvp.Value.Property1 = testValue OrElse kvp.Value.Property2 = testValue
End Function
Dim contains = testDictionary.Any(predicate)
If contains Then
Dim containsEntries = testDictionary.Where(predicate)
End If
This is hard-coded to just these properties Property1 and Property2.
(you really don't need the Any if you want the entities; I just figured the Any answered your question "How to know if..." with a boolean)
If you want to check all public instance string properties, you can use reflection
Dim predicate =
Function(kvp As KeyValuePair(Of String, Items))
Return GetType(Items).
GetProperties(Reflection.BindingFlags.Public Or Reflection.BindingFlags.Instance).
Where(Function(pi) pi.PropertyType Is GetType(String)).
Aggregate(False, Function(pi1, pi2) pi1 Or (pi2.GetValue(kvp.Value) = testValue))
End Function
Dim containsWith = testDictionary.Any(predicate)
If containsWith Then
Dim containsEntries = testDictionary.Where(predicate)
End If

VB.NET - Randomize() with a function call in a string.replace method

I have a chat system and i want to put a "random string generator".
In my chat i have to write "%random%" and it is replaces with a random string.
I have a problem though, if i type "%random%%random%%random%" for example, it will generate the same string 3 times.
• Here is my function:
Public Function getRandomString(ByVal len As Integer) As String
Randomize()
Dim stringMap as string = "abcdefghijklmnopqrstuwvxyz0123456789"
Dim rndString As String = ""
Dim rnd As New Random()
For i As Integer = 0 To len - 1
Randomize()
rndString &= stringMap.Substring(rnd.Next(0, stringMap.Length), 1)
Next
Return rndString
End Function
• And here is my function call:
Dim msg As String = "Random string: %random%%random%%random%"
msg = msg.Replace("%random%", getRandomString(8))
MsgBox(msg)
The output for example: Random string: 5z15if725z15if725z15if72
I guess this is because it keeps the 1st return value in memory and pastes it, how can i fix that ?
Do i have to make a string.replace function myself ? Thanks
Oh no! You shouldn't call Randomize() here at all! Random is used in combination with the Rnd() function of VB. Creating a new Random object is enough here.
The reason you are getting the same results every time is because you are creating a new Random every time. You should reuse the same object to get different results.
'Create the object once
Private Shared rnd As New Random()
Public Function getRandomString(ByVal len As Integer) As String
Dim stringMap as string = "abcdefghijklmnopqrstuwvxyz0123456789"
Dim rndString As String = ""
For i As Integer = 0 To len - 1
rndString &= stringMap.Substring(rnd.Next(0, stringMap.Length), 1)
Next
Return rndString
End Function
EDIT: I realize that in addition to the above changes, you need to call the getRandomString function for every "%random%". String.Replace only calls the function once and pastes the result everywhere. With Regex, you could do something like this:
msg = new Regex("%random%").Replace(input, Function (match) getRandomString(8))
An easy way to do it is to find the first occurrence of "%random%", replace that, then repeat as necessary.
Written as a console application:
Option Infer On
Module Module1
Dim rand As New Random
Public Function getRandomString(ByVal len As Integer) As String
Dim stringMap As String = "abcdefghijklmnopqrstuwvxyz0123456789"
Dim rndString As String = ""
For i As Integer = 0 To len - 1
rndString &= stringMap.Substring(rand.Next(0, stringMap.Length), 1)
Next
Return rndString
End Function
Function ReplaceRandoms(s As String) As String
Dim stringToReplace = "%random%"
Dim r = s.IndexOf(stringToReplace)
While r >= 0
s = s.Remove(r, stringToReplace.Length).Insert(r, getRandomString(stringToReplace.Length))
r = s.IndexOf(stringToReplace)
End While
Return s
End Function
Sub Main()
Dim msg As String = "Random string: %random%%random%%random%"
msg = ReplaceRandoms(msg)
Console.WriteLine(msg)
Console.ReadLine()
End Sub
End Module

More effective loop

I'm making a player-match-up program in Visual Basic. The program is supposed to pick random registered players and and pair them. I'm currently working on the odd-number-of-players-part.
The solution I have is working but is perhaps not that effective. Is there a better way for me to write this code?
The code is supposed to pick the random players and make sure they are not picked again. As you see, for the code to work i must make it loop thousands of times. If I don't some of the players won't show up in the listbox. Is there a better solution???
In case it's confusing "spiller" is norwegian for "player"
For i As Integer = 0 To 100000
Dim spiller1 As Integer
Dim spiller2 As Integer
Do
spiller1 = CInt(Math.Floor(Rnd() * spillerListe.Count))
spiller2 = CInt(Math.Floor(Rnd() * spillerListe.Count))
Loop Until CBool(spiller1 <> spiller2)
If brukteSpillere(spiller1) = False And brukteSpillere(spiller2) = False Then
brukteSpillere(spiller1) = True
brukteSpillere(spiller2) = True
lstSpillere.Items.Add(spillerListe(spiller1).ToString + " VS. " + spillerListe(spiller2).ToString())
End If
Next i
This is a mess... Have a List(Of Integer) with all the available index.
Loop while availableIndex.Count > 1
Pick a random index from availableIndex and remove it from that list
Pick a random index from availableIndex and remove it from that list
Add these two index to the list of pairs
End Loop
that way you don't need to check if the random values are the same or if they were already picked.
Now, if you don't want to create a list. Then threat the random number not as an index, but as the number of items to check.
Delta = RandomNumber
x = 0
For i As Integer = 0 To itemList.Count-1
If Not itemList(i).IsChoosen Then
x += 1
If x = Delta Then
' i is now your item to pick
itemList(i).IsChoosen = True
Exit For
End If
End If
Next
There are two efficient ways in approaching this problem:
Sort your player list by random number, then match up 1 with 2, 3 with 4 and so on.
Dim r As New Random
Dim randomListe = spillerListe.OrderBy(Function() r.Next).ToList
Generate two random numbers from your range, match up those players into a separate List, remove players from the original list. General two more random numbers from a smaller range (original minus 2), match up, etc.
EDIT: Having looked at MSDN, List has O(n) performance for RemoveAt, so it's not quite efficient, better be using a dictionary, which is O(1) at removing items, so instead of spillerListe have some spillerDicte, where you would add entries in a form (key = index, value = item).
Instead of working with integers, what if you keep your players name in a list and, after picking a player you remove it from the list. Probably this will not be the best performant solution, but it is clear what are you trying to do
Dim lstSpillere = new List(Of String)() ' Just for the example below
Dim spillerListe = new List(Of String)() from {"Marc", "John", "Steve", "George", "David", "Jeremy", "Andrew" }
Dim rnd = new Random()
While spillerListe.Count > 1
Dim firstPlayer = spillerListe(rnd.Next(0, spillerListe.Count))
spillerListe.Remove(firstPlayer)
Dim secondPlayer = spillerListe(rnd.Next(0, spillerListe.Count))
spillerListe.Remove(secondPlayer)
lstSpillere.Add(firstPlayer + " VS. " + secondPlayer)
' for debug purpose....
Console.WriteLine(firstPlayer & " VS. " & secondPlayer)
End While
if spillerListe.Count > 0 Then
Console.WriteLine("Excluded from play list is:" & spillerListe(0))
End if
The important key here is the generation of Random instance that should be outside the loop to avoid to generate the same number in the short time period required by the loop to execute.
Try this:
Module Module1
Dim rnd As New Random
Sub Main()
Dim RegisteredPlayers As New List(Of Player)
' Fill List (example 100 players)
For index As Integer = 1 To 100
RegisteredPlayers.Add(New Player(String.Format("Player{0}", index)))
Next
'Sort Players using a random number
Dim SortedPlayersArray = RandomSortItems(RegisteredPlayers.ToArray())
'Pair players by selecting 2 consequative ones from randomly sorted array
Dim Matches As New List(Of Match)
For index As Integer = 1 To SortedPlayersArray.Length Step 2
Dim m As Match = New Match(SortedPlayersArray(index - 1), SortedPlayersArray(index))
Matches.Add(m)
Debug.WriteLine(m.ToString())
Next
' Match Player48 vs. Player79
' Match Player3 vs. Player53
' Match Player18 vs. Player43
' Match Player85 vs. Player1
' Match Player47 vs. Player56
' Match Player23 vs. Player66
' etc..
End Sub
Public Function RandomSortItems(Of T)(ByVal items As T()) As T()
Dim sorted As T() = New T(items.Length-1) {}
Array.Copy(items, sorted, sorted.Length)
Dim keys As Double() = New Double(items.Length-1) {}
For i As Integer = 1 To items.Length
keys(i - 1) = rnd.NextDouble()
Next
Array.Sort(keys, sorted)
Return sorted
End Function
End Module1
Public Class Player
Dim m_name As String
Public Sub New(ByVal player_name As String)
m_name = player_name
End Sub
Public ReadOnly Property Name() As String
Get
Return m_name
End Get
End Property
Public Overrides Function ToString() As String
Return m_name
End Function
End Class
Public Class Match
Dim m_player_1 As Player, m_player_2 As Player
Public Sub New(ByVal player_1 As Player, ByVal player_2 As Player)
m_player_1 = player_1
m_player_2 = player_2
End Sub
Public ReadOnly Property Player1() As Player
Get
Return m_player_1
End Get
End Property
Public ReadOnly Property Player2() As Player
Get
Return m_player_2
End Get
End Property
Public Overrides Function ToString() As String
Return String.Format("Match {0} vs. {1}", Player1, Player2)
End Function
End Class
Edit 1:
An alternate random sorter (which should be faster) is
Public Function RandomSortItems(Of T)(ByVal items As T()) As T()
Dim slist As New SortedList(Of Double, T)
For i As Integer = 1 to items.Length
slist.Add(rnd.NextDouble(), items(i-1) )
Next i
return slist.Values.ToArray()
End Function

How can I search an array in VB.NET?

I want to be able to effectively search an array for the contents of a string.
Example:
dim arr() as string={"ravi","Kumar","Ravi","Ramesh"}
I pass the value is "ra" and I want it to return the index of 2 and 3.
How can I do this in VB.NET?
It's not exactly clear how you want to search the array. Here are some alternatives:
Find all items containing the exact string "Ra" (returns items 2 and 3):
Dim result As String() = Array.FindAll(arr, Function(s) s.Contains("Ra"))
Find all items starting with the exact string "Ra" (returns items 2 and 3):
Dim result As String() = Array.FindAll(arr, Function(s) s.StartsWith("Ra"))
Find all items containing any case version of "ra" (returns items 0, 2 and 3):
Dim result As String() = Array.FindAll(arr, Function(s) s.ToLower().Contains("ra"))
Find all items starting with any case version of "ra" (retuns items 0, 2 and 3):
Dim result As String() = Array.FindAll(arr, Function(s) s.ToLower().StartsWith("ra"))
-
If you are not using VB 9+ then you don't have anonymous functions, so you have to create a named function.
Example:
Function ContainsRa(s As String) As Boolean
Return s.Contains("Ra")
End Function
Usage:
Dim result As String() = Array.FindAll(arr, ContainsRa)
Having a function that only can compare to a specific string isn't always very useful, so to be able to specify a string to compare to you would have to put it in a class to have somewhere to store the string:
Public Class ArrayComparer
Private _compareTo As String
Public Sub New(compareTo As String)
_compareTo = compareTo
End Sub
Function Contains(s As String) As Boolean
Return s.Contains(_compareTo)
End Function
Function StartsWith(s As String) As Boolean
Return s.StartsWith(_compareTo)
End Function
End Class
Usage:
Dim result As String() = Array.FindAll(arr, New ArrayComparer("Ra").Contains)
Dim inputString As String = "ra"
Enumerable.Range(0, arr.Length).Where(Function(x) arr(x).ToLower().Contains(inputString.ToLower()))
If you want an efficient search that is often repeated, first sort the array (Array.Sort) and then use Array.BinarySearch.
In case you were looking for an older version of .NET then use:
Module Module1
Sub Main()
Dim arr() As String = {"ravi", "Kumar", "Ravi", "Ramesh"}
Dim result As New List(Of Integer)
For i As Integer = 0 To arr.Length
If arr(i).Contains("ra") Then result.Add(i)
Next
End Sub
End Module
check this..
string[] strArray = { "ABC", "BCD", "CDE", "DEF", "EFG", "FGH", "GHI" };
Array.IndexOf(strArray, "C"); // not found, returns -1
Array.IndexOf(strArray, "CDE"); // found, returns index
compare properties in the array if one matches the input then set something to the value of the loops current position, which is also the index of the current looked up item.
simple eg.
dim x,y,z as integer
dim aNames, aIndexes as array
dim sFind as string
for x = 1 to length(aNames)
if aNames(x) = sFind then y = x
y is then the index of the item in the array, then loop could be used to store these in an array also so instead of the above you would have:
z = 1
for x = 1 to length(aNames)
if aNames(x) = sFind then
aIndexes(z) = x
z = z + 1
endif
VB
Dim arr() As String = {"ravi", "Kumar", "Ravi", "Ramesh"}
Dim result = arr.Where(Function(a) a.Contains("ra")).Select(Function(s) Array.IndexOf(arr, s)).ToArray()
C#
string[] arr = { "ravi", "Kumar", "Ravi", "Ramesh" };
var result = arr.Where(a => a.Contains("Ra")).Select(a => Array.IndexOf(arr, a)).ToArray();
-----Detailed------
Module Module1
Sub Main()
Dim arr() As String = {"ravi", "Kumar", "Ravi", "Ramesh"}
Dim searchStr = "ra"
'Not case sensitive - checks if item starts with searchStr
Dim result1 = arr.Where(Function(a) a.ToLower.StartsWith(searchStr)).Select(Function(s) Array.IndexOf(arr, s)).ToArray
'Case sensitive - checks if item starts with searchStr
Dim result2 = arr.Where(Function(a) a.StartsWith(searchStr)).Select(Function(s) Array.IndexOf(arr, s)).ToArray
'Not case sensitive - checks if item contains searchStr
Dim result3 = arr.Where(Function(a) a.ToLower.Contains(searchStr)).Select(Function(s) Array.IndexOf(arr, s)).ToArray
Stop
End Sub
End Module
Never use .ToLower and .ToUpper.
I just had problems in Turkey where there are 4 "i" letters. When using ToUpper I got the wrong "Ì" one and it fails.
Use invariant string comparisons:
Const LNK as String = "LINK"
Dim myString = "Link"
Bad:
If myString.ToUpper = LNK Then...
Good and works in the entire world:
If String.Equals(myString, LNK , StringComparison.InvariantCultureIgnoreCase) Then...
This would do the trick, returning the values at indeces 0, 2 and 3.
Array.FindAll(arr, Function(s) s.ToLower().StartsWith("ra"))