Accumulating data based on two criteria - arrays

I am trying to write a VB.NET program that reads data from a file and does a count for each column as shown in the format below and also writes to an output file.
I am able to do the count but I am unable to write the output per restaurant and day.
From what I have, I can only write the total sum from the array index.
Here is the code I have so far:
Dim IntSubjectArray(23) As String
OpenFileDialog1.ShowDialog()
strInputPath = OpenFileDialog1.FileName
FileOpen(IntInputFileName, strInputPath, OpenMode.Input)
Do While Not EOF(IntInputFileName)
Dim StrReadLine As String = LineInput(IntInputFileName)
Dim StrSplitRecord() As String = Split(StrReadLine, ",")
IntRestaurant = StrSplitRecord(0)
IntDay = StrSplitRecord(1)
Meal1 = StrSplitRecord(2)
Meal2 = StrSplitRecord(3)
Meal3 = StrSplitRecord(4)
If SaveDay <> IntDay Then
IntMealArray(meal1) += 1
IntMealArray(meal2) += 1
IntMealArray(meal3) += 1
SaveDay = IntDay
SaveDay = 0
End If
savetown = IntExamTown
Loop
Call WriteOutputArray()
FileClose(IntInputFileName)
MessageBox.Show("File written to specified location")
Public Sub WriteOutputArray()
IntOutputFileName = FreeFile()
For Each Array As String In IntMealArray
FileOpen(IntOutputFileName, "C:\Users\ireport\foodCount.txt", OpenMode.Append)
WriteLine(IntOutputFileName, IntMealArray(Array))
FileClose(IntOutputFileName)
Next
End Sub
File format is
001,1,5,6,21
001,1,5,6,21
001,1,5,6,21
001,1,10,12,18
001,2,5,6,19
001,2,8,9,19
001,2,6,19,21
001,2,5,6,21
001,3,7,12,18
001,3,8,12,19
001,3,7,12,18
040,4,7,12,18
040,4,7,12,18
040,4,7,12,18
040,4,9,12,19
Key:
The format is 001 is restaurant 1, then day 1, then foods eaten by a particular customer (there are to 23 different kinds of food), with each kind of meal represented by a code 1 to 23 as in the file.
Expected output is count of food eaten in each resturant in each day by a customer e.g.:
Rest day Rice Beans Yam Meat Snack coke Burger Meal8 Meal9 Meal10 M11 M12
001 1 0 0 0 0 3 3 0 0 0 1 0 1
001 2 0 0 0 0 2 3 0 1 1 0 0 0
001 3 0 0 0 0 0 0 2 1 0 0 0 3
040 4 0 0 0 0 0 0 3 0 1 0 0 4

First you need to get your data into some format which will make it easier to see in the code which part is which. An easy way to do that is create a Class with properties which have meaningful names.
Then you can group the data by restaurant, and for each restaurant you can group the data for each date.
As the output is in columns of the widths of the names of the foods, you need to take those names into account when formatting the output.
For simplicity, I created a console app instead of a Windows Forms app. Also, I would split it up into more methods if I was doing this for more than a proof-of-concept.
Imports System.IO
Imports System.Text
Module Module1
Dim Foods As Dictionary(Of String, String)
Class Datum
Property Restaurant As String
Property Day As Integer
Property FoodCodes As List(Of String)
Public Overrides Function ToString() As String
' Useful for debugging.
Return $"{Restaurant} {Day} " & String.Join(",", FoodCodes)
End Function
End Class
Sub LoadFoods()
' Generate some food names. The first food name has a code of "1".
Foods = New Dictionary(Of String, String)
Dim names = {"Rice", "Beans", "Banana", "Meat", "Snacks", "Potato", "Spinach",
"Fish", "Aubergine", "Peas", "Egg", "Chicken", "Cheese", "Onion",
"Carrots", "Brocolli", "Asparagus", "Garlic", "Cabbage", "Coconut", "Yam",
"Naan", "Lentils"}
For i = 1 To names.Count
Foods.Add(i.ToString(), names(i - 1))
Next
End Sub
Sub Main()
LoadFoods()
Dim src = "C:\temp\FoodRecords.txt"
Dim dest = "C:\temp\meals.txt"
Dim data As New List(Of Datum)
For Each line In File.ReadLines(src)
Dim parts = line.Split({","c})
If parts.Count = 5 Then
Dim d As New Datum With {.Restaurant = parts(0),
.Day = Integer.Parse(parts(1)),
.FoodCodes = parts.Skip(2).OrderBy(Function(s) s).ToList()}
data.Add(d)
End If
Next
' Prepare information on the widths of the columns to be output...
Dim colWidths As New List(Of Integer)
colWidths.Add(-("Restaurant".Length))
colWidths.Add(-("Day".Length))
For Each food In Foods
colWidths.Add(food.Value.Length)
Next
' Group the data by restaurant...
Dim restaurantData = From d In data
Group By RestCode = d.Restaurant
Into RestData = Group
Using sw As New StreamWriter(dest)
sw.Write("Restaurant Day ")
sw.WriteLine(String.Join(" ", Foods.Select(Function(f) f.Value)))
For Each x In restaurantData
'Console.WriteLine(x.RestCode & " " & String.Join(",", x.RestData))
' Get each day of data for this restaurant
Dim restaurantDay = From y In x.RestData
Group By Day = y.Day
Into DayData = Group
For Each rd In restaurantDay
' Count the occurrences of food codes for this day...
Dim dayFoodCounts As New Dictionary(Of String, Integer)
For Each fd In rd.DayData
For Each fc In fd.FoodCodes
If dayFoodCounts.ContainsKey(fc) Then
dayFoodCounts(fc) += 1
Else
dayFoodCounts.Add(fc, 1)
End If
Next
Next
' Generate the first two columns
Dim sb As New StringBuilder()
Dim fmt = "{0," & colWidths(0) & "}"
sb.AppendFormat(fmt, x.RestCode)
sb.Append(" ")
fmt = "{0," & colWidths(1) & "}"
sb.AppendFormat(fmt, rd.Day)
sb.Append(" ")
' Generate the columns with food consumption counts
Dim n = 0
For Each kvp In Foods
If dayFoodCounts.ContainsKey(kvp.Key) Then
sb.Append(String.Format("{0," & colWidths(n + 2) & "}", dayFoodCounts(kvp.Key)) & " ")
Else
' no count for this food item, so fill it with spaces
sb.Append(New String(" "c, colWidths(n + 2) + 1))
End If
n += 1
Next
sw.WriteLine(sb.ToString())
Next
Next
End Using
Console.WriteLine("Done.")
Console.ReadLine()
End Sub
End Module
Given the sample data from the question, the above code generates a file with this content:
Restaurant Day Rice Beans Banana Meat Snacks Potato Spinach Fish Aubergine Peas Egg Chicken Cheese Onion Carrots Brocolli Asparagus Garlic Cabbage Coconut Yam Naan Lentils
001 1 3 3 1 1 1 3
001 2 2 3 1 1 3 2
001 3 2 1 3 2 1
040 4 3 1 4 3 1

(In my previous solution I had wrongly assumed that the numbers are counts and each column represents an article, which was not the case. Here my new solution.)
I would separate reading the file from writing the table. To be able to represent the content of the file easily, let's create a class to represent food.
Public Class Food
Public Property Restaurant As String
Public Property Day As Integer
Public Property ArticleNo As Integer
Public Property Quantity As Integer
End Class
The Quantity property is not strictly necessary, since it will always be 1. But it seems logical to have it in case the file format evolves in future.
Now we can read the file like this
Public Function ReadFoodFile(inputPath As String) As List(Of Food)
Dim foodList = New List(Of Food)
For Each line As String In File.ReadLines(inputPath)
Dim parts As String() = line.Split(",")
If parts.Length > 2 Then 'Make sure we don't try to read an empty line,
' e.g.at the end of the file.
Dim dayNo As Integer = CInt(parts(1))
For i = 2 To parts.Length - 1
Dim articleNo As Integer
If Int32.TryParse(parts(i), articleNo) AndAlso articleNo <> 0 Then
Dim food = New Food()
food.Restaurant = parts(0)
food.Day = dayNo
food.ArticleNo = articleNo
food.Quantity = 1
foodList.Add(food)
End If
Next
End If
Next
Return foodList
End Function
The function for reading the file has the input path as parameter and returns a list of food where each entry corresponds to one restaurant, one day and one food article.
This was the easy part. Writing the table is complicated as we must group by restaurants and days as well as by article for each row. Then we must be able to look up the article by its article number. We need a class representing an article:
Public Class Article
Public Property ArticleNo As Integer
Public Property Name As String
Public Sub New(articleNo As Integer, name As String)
Me.ArticleNo = articleNo
Me.Name = name
End Sub
Private Shared _allArticles = New Article() {
New Article(1, "Rice"), New Article(2, "Beans"), New Article(3, "Yam"), New Article(4, "Meat"),
New Article(5, "Snack"), New Article(6, "Coke"), New Article(7, "Burger"), New Article(8, "Meal8"),
New Article(9, "Meal9"), New Article(10, "Meal10"), New Article(11, "M11"), New Article(12, "M12"),
New Article(13, "M13"), New Article(14, "M14"), New Article(15, "M15"), New Article(16, "M16"),
New Article(17, "M17"), New Article(18, "M18"), New Article(19, "M19"), New Article(20, "M20"),
New Article(21, "M21"), New Article(22, "M22"), New Article(23, "M23")
}
Shared ReadOnly Property AllArticles() As Article()
Get
Return _allArticles
End Get
End Property
End Class
Besides article no. and name, it contains a shared property returning a list of articles. In a real-life application, the list of articles should probably be read from a file or a database instead of being hard-coded.
Now, we can formulate the Sub writing the table. It makes heavy use of LINQ and uses the new ValueTuple type available since VB/VS 2017.
Public Sub WriteFoodTable(outputPath As String, foods As IEnumerable(Of Food))
Const ColumnSize = 8
'Create an IEnumerable(Of (Restaurant As String,
' Day As Integer,
' Articles As Dictionary(Of Integer, Integer))
' )
' )
' where the dictionary stores article quantities using the article no. as key.
Dim groupedFood = From food In foods
Group By food.Restaurant, food.Day Into g1 = Group
Select (
Restaurant:=Restaurant, Day:=Day,
Articles:=
(From x In g1
Group By x.ArticleNo Into g2 = Group
Select (ArticleNo:=ArticleNo,
Quantity:=g2.Sum(Function(f) f.Quantity))
).ToDictionary(Function(a) a.ArticleNo, Function(a) a.Quantity)
)
Using writer As New StreamWriter(outputPath)
' Write header
writer.Write("Rest Day")
For Each art In Article.AllArticles
writer.Write(art.Name.PadLeft(ColumnSize))
Next
writer.WriteLine()
' Write rows
For Each row In groupedFood
writer.Write(row.Restaurant.PadRight(5))
writer.Write(row.Day.ToString().PadLeft(4))
For Each art In Article.AllArticles
Dim quantity As Integer
row.Articles.TryGetValue(art.ArticleNo, quantity) ' yields 0 if not found.
writer.Write(quantity.ToString().PadLeft(ColumnSize))
Next
writer.WriteLine()
Next
End Using
End Sub
Putting things together
Dim foods As List(Of Food) = ReadFoodFile(inputPath)
WriteFoodTable(outputPath, foods)
See also:
Tuples (Visual Basic)
LINQ in Visual Basic

Related

VBA Excel Problem Using an Array of Custom Classes in a Custom Class

This is frustrating. I feel I am doing something stupid but can't nail it, so any help appreciated. I am of course a novice so I expect I am making a fundamental error.
Issue is:
I have created two apparently simple Custom Classes. The first defines a record of five fields (String, Integer and Currency) The second, defines an Array of the Record Objects defined in the first Class and adds a few simple reference fields. (String etc, nothing complex)
I have a simple test program. It first declares an Array of Records (50), then creates all the objects by "Set...= New" in a For Loop in the Initialize Sub. So far so (apparently) good.
The Test code writes 3 (garbage) records to the Array. After experimenting the Test sequence is now: Add a Record to the Array (Locations (0) to (2)). Retrieve a record from Location (0) (Which shouldn't change but does) immediately after each Add and print the result.
After all three have been written, the results are again printed for all three locations, using a For...Next loop. The results are shown in the debug.print output below:
Record 0 A nice Bunch of Flowers Bunch of Flowers 1 20
Record 1 A nsdfgh of Flowers Bunch of Fgfffwers 4 23345
Record 2 A nsdf3 Also Flowers BunchThirds 4 23345
Record 2 A nsdf3 Also Flowers BunchThirds 4 23345
Record 2 A nsdf3 Also Flowers BunchThirds 4 23345
Record 2 A nsdf3 Also Flowers BunchThirds 4 23345`
i.e. It Seems that - If re-read at the time of writing the record can be retrieved. - As soon as a further record is written, all records in the Array up to the highest one written become the latest and (not shown above) If I try to read records above (2) they are empty.
What am I doing wrong? It looks as though my latest write to the Array is always being written to all locations to which I have previously written instead of just the one addressed.
The relevant code is:
In the Array Class Declarations:
Dim intSize As Integer 'The currently declared size of the Array
Dim trrRec(50) As clsTransRecord 'Shown hard coded to 50 here for test.
In The Array Class Initialization:
Private Sub Class_Initialize()
Dim l As Integer 'Counter
intSize = 50 'The currently declared size of the Array
'Create the Objects
'==================
For l = 0 To intSize
Set trrRec(l) = New clsTransRecord
Next l
End Sub
In the Test Code: (Loads Three Records with dummy data and then Adds them to the Array)
Private Sub CommandButton2_Click()
Dim trcTest As clsTransRecord
Dim trcTest2 As clsTransRecord
Set trcTest = New clsTransRecord
Set trcTest2 = New clsTransRecord
Dim j As Integer
Dim traTest As clsTransArray
Set traTest = New clsTransArray
trcTest.LoadRecord "Record 0", "Bunch of Flowers", "A nice Bunch of Flowers", 1, 20
traTest.AddRecord trcTest
Set trcTest2 = traTest.GetRecordAccount(0)
Debug.Print trcTest2.TrCat, trcTest2.TrDesc, trcTest2.TrItem, trcTest2.TrTransDay, trcTest2.TrValue
trcTest.LoadRecord "Record 1", "Bunch of Fgfffwers", "A nsdfgh of Flowers", 4, 23345
traTest.AddRecord trcTest
Set trcTest2 = traTest.GetRecordAccount(0)
Debug.Print trcTest2.TrCat, trcTest2.TrDesc, trcTest2.TrItem, trcTest2.TrTransDay, trcTest2.TrValue
trcTest.LoadRecord "Record 2", "BunchThirds", "A nsdf3 Also Flowers", 4, 23345
traTest.AddRecord trcTest
Set trcTest2 = traTest.GetRecordAccount(0)
Debug.Print trcTest2.TrCat, trcTest2.TrDesc, trcTest2.TrItem, trcTest2.TrTransDay, trcTest2.TrValue
Debug.Print
For j = 0 To 5
Set trcTest2 = traTest.GetRecordAccount(j)
Debug.Print trcTest2.TrCat, trcTest2.TrDesc, trcTest2.TrItem, trcTest2.TrTransDay, trcTest2.TrValue
Next
End Sub
The AddRecord Sub is:
Public Sub AddRecord(clsNewRcd As clsTransRecord)
intRcdCnt = intRcdCnt + 1 'Increment the Record Counter
'Write the Record
'================
Set trrRec(intRcdCnt - 1) = clsNewRcd
End Sub
And the GetRecordAccount Function is:
'Gets an individual Record from the Object.
Public Function GetRecordAccount(k As Integer) As clsTransRecord
Set GetRecordAccount = trrRec(k)
End Function
clsTransRecord code is shown below: The entries relate to Bank Records and Descriptions
Declarations:
'clsTransRecord Variables
Private strCat As String '- Category of Transaction. Allows Grouping of Items. Not always used
Private strItem As String '- Describes the Item as it appears in the Budget Entry or Bank Statement. Used to compare Budheted to Actual
'so can be difficult to read due to strange Bank Statements
Private strDesc As String '- The longer, uderstandable, version of the Item Description.
Private intTransDay As Integer '- The day of the month on which the transaction occurs
Private curValue As Currency '- The Value of the Transaction. Positive for Income, Negative for Expenditure.
The clsTransRecord Class LoadRecord Code is
Public Sub LoadRecord(strRecCat As String, strRecItem As String, strRecDesc As String, intRecTransDay As Integer, curRecValue As Currency)
'Loads an individual Record
strCat = strRecCat 'Record Category
strItem = strRecItem 'Short Item Budget or Statement description.
strDesc = strRecDesc 'Full Description of Item
intTransDay = intRecTransDay 'Day on which the transaction happened/will happen
curValue = curRecValue 'Value of the Transaction
End Sub
The Initialize Sub for the clsTransRecord is below.
Private Sub Class_Initialize()
'Clears everything
strCat = "" '- Category of Transaction.
strItem = "" '- Describes the Item as it appears in the Budget Entry or Bank Statement
strDesc = "" '- The longer, uderstandable, version of the Item Description.
intTransDay = 0 '- The day of the month on which the transaction occurs
curValue = 0 '- The Value of the Transaction
End Sub
The problem is that you change the same instance of the record named trcTest. You add this instance and change it again etc. So you just add and change the same location in memory every time. Therefore you have same results.
If you need three instances, then you need to create three instances, e.g. something like this. HTH
Private Sub CommandButton2_Click()
' Array wrapper
Dim traTest As clsTransArray
Set traTest = New clsTransArray
' New records
Dim trcTest0 As clsTransRecord
Dim trcTest1 As clsTransRecord
Dim trcTest2 As clsTransRecord
' Record for print
Dim trcTestPrint As clsTransRecord
Set trcTest0 = New clsTransRecord
Set trcTest1 = New clsTransRecord
Set trcTest2 = New clsTransRecord
' Firts record
trcTest0.LoadRecord "Record 0", "Bunch of Flowers", "A nice Bunch of Flowers", 1, 20
traTest.AddRecord trcTest0
Set trcTestPrint = traTest.GetRecordAccount(0)
Debug.Print trcTestPrint.TrCat, trcTestPrint.TrDesc, trcTestPrint.TrItem, trcTestPrint.TrTransDay, trcTestPrint.TrValue
' Second record
trcTest1.LoadRecord "Record 1", "Bunch of Fgfffwers", "A nsdfgh of Flowers", 4, 23345
traTest.AddRecord trcTest1
Set trcTestPrint = traTest.GetRecordAccount(1)
Debug.Print trcTestPrint.TrCat, trcTestPrint.TrDesc, trcTestPrint.TrItem, trcTestPrint.TrTransDay, trcTestPrint.TrValue
' Third record
trcTest2.LoadRecord "Record 2", "BunchThirds", "A nsdf3 Also Flowers", 4, 23345
traTest.AddRecord trcTest2
Set trcTestPrint = traTest.GetRecordAccount(2)
Debug.Print trcTestPrint.TrCat, trcTestPrint.TrDesc, trcTestPrint.TrItem, trcTestPrint.TrTransDay, trcTestPrint.TrValue
Debug.Print
Dim j As Integer
For j = 0 To 5
Set trcTestPrint = traTest.GetRecordAccount(j)
Debug.Print trcTestPrint.TrCat, trcTestPrint.TrDesc, trcTestPrint.TrItem, trcTestPrint.TrTransDay, trcTestPrint.TrValue
Next
End Sub

Need Help properly displaying an array in a listbox

Public Class Form1
Private Sub btnCalculate_Click(sender As Object, e As EventArgs) Handles btnCalculate.Click
Dim Teams() As String = IO.File.ReadAllLines("SBWinners.txt")
Dim Team As String
Dim SteelersWins As Integer = 0
Dim RaidersWins As Integer = 0
Dim PackersWins As Integer = 0
Dim CowboysWins As Integer = 0
Dim GiantsWins As Integer = 0
Dim RamsWins As Integer = 0
Dim RavensWins As Integer = 0
Dim SaintsWins As Integer = 0
Dim FortyNinersWins As Integer = 0
Dim RedskinsWins As Integer = 0
Dim BroncosWins As Integer = 0
Dim PatriotsWins As Integer = 0
Dim ColtsWins As Integer = 0
Dim DolphinsWins As Integer = 0
Dim BearsWins As Integer = 0
Dim JetsWins As Integer = 0
Dim ChiefsWins As Integer = 0
Dim BuccWins As Integer = 0
For Each team In Teams
If team = "Steelers" Then
SteelersWins += 1
End If
If team = "Raiders" Then
RaidersWins += 1
End If
If team = "Packers" Then
PackersWins += 1
End If
If team = "Cowboys" Then
CowboysWins += 1
End If
If Team = "Giants" Then
GiantsWins += 1
End If
If team = "Rams" Then
RamsWins += 1
End If
If team = "Ravens" Then
RavensWins += 1
End If
If team = "Saints" Then
SaintsWins += 1
End If
If team = "Forty-Niners" Then
FortyNinersWins += 1
End If
If team = "Redskins" Then
RedskinsWins += 1
End If
If team = "Broncos" Then
BroncosWins += 1
End If
If team = "Patriots" Then
PatriotsWins += 1
End If
If team = "Colts" Then
ColtsWins += 1
End If
If team = "Dolphins" Then
DolphinsWins += 1
End If
If team = "Bears" Then
BearsWins += 1
End If
If team = "Jets" Then
JetsWins += 1
End If
If Team = "Chiefs" Then
ChiefsWins += 1
End If
If team = "Buccaneers" Then
BuccWins += 1
End If
Next
Dim Wins() As Integer = {SteelersWins, RaidersWins, PackersWins, CowboysWins, GiantsWins, RamsWins, RavensWins, SaintsWins, FortyNinersWins, RedskinsWins,
BroncosWins, PatriotsWins, ColtsWins, DolphinsWins, BearsWins, JetsWins, ChiefsWins, BuccWins}
For Each win In Wins
Array.Sort(Wins)
Array.Reverse(Wins)
lstOutput.Items.Add(win)
Next
End Sub
End Class
What I have right now is code that reads a text file with the names of Superbowl winners, and counts the number of wins by the number of times the team name appears. These wins are then put into an array and sorted in descending order and displayed in a listbox. This all works fine.
My problem is that I need to display the corresponding team name with their number of wins on the same line in the listbox. So, instead of being:
6
5
5
4
4
It needs to be:
6 Steelers
5 49ers
5 Cowboys
4 Giants
4 Packers
And so on.
There are a couple of issues with your code, not the least of which is you are using hard coded names and variables to collect your team information. This is severely limiting if new names are added or names change.
As Plutonix mentioned in the comments you need to use a class to properly identify and collate your information. I also strongly suggest you use a List(of T) collection to contain your data.
The following code does what you desire.
Private Class cls_Team_wins
Public Team_Name As String
Public Wins As Integer
Public Sub New(Name As String)
Team_Name = Name
Wins = 1
End Sub
Public Overrides Function ToString() As String
Return Strings.Right(" " & Wins, 2) & " - " & Team_Name
End Function
End Class
Private Sub btnCalculate_Click(sender As Object, e As EventArgs) Handles btnCalculate.Click
Dim Games = New List(Of cls_Team_wins)
Dim Teams() As String = IO.File.ReadAllLines("SBWinners.txt")
For Each Team As String In Teams
If Team <> "" Then
Dim Team_Win As cls_Team_wins = Games.Find(Function(x) x.Team_Name = Team)
If Team_Win Is Nothing Then
Games.Add(New cls_Team_wins(Team))
Else
Team_Win.Wins += 1
End If
End If
Next
Games.Sort(Function(x, y) -x.Wins.CompareTo(y.Wins))
ListBox1.DataSource = Games
End Sub
The class included the team name and win counter. It over-rides the ToString method so the list box knows what to show as text. The New function requires you pass the team name and presets the win counter to one.
The For Loop checks for and ignores blank team names, this can be an issue reading text files, esp on the last line. Otherwise it will either add a new team to the list if it does not already exist, or it will increment the win counter if it does.
Once the list is built it is reverse sorted (Note the minus sign in the "Function" compare statement, that's a little trick that reverses the sort), and finally given to the listbox as a datasource.
Good luck to you :)

Sort array numerically, with strings and integers

i have an array which has a name and a score next to it, i need it to be sorted high to low. i already have it sorted alphabetically.
Dim reply3 As String
Dim name As String
Dim score As Integer = 0
Dim classnum As Integer
Dim filePath As String
Dim reply As Integer
Dim reply2 As Integer
Dim strline As String
Dim array() As String
Sub Main()
Console.Title = "Math Test"
Console.WriteLine("Do you want to start the test or view previous results? Press 1 for test, 2 for results")
reply = Console.ReadLine
If reply > 2 Then
MsgBox("Invalid Reply Press Ok to end")
End
End If
If reply = 2 Then
Console.WriteLine("What class do you want to see the results of? 1, 2, or 3?")
reply2 = Console.ReadLine()
End If
If reply2 > 3 Then
MsgBox("Invalid Reply Press Ok to exit")
End
End If
Select Case reply2
Case 1
Dim results1 As String = File.ReadAllText("Z:\scores class 1.txt")
array = Split(results1, "-")
For i As Integer = 0 To array.Length - 1
Console.WriteLine(array(i))
Next
Console.WriteLine("Would you like these to be sorted? Press 1 for yes, 2 for no")
If Console.ReadLine = 1 Then
System.Array.Sort(array)
For i As Integer = 0 To array.Length - 1
Console.WriteLine(array(i))
Next
ElseIf Console.ReadLine = 2 Then
End
End If
Console.ReadLine()
Case 2
Dim results1 As String = File.ReadAllText("Z:\scores class 2.txt")
array = Split(results1, "-")
For i As Integer = 0 To array.Length - 1
Console.WriteLine(array(i))
Next
Console.WriteLine("Would you like these to be sorted? Press 1 for yes, 2 for no")
If Console.ReadLine = 1 Then
System.Array.Sort(array)
For i As Integer = 0 To array.Length - 1
Console.WriteLine(array(i))
Next
ElseIf Console.ReadLine = 2 Then
End
End If
Console.ReadLine()
Case 3
Dim results1 As String = File.ReadAllText("Z:\scores class 3.txt")
array = Split(results1, "-")
For i As Integer = 0 To array.Length - 1
Console.WriteLine(array(i))
Next
Console.WriteLine("Would you like these to be sorted? Press 1 for yes, 2 for no")
If Console.ReadLine = 1 Then
System.Array.Sort(array)
For i As Integer = 0 To array.Length - 1
Console.WriteLine(array(i))
Next
ElseIf Console.ReadLine = 2 Then
End
End If
Console.ReadLine()
End Select
If reply = 1 Then
Console.WriteLine("What is your name?")
name = Console.ReadLine
Console.WriteLine("What class are you in, 1, 2 or 3?")
classnum = Console.ReadLine
If classnum < 1 Then
MsgBox("Invalid Class number")
End
ElseIf classnum > 3 Then
MsgBox("Invalid Class number")
End
End If
Console.WriteLine("What is 9+10 ?")
If Console.ReadLine = 19 Then
score += 1
End If
Console.WriteLine("What is 5x10 ?")
If Console.ReadLine = 50 Then
score += 1
End If
Console.WriteLine("What is 122÷2 ?")
If Console.ReadLine = 61 Then
score += 1
End If
Console.WriteLine("What is 424 + 10 ?")
If Console.ReadLine = 434 Then
score += 1
End If
Console.WriteLine("What is 234 x 3 ?")
If Console.ReadLine = 702 Then
score += 1
End If
Console.WriteLine("What is 10 x 10 ?")
If Console.ReadLine = 100 Then
score += 1
End If
Console.WriteLine("What is 12 x 64 ?")
If Console.ReadLine = 768 Then
score += 1
End If
Console.WriteLine("What is the value of N in this equation? 2n+6=10?")
If Console.ReadLine = 4 Then
score += 1
End If
Console.WriteLine("What is 9 x 73 ?")
If Console.ReadLine = 657 Then
score += 1
End If
Console.WriteLine("What is 1 + 1 ?")
If Console.ReadLine = 2 Then
score += 1
End If
MsgBox("Your score was " & score & " Click ok to finish.")
Dim output1 As String = name & " " & score & "-"
Select Case classnum
Case 1
filePath = System.IO.Path.Combine(
My.Computer.FileSystem.SpecialDirectories.MyDocuments, "scores class 1.txt")
My.Computer.FileSystem.WriteAllText(filePath, output1, True)
Case 2
filePath = System.IO.Path.Combine(
My.Computer.FileSystem.SpecialDirectories.MyDocuments, "scores class 2.txt")
My.Computer.FileSystem.WriteAllText(filePath, output1, True)
Case 3
filePath = System.IO.Path.Combine(
My.Computer.FileSystem.SpecialDirectories.MyDocuments, "scores class 3.txt")
My.Computer.FileSystem.WriteAllText(filePath, output1, True)
End Select
End If
End Sub
I need the array called array to be sorted numerically. i will add the option for the user to choose if he/she wants further sorting after it has been done alphabetically.
Strings are not numbers. They are text.
In string format, they are simply characters (numerals) and they do not and will not sort numerically:
Dim myVals As String() = {"5", "07", "178", "9", "899", "42", "3"}
' add two of the "numbers"
Console.WriteLine("{0} + {1} = {2}", myVals(0),
myVals(1), (myVals(0) + myVals(1)))
Array.Sort(myVals)
Array.Reverse(myVals)
For Each s As String In myVals
Console.Write("{0} ", s)
Next
Output:
5 + 07 = 507
9 899 5 42 3 178 07
The values do not add as expected, because they are not numbers. Instead, the code simply concatenates (glues together) two bits of string. The array contents sort as characters also. When sorting, "9" will always be seen as larger than the others.
This applies to dates stored as strings as well. "9/16/1914" will always evaluate larger/later than "12/1/2015".
Why
Because as text, the lexicographical or alphabetical order used (0,1,2,3,4,5,6,7,8,9).
Stored as string, numbers loose their numeric value (until converted). "9" sorts higher than "899" because "9" is higher than "899". The same way that "Able" and "Baker" will sort based on "A" and "B". The "07" sorts lowest because of the zed. The numerical value is ignored because they are strings (text).
The same is true for string "dates" - they are not dates and have no date value. Your brain interprets them as dates based on the pattern; to the computer, they remain text and "9/16/yyyy" will evaluate larger than "12/1/yyyy".
Sort array numerically, with strings and integers
We now know that there are no integers in string arrays, just numerals. But also, if you have 2 pieces of data, you need to store them separately if you want to use them individually. A Class is ideal for this:
Public Class Student
Public Property Name As String
Public Property Score As Integer
Public Sub New()
End Sub
' overload
Public Sub New(n As String, s As Integer)
Name = n
Score = s
End Sub
Public Overrides Function ToString() As String
Return String.Format("{0} ({1})", Name, Score)
End Function
End Class
Notice that the name and score are stored as the correct data type which will allow code to use the Score as a numeric value. Unlike using 2 arrays, the name cannot become detached from the score.
Note also that ToString() allows us to display the data however we want. Just because we want to show the name and score together does not mean we must glue that information together. Sample output:
Charlie (89)
A Students Container
Give up your arrays (and ArrayLists)! It is the 21st Century and we have Typed collections (called Generics):
Private Students As List(of Student) ' declaration
Students is a collection which can only store Student objects, which in turn contains 2 pieces of data. Let's add some:
Students = New List(Of Student) ' create List instance
Dim stud As New Student ' create new student
stud.Name = "Harvey" ' set props
stud.Score = 72 ' 72 is a NUMBER
Students.Add(stud) ' add to collection
' fast way, using the overload, no temp student object needed
Students.Add(New Student("Bob", 67))
Students.Add(New Student("Hoover", 82))
Students.Add(New Student("Ziggy", 97))
...
Students.Add(New Student("Zoey", 89))
Note that we do not have to set the size of the List(of Student) - it grows as it needs to. You can still reference them by element/position: Students(0) is a Student object, and Students(0).Name will be the name of the student in the first slot ("Harvey").
Sorting
Students = Students.OrderByDescending(Function(x) x.Score).ToList()
This creates a new collection in the desired order. To sort matching scores by name:
Students = Students.OrderByDescending(Function(x) x.Score).
ThenBy(Function(q) q.Name).ToList()
' print the contents:
For Each st As Student In Students
Console.WriteLine(st)
Next
The result after adding several matching scores of 89:
Ziggy (97)
Charlie (89)
Echo (89)
Tango (89)
Zoey (89)
Hoover (82)
Harvey (72)
Bob (67)
To track multiple class scores (hinted at in the code), you could add an Id (1,2,3) or Code ("101", "201", "303") to the class to qualify which class, series or set each User-Score belongs. Use an additional .Where() above to get just a subset of the master list.
Finally, rather than storing score sets to individual files, the entire list in its current state can be serialized and saved in 3 or 4 lines of code.
tl;dr
Strings do not contain numbers, nor Dates. They are text.
Don't store individual data elements as one, if you want to use them individually.
What you have is a essentially a multidimensional array containing multiple data types (but in your case, you have concatenated each row into a single string). There isn't a catch-all way to handle this situation, but here are a few methods to consider:
One multidimensional object array:
Dim array(,) As Object = {{"Student1", 95},{"Student2", 87}...}
Two parallel, one dimensional arrays:
Dim names() As String = {"Student1","Student2"...}
Dim scores() As Integer = {95, 87...}
Create a Class or Struct:
Public Class Student
Property name As String
Property score As Integer
End Class
These are all generally better ways of storing your data, but typically put the burden of sorting your data on you. Which leads me to the final option...
Hack together a workaround:
This is essentially what you've already done- concatenated a string with an integer to allow use of the built-in Sort procedure. You can use the same trick to sort by score, but you have to 0-pad your integers if you want them to sort correctly. See code below, but you've been warned: these type of solutions are invariably easier to implement in the short term but grow unwieldy and less useful in the long term, often needing to be replace by one of the "better" solutions above.
Private Sub FlipStrings(ByRef students() As String)
If students Is Nothing Then Exit Sub
For i As Integer = 0 To UBound(students)
Dim parts() As String = Split(students(i))
If parts Is Nothing OrElse UBound(parts) <> 1 Then Continue For
If IsNumeric(parts(0)) Then
students(i) = parts(1) & " " & CInt(parts(0))
ElseIf IsNumeric(parts(1)) Then
Do Until len(parts(1)) = 3 'Assuming max score of 100'
parts(1) = "0" & parts(1)
Loop
students(i) = parts(1) & " " & parts(0)
End If
Next
End Sub
I wrote this so you can still store and display your array exactly as before. Now, to sort by score
FlipStrings(array)
System.Array.Sort(array)
FlipStrings(array)
should give you exactly what you're looking for.

Grade Array Averager troubles

I am currently in an IT curriculum in college. Advance Visual Basic 2010 is a requirement however, I am not a programmer. I have been struggling to find my way through VB but this last assignment has me stumped.I am able to get the first name into the array and the 5 grades for that name . At that point, the loop will continue to ask for the next name and that names 5 grades and so on until the 4th name and grades are entered and then it should display all 4 names and grade averages in the listbox.
Here is the assignment...
Write a program that will input four students’ names and average five test grades for each student. The program should have an array for the students name and then a two-dimensional array for all their grades.
Your program should ask for the students name and then five test scores for that student.
Create a method that does the averaging and pass the arrays to that method. That method can also output the student name and average in a list box.
Call a method to figure up the average once you get all the grades. Do not figure it up as you get the information!! You’ll get a big ole zero if you do! Then have that same method output the results into the list box:
After 4 days of struggling with this, here is what I have come up with so far. Any guidance is greatly appreciated. Thank you in advance.
Public Class Form1
Private Sub btnNames_Click(sender As System.Object, e As System.EventArgs) Handles btnNames.Click
Dim NamesList(3) As String
Dim GradeArray(4) As Integer
Dim x As Integer
Dim y As Integer
Dim Sum As Integer
Dim Avg As Integer
For y = 0 To NamesList(3)
NamesList(x) = InputBox("Enter student number " & y + 1 & "'s name:", "Enter a name")
Next
For y = 0 To GradeArray.Length - 1
GradeArray(y) = InputBox("Enter grade number " & y + 1 & " for " & NamesList(0) & " in the box:", "Enter the grades")
Next
For Each item In GradeArray
Sum = Sum + item
Next
Avg = Sum / 5
lstAverages.Text = Avg.ToString
End Sub
Private Sub btnExit_Click(sender As System.Object, e As System.EventArgs) Handles btnExit.Click
Me.Close()
End Sub
End Class
I had nothing else better to do, so I took up giving it a try... Also this includes per as you stated: 4 students - 5 grades each, array for students names and a 2D array to hold all their grades. There's a method that passes these to it and performs the averaging of the students grades and then spits them to a listbox as requested ... Happy Coding!
P.S. I didn't do any error handling either, you may want to add that or at least implement something to handle such cases ...
Public Class Form1
Private arrStudents(3) As String 'Student's name array (4)
Private arrScores(3, 4) As Integer 'Students scores (5 each)
'Start getting the data we need
Private Sub btnGetStudents_Click(sender As Object, e As EventArgs) Handles btnGetStudents.Click
Dim strStudent As String = String.Empty
Dim intScore As Integer = 0
Dim intPlace As Integer = 0
'Get the students name...
For i As Integer = 0 To arrStudents.Length - 1
Do Until strStudent <> String.Empty
strStudent = InputBox("Please enter student's name: ", "Gather Student Grades")
Loop
arrStudents(i) = strStudent
'Reset our variable...
strStudent = String.Empty
'Get the students grades...
For s As Integer = 0 To arrScores.Length - 1
Do Until intScore > 0
intScore = CInt(InputBox("Please enter student's scores: ", "Gather Student Scores"))
Loop
If (intPlace = 4 AndAlso i = arrStudents.Length) Then
intPlace = 0
arrScores(i, s) = intScore
intScore = 0
ElseIf intPlace = 4 Then
intPlace = 0
arrScores(i, s) = intScore
intScore = 0
Exit For
Else
arrScores(i, intPlace) = intScore
intPlace += 1
End If
'Reset our variables...
intScore = 0
Next
Next
'Calculate and output the data to the listbox...
GetStudentAverages(arrStudents, arrScores)
End Sub
'Function to average per student grades and then display them all in the listbox...
Private Sub GetStudentAverages(ByVal arStudent As Array, ByVal arScore As Array)
Dim strStudentData As String = String.Empty
Dim intAverage As Integer = 0
Dim intPlace As Integer = 0
'Start averaging the students scores and then add them to the listbox...
For i As Integer = 0 To arStudent.Length - 1
For g As Integer = 0 To arScore.Length - 1
If intPlace = arStudent.Length Then
intAverage += arScore(i, intPlace)
Exit For
Else
intAverage += arScore(i, intPlace)
intPlace += 1
End If
Next
intAverage = CInt(intAverage / 5)
intPlace = 0
'Output the student information...
ListBox1.Items.Add("Student: " & arStudent(i).ToString & " Average: " & intAverage.ToString)
intAverage = 0
Next
End Sub
End Class

summing elements of a .txt file array in VB.net

College student in an advanced VB class who is turning to a forum for help - I've found a few examples of code but am having a hard time figuring this one out.. any and all help is appreciated :)
This application imports a .txt file stored in the bin, debug folder called data.txt
..20 records, 3 lines per record, the last line is the student's grade, I need to average the grades by summing each records grade and dividing by 20 and then displaying on the lstbox showing the average.
So far I've got..
Dim objReader As IO.StreamReader
Dim intFill As Integer
Dim intCount As Integer = 0
Dim intAverage As Integer
Dim strLocationAndNameOfFile As String = "data.text"
If IO.File.Exists(strLocationAndNameOfFile) = True Then
objReader = IO.File.OpenText(strLocationAndNameOfFile)
Else
MsgBox("The file is not available. Restart the program when the file is avilable", , "Error")
Me.Close()
End If
If IO.File.Exists(strLocationAndNameOfFile) Then
objReader = IO.File.OpenText(strLocationAndNameOfFile)
Do While objReader.Peek <> -1
_strName(intCount) = Convert.ToString(objReader.ReadLine())
_strItemID(intCount) = Convert.ToString(objReader.ReadLine())
_intGrade(intCount) = Convert.ToInt32(objReader.ReadLine())
intCount += 1
Loop
objReader.Close()
End If
For intFill = 0 To (_strName.Length - 1)
*'intAverage = SUM OF ALL AVERAGES / LENGTH OF ARRAY -1*
Me.lstAverage.Items.Add(intAverage.ToString())
While looping to read the grades sum them up
Dim total as Integer
Do While objReader.Peek <> -1
_strName(intCount) = Convert.ToString(objReader.ReadLine())
_strItemID(intCount) = Convert.ToString(objReader.ReadLine())
_intGrade(intCount) = Convert.ToInt32(objReader.ReadLine())
total += _intGrade(intCount)
intCount += 1
Loop
And then just divide by 20 or _intGrade.Length
intAverage = total / _intGrade.Length
So many issues, as much as I loathe doing other's homework I want you to see what this could look like
Public Function GetAverageGrade(ByVal filename As String) As Double
Dim totalGrade As Integer = 0
Dim lineCount As Integer = 0
Dim line As String
Using rdr As New IO.StreamReader(filename)
While (line = rdr.ReadLine()) IsNot Nothing
lineCount += 1
If lineCount Mod 3 = 0 Then totalGrade += Convert.ToInt32(line)
End While
End Using
Return totalGrade / (lineCount / 3.0)
End Function
Of course, you probably want to do more with that data than just get the average grade. So an even better option is build code that reads it all in as a set of records:
Public Class GradeItem
Public Property Name As String
Public Property Item As String
Public Property Grade As Integer
End Class
and then
Public Iterator Function ReadGradeItems(ByVal fileName As String) As IEnumerable(Of GradeItem)
Using rdr As New IO.StreamReader(fileName)
While rdr.Peek() <> -1
Yield New GradeItem With {.Name = rdr.ReadLine(), .Item= rdr.ReadLine(), .Grade = Convert.ToInt32(rdr.ReadLine()}
End While
End Using
End Function
and now put it all together:
Dim grades As IEnumerable(Of GradeItem) = ReadGradeItems("data.text")
lstAverage.Items.Add(grades.Select(Function(g) g.Grade).Average())

Resources