Arrays and Structures
An array is a collection of related variables of the same type. All of the variables in the array have the same name, but each is given a unique index number in accordance with its position within the array. These index numbers (called array subscripts) start from zero, so the first item in an array has the index number 0, the second item has the index number 1, and so on.
All of the data items in an array (called the array elements) are stored in contiguous locations in memory, and occupy the same amount of space. In order to find a particular array element, it is only necessary to know the address in memory of the first item in the array and the array subscript of the element being sought. This feature makes it very easy to use a loop construct to navigate through the elements of an array, or to sort the elements into alphabetical or numerical order.
An array variable must be declared just like other variables, but it is not necessary to specify the number of elements. That said, you can declare an array and populate it with data at the same time. The following code declares an array of type String with seven elements, and then populates it:
Dim strWeekDays(6) As String
strWeekDays(0) = "Monday"
strWeekDays(1) = "Tuesday"
strWeekDays(2) = "Wednesday"
strWeekDays(3) = "Thursday"
strWeekDays(4) = "Friday"
strWeekDays(5) = "Saturday"
strWeekDays(6) = "Sunday"
The following statement declares and populates the array at the same time (note that this time there is no number within the parentheses) :
Dim strWeekDays() As String = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
You can also declare an array without specifying the number of elements it will hold:
Dim strMembers() As String
A simple one-dimensional array is essentially a numbered list of variables of the same data type. To demonstrate the principle, we will create a simple program that stores a set of exam scores in an array, and then manipulates the array data in various ways. Proceed as follows:
- Open a new project called "ExamScores" and create an interface like the one illustrated below.
The ExamScore program interface
- Set the control names as shown in the table below.
Control | Name |
---|---|
Form1 | frmExamScores |
Button1 | cmdRawData |
Button2 | cmdSortData |
Button3 | cmdAverage |
Button4 | cmdHighScore |
Button5 | cmdLowScore |
Button6 | cmdExit |
- Set the application's Startup Object property to ExamScores.frmExamScores.
- In the code editor, add the following program statement to the frmExamScores class declaration:
Dim examScores() As Integer = _
{32, 64, 21, 97, 88, 59, 70, 55, 44, 11, 23, 66, 70, 47, 61, 77, 28, 38, 60, 93}
- In the form design window, double-click on the cmdRawData button and add the following code at the cursor:
Dim strOutput As String = ""
For count = 0 To 19
strOutput &= examScores(count) & vbCrLf
Next
MsgBox(strOutput, , "Raw Data")
Your code editor window should now look like the illustration below.
The array declaration and cmdRawData Click event procedure
- Run the program and click on the Display raw data button. The message box that appears should look something like the following:
The message box output for the Display raw data button
The initial Dim statement assigns twenty exam scores (each representing marks out of 100) to the integer array examScores(). The For . . . Next loop accesses each array element in turn, and appends each value, plus a newline character, to the string variable strOutput. The message box then displays the string.
Simply displaying the array values is relatively simple, as you can see. The other functions we want to implement include displaying the average, highest and lowest scores, which involves slightly more effort.
The most difficult thing we will attempt is to sort the scores and display them in order of magnitude. At one time this would have involved writing something like a bubble sort. Thanks to the facilities available for handling arrays in Visual Basic however, the task is now much easier.
- In the form design window, double-click on the cmdSortData button and add the following code at the cursor:
Dim strOutput As String = ""
Array.Sort(examScores)
For count = 0 To 19
strOutput &= examScores (count) & vbCrLf
Next
MsgBox(strOutput, , "Sorted Data")
The code for the cmdSortData button is almost identical to the code for the cmdRawData button, except for the command Array.Sort(examScores). This line of code calls on the Array.Sort() method provided for arrays in Visual Basic to sort the array values by ascending numerical order. The method is limited to sorting one-dimensional arrays, but if that is all you need to do it is quite useful.
- Run the program and click on the Display sorted data button. The message box that appears should look something like the following:
The message box output for the Display sorted data button
Note that the task of programming the loops is made somewhat easier because we know that there are 20 array elements. It is possible (probable, in fact) that we will not always know in advance how big the array is. Fortunately, Visual Basic provides methods for arrays that allows us to find the size of the array (GetLength()), and the upper and lower bounds of the array (GetLowerBound() and GetUpperBound()).
The length of the array is the number of items it contains. The lower bound is the lowest index number (or array subscript) used by the array (normally 0) while the upper bound is the highest index number (normally one less than the number of items in the array). As an example of how this could help us, if we did not know the length of the examScores() array we could replace the first line in each of our loops:
For count = 0 To 19
with:
For count = examScores.GetLowerBound(0) To examScores.GetUpperBound(0)
Note also that the Array.sort() function changes the order of the array variables within the array, so if you need to preserve the original ordering of the array elements, you may need to include some intermediate code to save the state of the original array prior to sorting.
One option is to create a copy of the array and perform a sort on the copy, thus preserving the order of the original. Visual Basic even provides a Copy method for arrays that enables us to do this. The code for the cmdSortData button's Click event could be amended to read as follows:
Dim strOutput As String = ""
Dim examScoresNew (examScores.Length - 1) As Integer
Array.Copy (examScores, examScoresNew, examScores.Length)
Array.Sort (examScoresNew)
For count = examScoresNew.GetLowerBound(0) To examScoresNew.GetUpperBound(0)
strOutput &= examScoresNew(count) & vbCrLf
Next
MsgBox(strOutput)
In the bubble sort algorithm referred to above, the program loops through the array one element at a time, and compares each item with the next item in the array. If they are in the wrong order with respect to each other, they must swap places. After the first pass through the loop, the last item in the array is in its correct position.
Since the remaining items may still require some adjustment the process is repeated, but (if you write the bubble sort code efficiently) only for the remaining items. After the second pass through the loop, the second to last item in the array is now also in its correct position. For each repetition of the procedure, the number of array elements that must be traversed is reduced by one, until the sort is complete.
The algorithm effectively relies on a nested loop construct and the availability of an additional variable in which to temporarily store one of the two values being swapped (if I were going to swap the contents of two jars, I would need a third jar in which to temporarily store the contents of one of the first jar so that I could transfer the contents of the second jar to it).
The mechanism may become a little clearer once you have examined the code. We could re-write the code for the cmdSortData button's Click event handler to use the bubble sort method as follows:
Dim strOutput As String = ""
Dim examScoresNew(examScores.Length - 1) As Integer
Dim intSwap As Integer
Array.Copy(examScores, examScoresNew, examScores.Length)
For i = examScoresNew.GetLowerBound(0) To examScoresNew.GetUpperBound(0) - 1
For j = examScoresNew.GetLowerBound(0) To (examScoresNew.GetUpperBound(0) - 1 - i)
If examScoresNew(j) > examScoresNew(j + 1) Then
intSwap = examScoresNew(j)
examScoresNew(j) = examScoresNew(j + 1)
examScoresNew(j + 1) = intSwap
End If
Next
Next
For count = examScoresNew.GetLowerBound(0) To examScoresNew.GetUpperBound(0)
strOutput &= examScoresNew(count) & vbCrLf
Next
MsgBox(strOutput)
The bubble sort algorithm works for character data and strings, as well as for numbers, sorting the array elements alphabetically rather than by numerical ascendancy. Bear in mind, however, that the sort uses ASCII character codes to sort the data, so an upper-case character will always appear ahead of a lower case character regardless of their respective position in the alphabet. Sorting string values therefore normally requires that the string values are first converted to either all upper-case or all lower-case strings in order for a meaningful sort to be achieved.
Note that the above code can probably be "tweaked" to make it more efficient. It can also be relatively easily changed in order to perform a sort in the reverse direction (i.e. in descending rather than ascending numerical order).
The code for the remaining buttons (cmdAverage, cmdHighScore, cmdLowScore and cmdExit) is given below in its entirety. The code itself is sufficiently trivial that a lengthy explanation of how it works should be unnecessary. Suffice it to say that the code further demonstrates that an array is a useful structure that can be manipulated relatively easily using a For . . . Next loop.
Private Sub cmdAverage_Click(sender As Object, e As EventArgs) Handles cmdAverage.Click
Dim intTotal As Integer = 0
For count = 0 To 19
intTotal += examScores(count)
Next
MsgBox("Average score: " & CSng(intTotal / 20), , "Average Score")
End Sub
Private Sub cmdHighScore_Click(sender As Object, e As EventArgs) _
Handles cmdHighScore.Click
Dim intHighScore As Integer = 0
For count = 0 To 19
If intHighScore < examScores(count) Then
intHighScore = examScores(count)
End If
Next
MsgBox("High score: " & intHighScore, , "High Score")
End Sub
Private Sub cmdLowScore_Click(sender As Object, e As EventArgs) _
Handles cmdLowScore.Click
Dim intLowScore As Integer = 100
For count = 0 To 19
If intLowScore > examScores(count) Then
intLowScore = examScores(count)
End If
Next
MsgBox ("Low score: " & intLowScore, , "Low Score")
End Sub
Private Sub cmdExit_Click (sender As Object, e As EventArgs) Handles cmdExit.Click
End
End Sub
Working with files
In real life, we are faced with problems that are somewhat more complex than the simple example described above. For a start, the data we need to process will probably be stored in a file, and we may not know how many records are in the file before we open it.
If we are going to stick with the example of a set of exam results, we should consider that these results will never exist in isolation. Each result will be associated with a student, who is probably identified in our file system by a student ID number. Indeed, it is likely that during a term or semester each student will sit a number of exams, and the scores for each exam will be recorded in the student's record for that term or semester.
Let us therefore consider a scenario in which twenty students have just completed the first semester on an Honours degree, and have taken a separate exam for each of six modules. The results are stored in a text-based data file (exam_results.dat) that stores the student ID, name, and exam results for each student in a record consisting of eight comma-delimited fields (an eight-digit student ID number, six exam results, and a string containing the student's last name and first initial). We will create a program that reads the file and makes the following data items available:
- the number of students taking each exam
- the average score for each exam
- the median for each exam
- the standard deviation for each exam
- the highest score for each exam, and the student who achieved it
- the average exam score for each student
- the projected Honours degree level for each student (e.g. 1, 2.1, 2.2 etc.)
The source data file is sorted by student ID number, but the application must be able to create a new data file containing the student ID and name, together with the student's average exam result and projected Honours degree level. The target file should be sorted by student last name and first initial.
- Open a new project called "StudentRecords" and create an interface like the one illustrated below.
The StudentRecords program interface
- Set the control names as shown in the table below.
Control | Name |
---|---|
Form1 | frmStudentRecords |
Label1 | lblTitle |
RichTextBox1 | rtbOutput |
Button1 | cmdLoad |
Button2 | cmdStats |
Button3 | cmdPerformance |
Button4 | cmdCreateFile |
Button5 | cmdExit |
Label2 | lblNumStudents |
TextBox1 | txtNumStudents |
- Set the application's Startup Object property to StudentRecords.frmStudentRecords.
- Save the project to create the project folder.
- Download the file exam_results.zip from the link below, and unzip the contents into the \bin\Debug\net6.0-windows or \bin\Debug\net7.0-windows sub-directory of your project folder, depending on which framework you chose for the project. The .zip file contains the exam_results.dat file.
Download the file exam_results.zip
- In the form design window, right-click anywhere in the form and select View Code from the pop-up menu.
- Add the following lines of code to the form's class definition (Note: these statements must appear before any event handlers or sub-routines):
Dim strStudent(,) As String
Dim intResults(,) As Integer
Dim strRecord() As String
Dim strTemp() As String
Dim intRecTotal As Integer
Dim dblAverages(5) As Double
Dim dblMedians(5) As Double
Dim dblStdDevs(5) As Double
Dim intHiScores(5) As Integer
Dim strHiScoreStudent(5) As String
Dim dblStudentAverage() As Double
Dim strStudentLevel() As String
Dim strNamesSorted() As String
The first two statements declare empty two-dimensional arrays (strStudent(,) and intResults(,)) to hold the data we want to extract from the input file, in the format in which we need it. Whereas a one-dimensional array is like an ordered list of items of the same data type, a two-dimensional array is more like a table (i.e. you can store data in rows and columns).
For this application we need two arrays, one to hold the student ID number and student name (which are stored as strings), and one to hold the six examination results for each student (which we need to store as integers in order to perform calculations on them). Two-dimensional arrays, like one-dimensional arrays, can only store data items with the same data type. The arrays have not been dimensioned, since we must assume that the number of records in the input file could vary.
The un-dimensioned string arrays strRecord() and strTemp() are declared next, and are used during the file input stage of the program. The strRecord() array will hold each comma-delimited string read in from the input file, and will be dynamically re-dimensioned during this process to accommodate the correct number of records. The strTemp() array will be used to hold the contents of each comma-delimited input string in turn as an array.
The next line declares the integer variable intRecTotal which will store the number of records read in from the file. It will subsequently be used in various processing loops to set the upper array bound.
The next three statements create floating point array variables (dblAverages(), dblMedians() and dblStdDevs()) that will store the results of the average, median and standard deviation calculations for each of the six modules.
The next two lines create array variables to hold information about the highest scores for each module. The integer array intHighScores() will hold the highest marks for each exam, and the string variable strHighScoreStudents will hold the names of the students achieving those scores.
The next two lines declare variables to store data about each student's progress to date. The floating point array variable dblStudentAverage() holds the average exam score for each student, while the string array variable strStudentLevel() will hold information about the degree level currently attained (e.g. "First", "Upper Second" and so on).
The last declaration creates a string array called strNamesSorted(), which will hold the names of the students sorted into alphabetical order. This array will be used for comparison purposes when sorting the student data by name to be written to the output file.
- Set the Font property of the RichTextBox control to "Courier New, 8.25pt"
- Set the Enabled property of the cmdStats, cmdPerformance, and cmdCreateFile control to "False"
- In the form design window, double-click on the cmdLoad button and add the following code at the cursor:
Dim srdFile As IO.StreamReader
intRecTotal = -1
lblTitle.Text = "Student Input Data"
rtbOutput.Clear()
rtbOutput.AppendText _
("StudentID Mod1 Mod2 Mod3 Mod4 Mod5 Mod6 Candidate" & vbCrLf)
rtbOutput.AppendText _
("--------- ---- ---- ---- ---- ---- ---- ---------" & vbCrLf)
srdFile = New IO.StreamReader("exam_results.dat")
Do Until srdFile.Peek = -1
intRecTotal += 1
ReDim Preserve strRecord(intRecTotal)
strRecord(intRecTotal) = srdFile.ReadLine()
Loop
srdFile.Close()
ReDim strStudent(intRecTotal, 1)
ReDim intResults(intRecTotal, 5)
For i = 0 To intRecTotal
strTemp = strRecord(i).Split(",")
strStudent(i, 0) = strTemp(0)
strStudent(i, 1) = strTemp(7)
For j = 0 To 5
intResults(i, j) = strTemp(j + 1)
Next
rtbOutput.AppendText(strStudent(i, 0).PadRight(9))
For j = 0 To 5
rtbOutput.AppendText(Convert.ToString(intResults(i, j)).PadLeft(6))
Next
rtbOutput.AppendText(" " & strStudent(i, 1).PadRight(9) & vbCrLf)
Next
txtNumStudents.Text = intRecTotal + 1
cmdLoad.Enabled = False
cmdStats.Enabled = True
cmdPerformance.Enabled = True
cmdCreateFile.Enabled = True
This is probably the longest single block of code in the whole program. The first line, as we have seen previously, creates a StreamReader object called srdFile. The next line sets the value of incrRecTotal to 0. We will use this variable to keep track of the number of records read in from the exam_results.dat file, and it will also serve as the upper array bound for various arrays.
The third line of code sets the text displayed by the label at the top of the form to "Student Input Data" to inform the user what the output they are looking at represents, and the next line clears the RichTextBox in readiness for the student record data to be displayed.
The next two lines set up the column headings for the student record data.
The next line uses the New keyword to allocate memory to the StreamReader variable srdFile, and associates it with the exam_results.dat file. It also opens the file for input. The Do . . . Until loop increments intRecTotal, re-dimensions the strRecord() array variable to accommodate the next record to be read, and reads the next record from the file into the array using the StreamReader object's ReadLine() method. It carries on doing this until the StreamReader object's Peek method detects the end of the file.
Once the program exits the Do . . . Until loop, the input stream is closed and the two-dimensional arrays strStudent() and intResults() are re-dimensioned to the required size (this is determined using the final value of incrRecTotal).
The main For . . . Next loop splits each of the comma-delimited record strings into its eight constituent parts (the student's ID, the six examination marks for each student, and the student's name). The loop then inserts these values into the strStudent() array (the student ID and student name) and the intResults() array (the six exam results). The last part of the loop outputs the formatted data to the RichTextBox, making use of the PadLeft() and PadRight() methods to vertically align the data within each column.
One point of interest here is the use of two nested For . . . Next loops within the main For . . . Next loop - one to allocate the exam six results for each record to the intResults() array, and one to output them on each line of output in the RichTextBox.
The line immediately following the main For . . . Next loop displays the number of students for whom records are stored in the input data file by setting the text property of the text box txtNumStudents to the value of incrRecTotal (the number of student records read).
The last four lines of code are simply concerned with disabling the cmdLoad button (since the button's event handler code only needs to be executed once), and enabling the buttons that create the program's output (these were initially disabled because they cannot produce any output until the source data has been loaded).
At this stage you may be wondering why we didn't just place the above code into the form's Load event, since this would save the user the bother of having to click on the cmdLoad button at all. The reason is that we want to make some enhancements to the interface later on that will enable the user to open any file that contains student exam records in the same format.
To see what we have achieved so far, run the program and click on the Load file button to display the data. Your output should look something like the screenshot below.
The formatted student exam results data
The Exam statistics button will display all of the required statistical information for the six module examinations taken by the students included in the input file. This information includes the average examination mark, the median and standard deviation, and the highest score (and by whom it was achieved).
We could of course write all the code to extract and display this information in the button's event handler, but it would become somewhat unwieldy and difficult to maintain if we did that. We might also consider the possibility that some of the functionality involved might be usefully encapsulated in separate procedures so that it can be re-used elsewhere. With this in mind therefore, the task of calculating the statistics will be carried out using four separate procedures, namely getAverages(), getMedians(), getStdDevs() and getHiScores().
The code for these procedures is given below, and should be entered as shown somewhere within the form's class definition.
Sub getAverages():
Sub getAverages()
Dim intSum As Integer
dblAverages = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
intSum = 0
For j = 0 To intRecTotal
intSum += intResults(j, i)
Next
dblAverages(i) = intSum / (intRecTotal + 1)
Next
End Sub
The average mark for each examination is calculated by adding together all the individual student marks for the examination and dividing the result by the number of students taking part. The getAverages() subroutine declares a local integer variable intSum to hold the result of adding together the results for each examination, and initialises the dblAverages() array elements to zero. It then uses a For . . . Next loop for each module examination in which intsum is initialised to zero at the beginning of the loop.
A second (nested) For . . . Next loop adds the result for each student to the total (intSum). The last line of code in the main loop divides intSum by the number of records (intRecTotal) to get the average mark for each examination and assigns the result to the dblAverages() array.
Sub getMedians():
Sub getMedians()
Dim intScores(intRecTotal) As Integer
dblMedians = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
For j = 0 To intRecTotal
intScores(j) = intResults(j, i)
Next
Array.Sort(intScores)
If intRecTotal Mod 2 = 1 Then
dblMedians(i) = _
(intScores((intRecTotal / 2) + 0.5) + intScores((intRecTotal / 2) - 0.5)) / 2
Else
dblMedians(i) = intScores((intRecTotal / 2))
End If
Next
End Sub
The median of a range of values is the value that occurs at the mid-point of the range. In other words, if there are n values higher than the median, there will be also be n values lower than the median. Obviously if the total number of values is even, there is no single value at the mid-point. If this is the case, the average of the two numbers on either side of the mid-point is used.
The getMedians() subroutine first declares an integer array variable to hold the marks for a single examination. It then initialises all of the array elements of dblMedians() to zero. The rest of the subroutine is taken up by a For . . . Next loop that iterates six times (once for each set of examination marks). A For . . . Next loop nested within the main loop reads the marks for an examination into the intScores() array. The Array.Sort() method is then called to sort the intScores() array elements into numerical order.
The If . . . Else conditional statement checks to see whether the number of values is odd or even. If the number is odd, the median value is that of the array element whose subscript is equal to intRecTotal divided by two. If the number is even, the median value is the average of the two array elements whose subscripts are equal to intRecTotal divided by two minus 1, and intRecTotal divided by two.
If you are not sure how this works, assume there are twenty values, in which case intRecTotal will be 20. We therefore want the average of the tenth and eleventh values to get the median, so we need to add together the values with the subscripts 9 and 10, and divide the result by two. Dividing twenty (the value of intRecTotal) by two gives us the higher subscript (10). Subtracting one from this value gives us the lower subscript (9).
Once the median for each set of examination marks has been determined, it is assigned to the dblMedians() array.
Sub getStdDevs():
Sub getStdDevs()
Dim dblSumDiffsSquared As Double
dblStdDevs = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
dblSumDiffsSquared = 0
For j = 0 To intRecTotal
dblSumDiffsSquared += (intResults(j, i) - dblAverages(i)) ^ 2
Next
dblStdDevs(i) = Math.Sqrt(dblSumDiffsSquared / intRecTotal)
Next
End Sub
The standard deviation of a range of values is a measure of the distribution of a dataset around the mean value. Essentially, it is a measure of how spread-out the values are. The lower the standard deviation, the less spread-out they are. To find the standard deviation, we first need to find the sum of the squares of the differences between each value in the dataset and the mean value. We then divide this sum by the number of results in the dataset. Finally, we find the square root of the result to arrive at the standard deviation. The mathematical formula for standard deviation is shown below.
σ = √ | Σ(x‾- x) 2 |
n - 1 |
The code for the subroutine declares a floating point variable (dblSumDiffsSquared) to hold the sum of the squared differences, and initialises all of the array elements of dblStdDevs() to zero. The subroutine's main For . . . Next loop gets the standard deviation values for each examination and stores them in the dblStdDevs() array.
The inner For . . . Next loop calculates the difference between each examination mark and the average, squares it, and adds it to dblSumDiffsSquared. The last line of the inner loop uses Visual Basic's Math.Sqrt() function to get the square root of dblSumDiffsSquared divided by intRecTotal (the intRecTotal variable is, by definition, n-1).
Sub getHiScores():
Sub getHiScores()
Dim intRecIndex (5) As Integer
intHiScores = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
For j = 0 To intRecTotal
If intResults(j, i) > intHiScores(i) Then
intHiScores(i) = intResults(j, i)
intRecIndex(i) = j
strHiScoreStudent(i) = strStudent(intRecIndex(i), 1)
End If
Next
Next
For i = 0 To 5
For j = 0 To intRecTotal
If intResults(j, i) = intHiScores(i) And j <> intRecIndex(i) Then
strHiScoreStudent(i) &= ", " & strStudent (j, 1)
End If
Next
Next
End Sub
The getHighScores() subroutine creates an integer array (intRecIndex()) to hold the array subscripts for the first occurrence of the highest mark for each examination. There exists the possibility that two students, or more than two students, will jointly achieve the highest mark. For this reason the subroutine uses two For . . . Next loops, one after the other. Both of these loops contain nested For . . . Next loops.
The first outer loop finds the highest score for each of the six examinations, and the name of the first student in the list with which this score is associated. The inner loop iterates through the scores for each exam to establish the highest score, stores the result in the intHiScores() array, and stores the name of the first student found to have achieved this score in the strHiScoreStudent() array.
The second loop iterates through the results for each exam once more, this time for the purpose of finding any other students that have achieved the same score and appending their names to the existing elements of the strHiScoresStudents() array. Now that the required subroutines have been created, we are ready to write the code for the cmdStats button.
- In the form design window, double-click on the cmdStats button and add the following code at the cursor:
lblTitle.Text = "Examination Statistics"
rtbOutput.Clear()
rtbOutput.AppendText _
("Module No Average Median Std Dev Hi Score Candidate (s) " & vbCrLf)
rtbOutput.AppendText _
("--------- ------- ------ ------- -------- ------------" & vbCrLf)
getAverages()
getMedians()
getStdDevs()
getHiScores()
For i = 0 To 5
rtbOutput.AppendText("Module " & i + 1)
rtbOutput.AppendText _
(Convert.ToString(Format (dblAverages(i), "Fixed")).PadLeft(9))
rtbOutput.AppendText _
(Convert.ToString(Format (dblMedians(i), "Fixed")).PadLeft(7))
rtbOutput.AppendText _
(Convert.ToString(Format (dblstdDevs(i), "Fixed")).PadLeft(8))
rtbOutput.AppendText _
(Convert.ToString(intHiScores(i)).PadLeft(8))
rtbOutput.AppendText _
(" " & strHiScoreStudent(i) & vbCrLf)
Next
The event handler for the cmdStats button first sets the text displayed by the label at the top of the form to "Examination Statistics" to inform the user what the output they are looking at represents, then clears the RichTextBox and sets up the column headers for the statistical output. It then calls the four subroutines getAverages(), getMedians(), getStdDevs() and getHiScores(). These subroutines essentially do all the hard work (the number-crunching, if you like). The final part of the event handler code is a For . . . Next loop that outputs the formatted data to the RichTextBox. Once you have entered the code, run the program, click on the Load file button, then click on the Exam Statistics button to display the data. Your output should look something like the screenshot below.
The formatted examination statistics data
The cmdPerformance button will display the average score and current level of achievement for each student. Once again, we are creating separate subroutines to do the number crunching, the code for which is shown below. Enter the code as shown somewhere within the form's class definition.
Sub getStudentAverages():
Sub getStudentAverages()
Dim scoreTotal As Integer
ReDim dblStudentAverage(intRecTotal)
For i = 0 To intRecTotal
scoreTotal = 0
For j = 0 To 5
scoreTotal += intResults(i, j)
Next
dblStudentAverage(i) = scoreTotal / 6
Next
End Sub
The getStudentAverages() subroutine declares a local integer variable (scoreTotal) to hold the sum of each student's six exam scores. The second line of code re-dimensions the dblStudentAverage() array. The rest of the code employs a For . . . Next loop and nested For . . . Next loop to iterate through each student's exam scores and calculate the averages.
Sub getStudentLevels():
Sub getStudentLevels()
ReDim strStudentLevel(intRecTotal)
For i = 0 To intRecTotal
If dblStudentAverage(i) >= 70 Then
strStudentLevel(i) = "First"
ElseIf dblStudentAverage(i) >= 60 Then
strStudentLevel (i) = "Upper Second"
ElseIf dblStudentAverage(i) >= 50 Then
strStudentLevel(i) = "Lower Second"
ElseIf dblStudentAverage(i) >= 40 Then
strStudentLevel(i) = "Third"
ElseIf dblStudentAverage(i) >= 35 Then
strStudentLevel(i) = "Ordinary Pass"
Else
strStudentLevel(i) = "Fail"
End If
Next
End Sub
The getStudentLevel() subroutine starts by re-dimensioning the strStudentLevel() array. It then uses a For . . . Next loop to examine the average score for each student. A series of If . . . Elseif statements is used to determine what degree level the student has currently achieved. The guidelines used by universities in the UK to determine what level of degree should be awarded are as follows:
- First: >= 70%
- Upper Second: >=60% to <70%
- Lower Second: >=50% to <60%
- Third: >=40% to <50%
- Ordinary Pass: >=35% to <40%
- In the form design window, double-click on the cmdPerformance button and add the following code at the cursor:
lblTitle.Text = "Student Performance"
rtbOutput.Clear()
rtbOutput.AppendText("StudentID".PadRight(12) & "Student Name".PadRight(15))
rtbOutput.AppendText("Average Mark".PadRight(15) & "Level Achieved" & vbCrLf)
rtbOutput.AppendText("---------".PadRight(12) & "------------".PadRight(15))
rtbOutput.AppendText("------------".PadRight(15) & "--------------" & vbCrLf)
getStudentAverages()
getStudentLevels()
For i = 0 To intRecTotal
rtbOutput.AppendText(strStudent (i, 0).PadRight(12) & strStudent(i, 1).PadRight(15))
rtbOutput.AppendText(Convert.ToString(Format(dblStudentAverage(i), "Fixed")).PadLeft(12))
rtbOutput.AppendText(" " & strStudentLevel(i))
rtbOutput.AppendText(vbCrLf)
Next
The cmdPerformance button's event handler code first sets the text displayed by the label at the top of the form to "Student Performance" to inform the user what the output they are looking at represents, clears the RichTextBox and sets up the column headers for the student performance data, and calls the subroutines getStudentAverages() and getStudentLevels().
The last part of the code is a For . . . Next loop that simply outputs the formatted data for each student. Once you have entered the code, run the program, click on the Load file button, then click on the Student Performance button to display the output, which should look something like the screenshot below.
The formatted student performance data
The last major task is to write the code for the cmdCreateFile button, which saves the student performance data to an output file. Once again we have a subroutine to write before we can encode the button's event handler, this time to create an ordered array of student names in order to be able to output the performance data to the file in alphabetical order by student name and initial. The code for the subroutine is shown below. Enter the code as shown somewhere within the form's class definition.
Sub sortNames()
ReDim strNamesSorted(intRecTotal)
For i = 0 To intRecTotal
strNamesSorted(i) = LCase(strStudent(i, 1))
Next
Array.Sort(strNamesSorted)
End Sub
The subroutine starts by re-dimensioning the strNamesSorted array(). It then uses a For . . . Next loop to copy the student names from the strStudent() array into the strNamesSorted() array. Note however that in the process of copying, the names are converted to lower-case. The last line of the subroutine uses the Array.Sort() method to sort the strNamesSorted() array alphabetically.
- In the form design window, double-click on the cmdCreateFile button and add the following code at the cursor:
Dim swrFile As IO.StreamWriter
Dim strOutput As String
swrFile = IO.File.CreateText("student_progress.dat")
sortNames()
getStudentAverages()
getStudentLevels()
For i = 0 To intRecTotal
For j = 0 To intRecTotal
strOutput = ""
If LCase (strStudent(j, 1)) = strNamesSorted(i) Then
strOutput &= strStudent(j, 0) & ","
strOutput &= strStudent(j, 1) & ","
strOutput &= Convert.ToString(Format(dblStudentAverage(j), "Fixed")) & ","
strOutput &= strStudentLevel(j)
swrFile.WriteLine(strOutput)
End If
Next
Next
swrFile.Close()
MsgBox("Output file created.", , "Student Records")
The first line of the code in this event handler creates a variable of type StreamWriter (swrFile). The second line creates a local string variable called strOutput that we will use to build the output for each line to be written to the output file. The third line creates the student_progress.dat file (or overwrites the file with a new, empty version if it already exists), opens the file for output, and associates the StreamWriter variable swrFile with it.
The event handler then calls the sortNames(), getStudentAverages() and getStudentLevels() subroutines. The main For . . . Next loop employs a nested For . . . Next loop to make one complete cycle through the student performance data for each student name in the strNamesSorted() array. Each time a match is found the student data for that student name is appended to a comma-delimited output string and written to the output file.
The last two lines of code close the file and display a message box to tell the user that the file was created (note that there is no error handling included at the moment). By default, the file will be created in the \bin\Debug sub-directory of your project folder. Once you have entered the code, run the program, click on the Load file button, then click on the Create student file button to display the data. Your output should look something like the screenshot below (note that this event handler does not write to the RichTextBox).
The Create student file button displays a message when the file has been created
Using Structures
The StudentRecords program is now fully functional and has been implemented using one-dimensional and two-dimensional arrays. This works quite well, because arrays lend themselves to the kind of list processing we have essentially been engaged in. The code is however somewhat unwieldy at times, and not particularly efficient. Because an array can only hold variables of a single data type, for example, we cannot put all the student data into a single array but must instead split it between a multi-dimensional string array (for the student ID and student name) and a multi-dimensional integer array (to hold the examination scores).
The problem becomes exacerbated when we are dealing with even more different types of variables. A more detailed student data file might require string variables to hold name and address information, integer variables to hold examination and coursework scores, floating point variables to hold statistical information, date and time variables to store information such as the student's date of birth, Boolean variables to indicate whether or not a student has completed a particular module, and so on.
The structure is a user-defined data type that can store information consisting of multiple terms with various data types, including other structures. This means we could store the different kinds of student data handled by our StudentRecords program in a single structure variable. Better still, because structures are data types in their own right, it is even possible to put them into an array. In fact we could have written our StudentRecords program using a single array of structures rather than having to use many different arrays.
Like other types of variable, a structure variable must be declared before it can be used. The general form of a structure declaration is shown below.
Scope Structure structureName
Dim memberName As Type
. . .
End Structure
Scope is simply the scope of the structure, either Public or Private. The Structure keyword is followed by structureName, which is the name by which the user-defined data type will be known. Within the body of the structure declaration, the individual structure members (or member variables) are declared using a series of Dim statements. The statement End Structure marks the end of the structure declaration.
For the StudentRecords program, we could use the following structure to store the relevant data items:
Public Structure structStudentRecord
Dim strID As String
Dim strName As String
Dim strResults() As Integer
End Structure
Note that the examination results for a student are still stored as an array, but the array itself is now one of the member variables of structStudentRecord. We present below the revised code for the StudentRecords program, this time using a structure variable to hold the input file data, and creating an array of structures to enable us to take advantage of the list processing features of arrays.
You will find that the code is not significantly reduced in volume, but it may be slightly easier to follow. Note that in order to access a structure variable member, the name of the structure is used together with the name of the member variable. The two names are separated by a dot, as in the following examples:
structRecords(4).strName
structRecords(4).intResults(0)
In the first example, the variable being referenced is the strName member variable of the fifth element of the array of structures, structRecords(). In the second example the same structure variable is being referenced, but this time the data item of interest is the first element in the intResults() array member variable.
Here is the revised code for the StudentRecords program in its entirety:
Public Class frmStudentRecords
Public Structure structStudentRecord
Dim strID As String
Dim strName As String
Dim intResults() As Integer
End Structure
Dim structRecords() As structStudentRecord
Dim intRecTotal As Integer
Dim dblAverages(5) As Double
Dim dblMedians (5) As Double
Dim dblstdDevs(5) As Double
Dim intHiScores(5) As Integer
Dim strHiScoreStudent(5) As String
Dim dblStudentAverage() As Double
Dim strStudentLevel() As String
Dim strNamesSorted() As String
Sub getAverages()
Dim intSum As Integer
dblAverages = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
intSum = 0
For j = 0 To intRecTotal
intSum += structRecords(j) .intResults (i)
Next
dblAverages(i) = intSum / (intRecTotal + 1)
Next
End Sub
Sub getMedians()
Dim intScores(intRecTotal) As Integer
dblMedians = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
For j = 0 To intRecTotal
intScores(j) = structRecords(j).intResults(i)
Next
Array.Sort(intScores)
If intRecTotal Mod 2 = 1 Then
dblMedians(i) = (intScores((intRecTotal / 2) + 0.5) + intScores((intRecTotal / 2) - 0.5)) / 2
Else
dblMedians(i) = intScores((intRecTotal / 2))
End If
Next
End Sub
Sub getStdDevs()
Dim dblSumDiffsSquared As Double
dblstdDevs = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
dblSumDiffsSquared = 0
For j = 0 To intRecTotal
dblSumDiffsSquared += (structRecords(j).intResults(i) - dblAverages(i)) ^ 2
Next
dblstdDevs(i) = Math.Sqrt(dblSumDiffsSquared / intRecTotal)
Next
End Sub
Sub getHiScores()
Dim intRecIndex(5) As Integer
intHiScores = {0, 0, 0, 0, 0, 0}
For i = 0 To 5
For j = 0 To intRecTotal
If structRecords(j).intResults(i) > intHiScores(i) Then
intHiScores(i) = structRecords (j).intResults(i)
intRecIndex(i) = j
strHiScoreStudent(i) = structRecords(j).strName
End If
Next
Next
For i = 0 To 5
For j = 0 To intRecTotal
If structRecords(j).intResults(i) = intHiScores(i) And j <> intRecIndex(i) Then
strHiScoreStudent(i) &= ", " & structRecords(j).strName
End If
Next
Next
End Sub
Sub getStudentAverages()
Dim scoreTotal As Integer
ReDim dblStudentAverage(intRecTotal)
For i = 0 To intRecTotal
scoreTotal = 0
For j = 0 To 5
scoreTotal += structRecords(i).intResults(j)
Next
dblStudentAverage(i) = scoreTotal / 6
Next
End Sub
Sub getStudentLevels()
ReDim strStudentLevel(intRecTotal)
For i = 0 To intRecTotal
If dblStudentAverage(i) >= 70 Then
strStudentLevel(i) = "First"
ElseIf dblStudentAverage(i) >= 60 Then
strStudentLevel(i) = "Upper Second"
ElseIf dblStudentAverage(i) >= 50 Then
strStudentLevel(i) = "Lower Second"
ElseIf dblStudentAverage(i) >= 40 Then
strStudentLevel(i) = "Third"
ElseIf dblStudentAverage(i) >= 35 Then
strStudentLevel(i) = "Ordinary Pass"
Else
strStudentLevel(i) = "Fail"
End If
Next
End Sub
Sub sortNames()
ReDim strNamesSorted(intRecTotal)
For i = 0 To intRecTotal
strNamesSorted(i) = LCase(structRecords(i).strName)
Next
Array.Sort(strNamesSorted)
End Sub
Private Sub cmdLoad_Click(sender As Object, e As EventArgs) Handles cmdLoad.Click
Dim srdFile As IO.StreamReader
Dim strArrayTemp() As String
intRecTotal = -1
lblTitle.Text = "Student Input Data"
rtbOutput.Clear()
rtbOutput.AppendText _
("StudentID Mod1 Mod2 Mod3 Mod4 Mod5 Mod6 Candidate" & vbCrLf)
rtbOutput.AppendText _
("--------- ---- ---- ---- ---- ---- ---- ---------" & vbCrLf)
srdFile = New IO.StreamReader("exam_results.dat")
Do Until srdFile.Peek = -1
intRecTotal += 1
ReDim Preserve structRecords(intRecTotal)
ReDim structRecords(intRecTotal).intResults(5)
strArrayTemp = srdFile.ReadLine().Split(",")
structRecords(intRecTotal).strID = strArrayTemp(0)
For i = 0 To 5
structRecords(intRecTotal).intResults(i) = CInt(strArrayTemp(i + 1))
Next
structRecords(intRecTotal).strName = strArrayTemp(7)
rtbOutput.AppendText(structRecords(intRecTotal).strID.PadRight(9))
For i = 0 To 5
rtbOutput.AppendText(Convert.ToString(structRecords(intRecTotal).intResults(i)).PadLeft(6))
Next
rtbOutput.AppendText(" " & structRecords (intRecTotal).strName.PadRight(9) & vbCrLf)
Loop
srdFile.Close()
txtNumStudents.Text = intRecTotal + 1
cmdLoad.Enabled = False
cmdStats.Enabled = True
cmdPerformance.Enabled = True
cmdCreateFile.Enabled = True
End Sub
Private Sub cmdStats_Click(sender As Object, e As EventArgs) Handles cmdStats.Click
lblTitle.Text = "Examination Statistics"
rtbOutput.Clear()
rtbOutput.AppendText("Module No Average Median Std Dev Hi Score Candidate(s)" & vbCrLf)
rtbOutput.AppendText("--------- ------- ------ ------- -------- ------------" & vbCrLf)
getAverages()
getMedians()
getstdDevs()
getHiScores()
For i = 0 To 5
rtbOutput.AppendText("Module " & i + 1)
rtbOutput.AppendText(Convert.ToString(Format(dblAverages(i), "Fixed")).PadLeft(9))
rtbOutput.AppendText(Convert.ToString(Format(dblMedians(i), "Fixed")).PadLeft(7))
rtbOutput.AppendText(Convert.ToString(Format(dblstdDevs(i), "Fixed")).PadLeft(8))
rtbOutput.AppendText(Convert.ToString(intHiScores(i)).PadLeft(8))
rtbOutput.AppendText(" " & strHiScoreStudent(i) & vbCrLf)
Next
End Sub
Private Sub cmdPerformance_Click(sender As Object, e As EventArgs) Handles cmdPerformance.Click
lblTitle.Text = "Student Performance"
rtbOutput.Clear()
rtbOutput.AppendText("StudentID".PadRight(12) & "Student Name".PadRight(15))
rtbOutput.AppendText("Average Mark".PadRight(15) & "Level Achieved" & vbCrLf)
rtbOutput.AppendText("---------".PadRight(12) & "------------".PadRight (15))
rtbOutput.AppendText("------------".PadRight(15) & "--------------" & vbCrLf)
getStudentAverages()
getStudentLevels()
For i = 0 To intRecTotal
rtbOutput.AppendText(structRecords(i).strID.PadRight(12) & structRecords(i).strName.PadRight(15))
rtbOutput.AppendText(Convert.ToString(Format(dblStudentAverage(i), "Fixed")).PadLeft(12))
rtbOutput.AppendText(" " & strStudentLevel(i))
rtbOutput.AppendText(vbCrLf)
Next
End Sub
Private Sub cmdCreateFile_Click(sender As Object, e As EventArgs) Handles cmdCreateFile.Click
Dim swrFile As System.IO.StreamWriter
Dim strOutput As String
swrFile = IO.File.CreateText("student_progress.dat")
sortNames()
getStudentAverages()
getStudentLevels()
For i = 0 To intRecTotal
For j = 0 To intRecTotal
strOutput = ""
If LCase(structRecords(j).strName) = strNamesSorted(i) Then
strOutput &= structRecords(j) .strID & ","
strOutput &= structRecords(j) .strName & ","
strOutput &= Convert.ToString(Format (dblStudentAverage(j), "Fixed")) & ","
strOutput &= strStudentLevel(j)
swrFile.WriteLine(strOutput)
End If
Next
Next
swrFile.Close()
MsgBox("Output file created.")
End Sub
Private Sub cmdExit_Click(sender As Object, e As EventArgs) Handles cmdExit.Click
End
End Sub
End Class
The main disadvantage of arrays is that the size of an array is fixed. This is not a problem if you know in advance how many elements you will need. The size of an array holding the days of the week or the months of the year, for example, will always have the same number of elements. If you have a group of related elements whose size can vary (the number of books in a library, for example) you have to make your array large enough to meet future needs, as well as any existing requirement.
Obviously, the larger an array is, the more space it occupies in memory, and the longer it takes to sort or search through the array elements. Making an array twice as big as we actually need in order to allow for an increase in the number of elements however, whilst prudent, is not a particularly efficient way to use memory, especially if the amount of memory available is limited.
It is possible in Visual Basic to change the size of (redimension) an array programatically, as we have seen. This is one way of getting around the problem of ensuring that our array will always be large enough to accomodate our requirements, but since it involves allocating a new (larger) block of memory and moving the entire contents of the array from its existing location to the new block of memory, this is again not a particularly efficient use of memory - or processor time (we will be look at alternatives to the use of arrays elsewhere).