Merge multiple list of string to list of object using VB.NET - vb.net

I have 3 list of strings.
List1 - Student Name List2 - Student School List3 - Student Location
Student 1 Student 1 School Student 1 Location
Student 2 Student 2 School Student 2 Location
Student 3 Student 3 School Student 3 Location
Student 4 Student 4 School Student 4 Location
Student 5 Student 5 School Student 5 Location
And a structure StudentDetails
Public Structure StudentDetails()
Public StudentName As String
Public StudentSchool As String
Public StudentLocation As String
End Structure
I want to make the first 3 list to List of StudentDetails
I have used the following code to do this
Dim StudentDetailsList As New List(Of StudentDetails)
For i = 0 to List1.Count - 1
Dim StudentDetail As New StudentDetail
With StudentDetail
.StudentName = List1(i)
.StudentSchool = List2(i)
.StudentLocation = List3(i)
End With
StudentDetailsList.Add(StudentDetail)
Next
Is there a better way to do this using Linq or some other method?

There are many ways to do that, some easier to read than others.
First, I would make StudentDetails a class instead of a structure (see, e.g., When should I use a struct instead of a class?.
Now that you have a class, you can give it a New constructor with parameters, as used in the third example here:
Option Infer On
Option Strict On
Module Module1
Public Class StudentDetails
Public Name As String
Public School As String
Public Location As String
Public Sub New()
' empty constuctor
End Sub
Public Sub New(name As String, school As String, location As String)
Me.Name = name
Me.School = school
Me.Location = location
End Sub
' make it easy to represent StudentDetails as a string...
Public Overrides Function ToString() As String
Return $"{Me.Name} {Me.School} {Me.Location}"
End Function
End Class
Sub Main()
Dim list1 As New List(Of String) From {"Adam", "Betty", "Charles", "Wilma"}
Dim list2 As New List(Of String) From {"Ace", "Best", "Classy", "Wacky"}
Dim list3 As New List(Of String) From {"Attic", "Basement", "Cellar", "Windowledge"}
' a not-very tidy example using Zip:
Dim StudentDetailsList = list1.Zip(list2, Function(a, b) New With {.name = a, .school = b}).Zip(list3, Function(c, d) New StudentDetails With {.Name = c.name, .School = c.school, .Location = d}).ToList()
' one way of writing out the StudentDetailsList...
For Each s In StudentDetailsList
Console.WriteLine(s.ToString())
Next
StudentDetailsList.Clear()
' a bit cleaner using a loop:
For i = 0 To list1.Count() - 1
StudentDetailsList.Add(New StudentDetails With {
.Name = list1(i),
.School = list2(i),
.Location = list3(i)})
Next
' another way of writing out the StudentDetailsList...
Console.WriteLine(String.Join(vbCrLf, StudentDetailsList))
StudentDetailsList.Clear()
' easy to write with a New constructor, but not necessarily as easy to read as the previous example:
For i = 0 To list1.Count() - 1
StudentDetailsList.Add(New StudentDetails(list1(i), list2(i), list3(i)))
Next
Console.WriteLine(String.Join(vbCrLf, StudentDetailsList))
Console.ReadLine()
End Sub
End Module
I used the $ string formatter in the .ToString() method: it was introduced with VS2015, so if you are using an earlier version you can use String.Format("{0} {1} {2}", Me.Name, Me.School, Me.Location) instead.
As a note on naming the properties of StudentDetails, the "Student" in StudentName, StudentSchool and StudentLocation is redundant.

Well, you can use an overload of Select extension method to projects each element of one of the list into a new StudentDetail by incorporating the element's index. Assuming the three lists have the same amount of elements, you can do the following:
// using C#
var result=List1.Select((e,i))=>new StudentDetail
{
StudentName =e,
StudentSchool = List2[i],
StudentLocation = List3[i]
}).ToList();
I think in Vb would be (sorry, I'm a c# programmer):
Dim StudentDetailsList=List1.Select(Function(e, i) _
New StudentDetail
With StudentDetail
.StudentName = e
.StudentSchool = List2(i)
.StudentLocation = List3(i)
End With).ToList();
But using a for is not a bad solution, in many cases is more readable.

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"

Combine three text files

How does one combine three text files together into one? I was also trying to make it alphabetical by state in the new text file. I figured how to combine two but three I am getting lost.
Public Class newsenatefrm
Dim current() As String = IO.File.ReadAllLines("Senate113.txt")
Dim retired() As String = IO.File.ReadAllLines("RetiredSen.txt")
Dim newSen() As String = IO.File.ReadAllLines("NewSen.txt")
Dim queryCurrent = From line In current
Let state = Split(","c)(1)
Let name = Split(","c)(0)
Let party = Split(","c)(2)
Order By state Ascending
Select state, name, party
Dim queryRetired = From line In retired
Let state = Split(","c)(1)
Let name = Split(","c)(0)
Let party = Split(","c)(2)
Order By state Ascending
Select state, name, party
Dim queryNew = From line In newSen
Let state = Split(","c)(1)
Let name = Split(","c)(0)
Let party = Split(","c)(2)
Order By state Ascending
Select state, name, party
Private Sub generatebtn_Click(sender As Object, e As EventArgs) Handles generatebtn.Click
IO.File.WriteAllText("Senate114.txt")
End Sub
End Class
I included sample text from the three text files below:
Senate113.txt:
Richard Shelby,Alabama,R
Bernard Sanders,Vermont,I
Kristen Gillibrand,New York,D
Retired.txt:
John Rockefeller,West Virginia,D
Tom Coburn,Oklahoma,R
Carl Levin,Michigan,D
NewSen.txt:
Shelly Capito,West Virginia,R
Steve Daines,Montana,R
Gary Peters,Michigan,D
As you're just learning Visual Basic, you might enjoy finding some new things in this answer.
You've got files with the data separated by commas, a common format known as a comma-separated values file, or CSV file. There are several parsers available for that format, I'm just using the TextFieldParser Class because it comes with VB.NET.
If you're going to be working with data, it is very often a good idea to make a class for the data items - it allows you to keep associated data together, with sensible names, and provide methods that work with that data.
So, you could have one main List(Of Senator) to which you can add more data (senators) from a file, perhaps like this:
Imports System.IO
Imports Microsoft.VisualBasic.FileIO
Module Module1
Public Class Senator
Property Name As String
Property State As String
Property Party As String
Public Sub New()
' Empty constructor
End Sub
Public Sub New(name As String, state As String, party As String)
Me.Name = name
Me.State = state
Me.Party = party
End Sub
Public Overrides Function ToString() As String
Return $"{Name}, {State}, {Party}"
End Function
End Class
Function GetSenators(fromFile As String) As List(Of Senator)
Dim s As New List(Of Senator)
Using csvReader As New TextFieldParser(fromFile)
csvReader.Delimiters = {","}
While Not csvReader.EndOfData
Dim parts = csvReader.ReadFields()
If parts.Count = 3 Then
s.Add(New Senator(parts(0), parts(1), parts(2)))
End If
End While
End Using
Return s
End Function
Sub Main()
Dim srcDir = "C:\temp"
Dim srcFiles = {"Senate113.txt", "RetiredSen.txt", "NewSen.txt"}
Dim combinedSenators As New List(Of Senator)
For Each f In srcFiles
Dim actualFile = Path.Combine(srcDir, f)
combinedSenators.AddRange(GetSenators(actualFile))
Next
Dim senatorsByState = combinedSenators.OrderBy(Function(sen) sen.State).Select(Function(s) s.ToString())
Console.WriteLine(String.Join(vbCrLf, senatorsByState))
'File.WriteAllLines("C:\temp\Senate114.txt", senatorsByState)
Console.Write("Finished.")
Console.ReadLine()
End Sub
End Module
Which, with the sample data in the question, outputs:
Richard Shelby, Alabama, R
Carl Levin, Michigan, D
Gary Peters, Michigan, D
Steve Daines, Montana, R
Kristen Gillibrand, New York, D
Tom Coburn, Oklahoma, R
Bernard Sanders, Vermont, I
John Rockefeller, West Virginia, D
Shelly Capito, West Virginia, R
There are many possible ways, but as it looks like the files you're dealing with will be quite small, you could read them all into a List(Of String) to start with:
Dim a = IO.File.ReadLines("C:\temp\Senate113.txt").ToList()
a.AddRange(IO.File.ReadLines("C:\temp\RetiredSen.txt"))
a.AddRange(IO.File.ReadLines("C:\temp\NewSen.txt"))
Then carry on as you did before, you just need to do it once instead of thrice.
(It's best to give a full path to a file.)

List(OF T).Contains Problem (Search through String or Integer Props of the Class)

List(Of T).Contains function help.
Here is my simple object.
Public Class Person
Name as string
Age as Integer
End Class
In the Sub:
Dim Person as new Person
Person.Name = "John"
Person.Age = 32
Dim myPersonList as new List(of Person)
myPersonList.Add(Person)
How would I exactly look up if the List Contains a person named "John" with the myPersonList.Contains() function, Or how would i look up myPersonListContains(AGE)???. I can't neccessarily put "John" or an Age because the Contains function is asking for the Person Class, and not the string "John". I'd like to use this method instead of writing a function that iterates through every Persons "Name"
I used the Find method of List(Of T). I made your Name and Age in the Person class real Properties.
Public Class Person
Public Property Name As String
Public Property Age As Integer
End Class
Private Sub OPCode()
Dim myPersonList As New List(Of Person)
Dim p1 As New Person
p1.Name = "John"
p1.Age = 32
myPersonList.Add(p1)
Dim p2 As New Person
p2.Name = "Mathew"
p2.Age = 73
myPersonList.Add(p2)
Dim p3 As New Person
p3.Name = "Mark"
p3.Age = 90
myPersonList.Add(p3)
Dim foundInList = myPersonList.Find(Function(x) x.Name = "John")
If foundInList Is Nothing Then
MessageBox.Show("John not found")
Else
Dim JohnsAgeIs = foundInList.Age
Debug.Print(JohnsAgeIs.ToString)
'32 appears in Immediate window
End If
End Sub
Corrected per comment by #jmcilhinney. Without checking first if a person was returned by .Find a NRE could occur.
Unless your item type implements IEquatable(Of T), Contains will only check for reference equality. If you do implement IEquatable(Of Person) in the Person class, that implementation will be specific and unchanging, so you still can't compare in different ways at different times. What you should do is use the LINQ Any method instead, e.g.
If myPersonList.Any(Function(p) p.Name = "John") Then
'...
End If
If myPersonList.Any(Function(p) p.Age = 32) Then
'...
End If

Can I force an object to be passed as "ByVal" (as a copy) in a call in .NET?

In the VB.NET code below I want to add three persons to a list of persons. I thought that I could use the same person-object over and over again and simply change it's name before next Add-call. I was hoping that each call should copy the object and add that copy to the list... But I end up with a list of three "Kim". It seems that the list elements refers to the same object.
Class Person
Public Name As String
End Class
Sub Foo()
Dim Persons as New List(Of Person)
Dim aPerson As New Person
aPerson.Name = "John"
Persons.Add(aPerson)
aPerson.Name = "Emma"
Persons.Add(aPerson)
aPerson.Name = "Kim"
Persons.Add(aPerson)
End Sub
Can I force that aPerson is copied in each Add-call? Or do I need to create separate objects for John, Emma and Kim?
What you are doing isn't good practice as you shouldn't use a class that way.
If your names come from a source like a list you could use a For Each loop.
Dim MyNames As New List(Of String)
MyNames.Add("John")
MyNames.Add("Emma")
MyNames.Add("Kim")
For Each person_name As String In MyNames
Dim aPerson As New Person
aPerson.Name = person_name
Persons.Add(aPerson)
Next
This approach could be very practical if the data comes from a file, SQL request or JSON for example.
Otherwise you would just have:
Dim Persons as New List(Of Person)
Dim aPerson As New Person
Dim bPerson As New Person
Dim cPerson As New Person
aPerson.Name = "John"
Persons.Add(aPerson)
bPerson.Name = "Emma"
Persons.Add(bPerson)
cPerson.Name = "Kim"
Persons.Add(cPerson)

VB.Net How to update every entry in an ArrayList?

I am using a structure similar to that below. I need to loop through the 'Persons' ArrayList and set every salary to 100, whilst leaving the LastNames intact.
Structure Person
Dim LastName As String
Dim salary As Integer
End Structure
public class Test
public Shared Sub Main
Dim Persons As New ArrayList
Dim Person As New Person
With Person
.LastName = "Smith"
.salary = 50
End With
Persons.Add(Person)
With Person
.LastName = "Jones"
.salary = 20
End With
Persons.Add(Person)
With Person
.LastName = "Brown"
.salary = 80
End With
Persons.Add(Person)
End Sub
End class
I realise that a simple For Each loop won't work here. I could copy each 'Person' to a second temporary arraylist and then delete the entry in the original arraylist, but I can't figure out how to change the salary for each person and 'Add' it back again whilst keeping the 'LastName' values as they originally were.
Use a List(Of Person) instead of ArrayList (implicitly Of Object).
And just write a helper function to simplify adding. You can iterate over the List(Of Person) easily since now it's typed as Person
Structure Person
Dim LastName As String
Dim salary As Integer
End Structure
Sub Main()
Dim Persons As New List(Of Person)()
AddPerson(Persons, "Smith", 50)
AddPerson(Persons, "Jones", 20) ' poor Jonesy
AddPerson(Persons, "Brown", 80)
For Each person In Persons
person.salary = 100
Next
End Sub
Public Sub AddPerson(persons As List(Of Person), lastName As String, salary As Integer)
persons.Add(New Person() With {.LastName = lastName, .salary = salary})
End Sub
Another point
Your original code works with a For Each loop
For Each p As Person In Persons
p.salary = 100
Next
but the risk of using an ArrayList is that you can add any object to it without error. Then you may run into an issue when casting the items back to Person if you weren't disciplined to always only add a Person to it. For example
Persons.Add(New Object)
For Each p As Person In Persons
p.salary = 100
Next
would iterate until the loop encoutered the New Object at the end, then would result in a runtime error. A List(Of Person) would prevent it from ever being added in the first place, which is why it is always preferred over ArrayList for new development.
A class might work better in this situation. Also, you could set the default value of Salary to be 100 so that each object already has a default value (no need to assign it later in a loop).
Public Class Person
Dim LastName As String = ""
Dim salary As Integer = 100
Public Sub New()
'
End Sub
Public Sub New(ByVal Last_Name As String, ByVal Salary As Integer)
Me.LastName = Last_Name
Me.salary = Salary
End Sub
End Class
The suggested loop:
For Each p As Person In Persons
p.salary = 100
Next
did not work as it did not permanently write the new value to 'Persons', but after further searching I found a loop that does:
For x = 0 To Persons.Count - 1
Dim p As Person = Persons(x)
p.salary = 100
Persons(x) = p
Next
I hope this helps someone else. I have also implemented the LIST ideas - thanks.