VB.NET Index out of Range exception related to text file - vb.net

I have some code I have used many times over which has always worked great for me. The latest use, however, throws an exception under certain circumstances that I cannot seem to resolve. Here it is:
I read from a text file to an array, use it as a binding source for some of my controls (it autofills 3 controls based on the selection of a single control). I created a Student class with 4 properties (Name, ID, DOB and DOE). Here is the code I use:
Private Sub autoFill()
Dim rost As String = "Roster.txt"
Dim lines As List(Of String) = File.ReadAllLines(rost).ToList
Dim list As List(Of Student) = New List(Of Student)
For i As Integer = 0 To lines.Count - 1
Dim data As String() = lines(i).Split(":")
list.Add(New Student() With {
.StudentName = data(0),
.StudentID = data(1),
.StudentDOB = data(2),
.StudentDOE = data(3)
})
Next
StudentBindingSource.DataSource = list
End Sub
Now here is the problem. In the "For" loop when I set i to 0 to lines.count -1 it throws this error:
VB>NET EXCEPTION
However...If I change i to 1 instead of 0 it works OR if I take away data(2) and data(3) it works with i = 0. I would prefer to use 0 so that I can have a blank line in the combobox or "--choose--", etc. The only thing I have thought that might be useful is that my first row in the text file has nothing to split. Here is the line format of the text file:
Student Name ID# DOB DOE <-----This header row is NOT in the text file
Last Name, First Name : 0000000 : 01/01/2021 : 01/01/2021
I'm going to assume I'm missing something really simple here. Any guidance would be greatly appreciated! Thank you.

Before we get to the actual problem, let's re-work some things.
A better way to structure code, especially when working with data loading, is to have a method that accepts an input and returns a result. Additionally, calling ToList() or ToArray() is a very expensive operation for performance. Very often you can improve performance dramatically by working with a lower-level IEnumerable for as long as possible.
With those principles in mind, consider this code:
Private Function ReadStudentData(fileName As String) As IEnumerable(Of Student)
Dim lines As IEnumerable(Of String) = File.ReadLines(fileName)
Return lines.
Select(Function(line) line.Split(":")).
Select(Function(data)
Return New Student() With {
.StudentName = data(0),
.StudentID = data(1),
.StudentDOB = data(2),
.StudentDOE = data(3)
}
End Function)
End Function
Private Sub autoFill()
StudentBindingSource.DataSource = ReadStudentData("Roster.txt")
End Sub
Now on to the actual issue. The problem was not from looping through the list variable. The problem is the data array. At some point you have a line that doesn't have enough elements. This is common, for example, as the last line in a file.
There are many ways to address this. In some cases, the exception is already the appropriate result, because if you have bad data you really don't want to continue. In other cases you want to log the bad records, perhaps to a report you can easily review later. Or maybe you just want to ignore the error, or pre-filter for rows with the right number of columns. Here is an example of the last option:
Private Function ReadStudentData(fileName As String) As IEnumerable(Of Student)
Return File.ReadLines(fileName).
Select(Function(line) line.Split(":")).
Where(Function(data) data.Length = 4).
Select(Function(data)
Return New Student() With {
.StudentName = data(0),
.StudentID = data(1),
.StudentDOB = data(2),
.StudentDOE = data(3)
}
End Function)
End Function
Private Sub autoFill()
StudentBindingSource.DataSource = ReadStudentData("Roster.txt")
End Sub

The problem is that you didn't check 'data' to have enough elements to create the 'Student'. A simple check should fix it.
Private Sub autoFill()
Dim rost As String = "Roster.txt"
Dim lines As List(Of String) = File.ReadAllLines(rost).ToList
Dim list As List(Of Student) = New List(Of Student)
For i As Integer = 0 To lines.Count - 1
Dim data As String() = lines(i).Split(":"c)
'Check data
If data.Length >= 4 Then '
list.Add(New Student() With {
.StudentName = data(0),
.StudentID = data(1),
.StudentDOB = data(2),
.StudentDOE = data(3)
})
End If
Next
StudentBindingSource.DataSource = list
End Sub

try this code:
Dim list As List(Of Student) = New List(Of Student)(100)
basically initialize the student list with a capacity. This is the capacity of the list, not the count/length.

Related

Visual Basic Text File

I'm currently learning about Visual Basic text files but I came across a problem. I'm supposed to create a text file (Players) with data inside and I have to design a form with listbox to include the players’ names that are more than 30 years old.
This is my current code:
Dim q1 = From itm As String In IO.File.ReadAllLines("Players.txt")
Let Data=itm.Split(","c)
Let fname = Data(0)
Let age = Data(4)
Let newline = fname * " "& age
Where age > 30
For Each itm1 As String in q1
ListBox1.Items.Add(itm1)
Next
My expected output should show the names of players that are over 30 years old. Thank you in advance to anyone that can help me solve this issue.
You can use linq. For example: assume you have a txt like that
Giuseppe, 30
Pippo, 13
Luca, 32
to extract only over 30 years old you can do...
Dim obj = My.Computer.FileSystem.ReadAllText("Players.txt").Split(vbCrLf).ToList()
Dim ret = (From a In obj Where a.Split(",")(1) > 30 Select a).ToList
The result is
Luca, 32
Best to use a class to define Player. I also made a class Players to hide the file processing from the consumer.
Public Class Player
Public Property Name As String
Public Property Age As Integer
End Class
Public Class Players
Private _list As New List(Of Player)()
Public ReadOnly Property List As IEnumerable(Of Player)
Get
Return _list
End Get
End Property
Public Sub New(path As String)
Dim lines = File.ReadAllLines(path)
For Each line In lines
Dim split = line.Split(","c)
If split.Count = 2 Then
_list.Add(New Player() With {.Name = split(0), .Age = Integer.Parse(split(1))})
End If
Next
End Sub
End Class
And use databinding to populate the ListBox
Dim ps = New Players("Players.txt")
Me.ListBox1.DataSource = ps.Items.Where(Function(p) p.Age >= 30).ToList()
Me.ListBox1.DisplayMember = "Name"
If you're not into the whole Players class and Items property, you can still use the Player class, and just do all the processing in your consuming code (it's basically the same thing, but the processing code is not encapsulated in the model).
Dim ps = New List(Of Player)()
Dim lines = File.ReadAllLines("Players.txt")
For Each line In lines
Dim split = line.Split(","c)
If split.Count = 2 Then
ps.Add(New Player() With {.Name = split(0), .Age = Integer.Parse(split(1))})
End If
Next
Me.ListBox1.DataSource = ps.Where(Function(p) p.Age >= 30).ToList()
Me.ListBox1.DisplayMember = "Name"

Use .Contains() to match on a property of a property of <T> in LINQ query

Looking for help on how to perform a LINQ query using the .Contains() method of a List(Of T) to get back elements that are not contained in a second List(Of T) based on a property of a property of T in the first List(Of T).
Here is some sample code that I wrote up, this scenario is ficticious, but the concept is still there.
Module Module1
Sub Main()
' Get all Files in a directory that contain `.mp` in the name
Dim AllFiles As List(Of IO.FileInfo) = New IO.DirectoryInfo("C:\Test\Path").GetFiles("*.mp*").ToList
Dim ValidFiles As New List(Of fileStruct)
' Get all Files that actually have an extension of `.mp3`
AllFiles.ForEach(Sub(x) If x.Extension.Contains("mp3") Then ValidFiles.Add(New fileStruct(prop1:=x.Name, path:=x.FullName)))
' Attempting the get all files that are not listed in the Valid files list
Dim InvalidFiles As IO.FileInfo() = From file As IO.FileInfo In AllFiles Where Not ValidFiles.Contains(Function(x As fileStruct) x.fleInfo.FullName = file.FullName) Select file
' Errors on the `.Contains()` method because I have no idea what I'm doing and I am basically guessing at this point
'Here is the same but instead using the `.Any()` Method
Dim InvalidFiles As IO.FileInfo() = From file As IO.FileInfo In AllFiles Where Not ValidFiles.Any(Function(x As fileStruct) x.fleInfo.FullName = file.FullName) Select file
' This doesn't error out, but all files are returned
End Sub
Public Structure fileStruct
Private _filePath As String
Private _property1 As String
Public ReadOnly Property property1 As String
Get
Return _property1
End Get
End Property
Public ReadOnly Property fleInfo As IO.FileInfo
Get
Return New IO.FileInfo(_filePath)
End Get
End Property
Public Sub New(ByVal prop1 As String, ByVal path As String)
_property1 = prop1
_filePath = path
End Sub
End Structure
End Module
This is a more or less direct implementation of the MP3 files list in the question. I did use a FileItem class instead of a structure. The good part is afterwards:
' note: EnumerateFiles
Dim AllFiles As List(Of IO.FileInfo) = New IO.DirectoryInfo("M:\Music").
EnumerateFiles("*.mp*", IO.SearchOption.AllDirectories).ToList()
Dim goofyFilter As String() = {"g", "h", "s", "a"}
' filter All files to those starting with the above (lots of
' Aerosmith, Steely Dan and Heart)
Dim ValidFiles As List(Of FileItem) = AllFiles.
Where(Function(w) goofyFilter.Contains((w.Name.ToLower)(0))).
Select(Function(s) New FileItem(s.FullName)).ToList()
Dim invalid As List(Of FileInfo)
invalid = AllFiles.Where(Function(w) Not ValidFiles.
Any(Function(a) w.FullName = a.FilePath)).ToList()
This is much the same as Sam's answer except with your file/mp3 usage. AllFiles has 809 items, ValidFiles has 274. The resulting invalid list is 535.
Now, lets speed it up 50-60x:
Same starting code for AllFiles and ValidFiles:
Dim FileItemValid = Function(s As String)
Dim valid As Boolean = False
For Each fi As FileItem In ValidFiles
If fi.FilePath = s Then
valid = True
Exit For
End If
Next
Return valid
End Function
invalid = AllFiles.Where(Function(w) FileItemValid(w.FullName) = False).ToList()
With a Stopwatch, the results are:
Where/Any count: 535, time: 572ms
FileItemValid count: 535, time: 9ms
You get similar results with a plain old For/Each loop that calls an IsValid function.
If you do not need other FileInfo, you could create your AllFiles as a list of the same structure as you are receiving so you can do property vs property compares, use Except and Contains:
AllFiles2 = Directory.EnumerateFiles("M:\Music", "*.mp3", IO.SearchOption.AllDirectories).
Select(Function(s) New FileItem(s)).ToList()
Now you can use Contains with middling results:
invalid2 = AllFiles2.Where(Function(w) Not ValidFiles.Contains(w)).ToList()
This also allows you to use Except which is simpler and faster:
invalid2 = AllFiles2.Except(ValidFiles).ToList()
Where/Contains count: 535, time: 74ms
Except count: 535, time: 3ms
Even if you need other items from FileInfo, you can easily fetch them given the filename
As others have noted, .Except() is a better approach but here is an answer to your question:
List<int> list1 = new List<int> { 1, 2, 3 };
List<int> list2 = new List<int> { 3, 4, 5 };
List<int> list3 = list1.Where(list1value => !list2.Contains(list1value)).ToList(); // 1, 2
Based on comments here as an example using different types. This query use .Any()
List<Product> list1 = new List<Produc> { ... };
List<Vendor> list2 = new List<Vendor> { ... };
List<Product> list3 = list1.Where(product => !list2.Any(vendor => product.VendorID == vendor.ID)).ToList();
// list3 will contain products with a vendorID that does not match the ID of any vendor in list2.
Simply use Except as CraigW suggested. You have to do some projections (select) to get it done.
Dim InvalidFiles as IO.FileInfo() = AllFiles.Select(Function(p) p.FullName).Except(ValidFiles.Select(Function(x) x.fleInfo.FullName)).Select(Function(fullName) New IO.FileInfo(fullName)).ToArray()
Note: This code is not really efficient and also not very readable but works.
But i would go for something like this:
Dim AllFiles As List(Of IO.FileInfo) = New IO.DirectoryInfo("C:\MyFiles").GetFiles("*.mp*").ToList
Dim ValidFiles As New List(Of fileStruct)
Dim InvalidFiles as New List(Of FileInfo)
For Each fileInfo As FileInfo In AllFiles
If fileInfo.Extension.Contains("mp3") Then
ValidFiles.Add(New fileStruct(prop1:=fileInfo.Name, path:=fileInfo.FullName))
Else
InvalidFiles.Add(fileInfo)
End If
Next
Simple, fast and readable.

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

VB.net Count multiples of specific items in a listbox

I've had a look around the internet for a few hours and found nothing to help my situation which I think is quite a common one.
I would like to count specific items in my list box and find out how many there are of each one. I wanna assign this value to a variable. I think I have most of it covered however I cannot get the code to work.
To explain I want something where it counts how many 'Garlic Bread' I have in the list and then assign it to a variable like 'item1' and so on for all the items.
Cheers
Public Class MyListItem
Public Sub New(id As Integer, name As String)
LiId = id
LiName = name
End Sub
Public LiId As Integer
Public LiName As String
End Class
Private Sub Test()
Dim listbox2 As New ListBox()
Dim list As New List(Of MyListItem)()
list.Add(New MyListItem(1, "a"))
list.Add(New MyListItem(2, "a"))
list.Add(New MyListItem(3, "b"))
list.Add(New MyListItem(4, "c"))
listbox2.DataSource = list
listbox2.ValueMember = "LiId"
listbox2.DisplayMember = "LiName"
Dim countOfA = Aggregate itm In DirectCast(listbox2.DataSource, List(Of MyListItem))
Where itm.LiName = "a"
Into Sum(1)
End Sub
Result: countOfA = 2

Count occurance of specific words in a text file in vb.net

I'm trying to count the number of an item in a text file, by counting each instance the item was entered into the file earlier on in the program.
I already have the text read from the file and in a text box. The problem is that my current code was just counting the characters in the textbox and not the number of times my desired word was in the file.
For Each desiredword As String In txtContentofFile.Text
intdesiredword = intdesiredword + 1
txtdesiredwordcount.Text = intdesiredword
Next
This counts the characters in the textbox instead of counting the number of desired words. I tried repeatedly before asking help and searched extensively, but I just don't understand what's wrong with my code. Please help :)
You can use Split Function :
C#:
int count = txtContentofFile.Text.Split(desiredword).Length - 1;
VB.net:
Dim count As Integer = txtContentofFile.Text.Split(desiredword).Length - 1
I prefer to use Regular Expressions in this type of situation. They are very tricky to understand but they are extremely powerful and typically faster than other string manipulation techniques.
Dim AllMatchResults As MatchCollection
Try
Dim RegexObj As New Regex(desiredword)
AllMatchResults = RegexObj.Matches(txtContentofFile.Text)
If AllMatchResults.Count > 0 Then
' Access individual matches using AllMatchResults.Item[]
Else
' Match attempt failed
End If
Catch ex As ArgumentException
'Syntax error in the regular expression
End Try
In your case you are looking for the value from AllMatchResults.Count.
Using a great Regular Expression tool like RegexBuddy to build and test the expressions is a great help too. (The above code snippet was generated by RegexBuddy!)
Try this:
Dim text As String = IO.File.ReadAllText("C:\file.txt")
Dim wordsToSearch() As String = New String() {"Hello", "World", "foo"}
Dim words As New List(Of String)()
Dim findings As Dictionary(Of String, List(Of Integer))
'Dividing into words
words.AddRange(text.Split(New String() {" ", Environment.NewLine()}, StringSplitOptions.RemoveEmptyEntries))
findings = SearchWords(words, wordsToSearch)
Console.WriteLine("Number of 'foo': " & findings("foo").Count)
Function used:
Private Function SearchWords(ByVal allWords As List(Of String), ByVal wordsToSearch() As String) As Dictionary(Of String, List(Of Integer))
Dim dResult As New Dictionary(Of String, List(Of Integer))()
Dim i As Integer = 0
For Each s As String In wordsToSearch
dResult.Add(s, New List(Of Integer))
While i >= 0 AndAlso i < allWords.Count
i = allWords.IndexOf(s, i)
If i >= 0 Then dResult(s).Add(i)
i += 1
End While
Next
Return dResult
End Function
You will have not only the number of occurances, but the index positions in the file, grouped easily in a Dictionary.
Try the following code
Function word_frequency(word_ As String, input As String) As Integer
Dim ct = 0
Try
Dim wLEN = word_.Length
Do While input.IndexOf(word_) <> -1
Dim idx = input.IndexOf(word_) + wLEN
ct += 1
input = input.Substring(idx)
Loop
Catch ex As Exception
End Try
Return ct
End Function