TechnologyUK - Programming (VB.NET) Logo

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 (or 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 (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. The main disadvantage of arrays is that very large arrays can occupy a lot of memory. 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:


The ExamScore program interface

The ExamScore program interface



ExamScores Form Controls
ControlName
Form1frmExamScores
Button1cmdRawData
Button2cmdSortData
Button3cmdAverage
Button4cmdHighScore
Button5cmdLowScore

Dim examScores() As Integer = {32, 64, 21, 97, 88, 59, 70, 55, 44, 11, 23, 66, 70, 47, 61, 77, 28, 38, 60, 93}

Dim strOutput As String = ""
For count = 0 To 19
    strOutput &= examScores(count) & vbNewLine
Next
MsgBox(strOutput)

Your code editor window should now look like the illustration below.


The array declaration and cmdRawData Click event procedure

The array declaration and cmdRawData Click event procedure



The message box output for the Display raw data button

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 .NET however, the task is now much easier.

Dim strOutput As String = ""
Array.Sort(examScores)
For count = 0 To 19
    strOutput &= examScores(count) & vbNewLine
Next
MsgBox(strOutput)


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 .NET 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.


The message box output for the Display sorted data button

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 .NET 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) & vbNewLine
Next
MsgBox(strOutput)

In the bubble sort algorithm referred to above, the program loops through the loop one array 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 a swap 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 (empty) jar in which to temporarily store the contents of one of the two jars so that I could transfer the contents of the other 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) & vbNewLine
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 is considered 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(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdAverage.Click
    Dim intTotal As Integer = 0
    For count = 0 To 19
        intTotal += examScores(count)
    Next
    MsgBox("Average score: " & CSng(intTotal / 20))
End Sub

Private Sub cmdHighScore_Click(ByVal sender As System.Object, ByVal e As System.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)
End Sub

Private Sub cmdLowScore_Click(ByVal sender As System.Object, ByVal e As System.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)
End Sub

Private Sub cmdExit_Click(ByVal sender As System.Object, ByVal e As System.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 examples dealt with 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 course of study, 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 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.


The StudentRecords program interface

The StudentRecords program interface



StudentRecords Form Controls
ControlName
Form1frmStudentRecords
Label1lblTitle
RichTextBox1rtbOutput
Button1cmdLoad
Button2cmdStats
Button3cmdPerformance
Button4cmdCreateFile
Button5cmdExit
Label3lblNumStudents

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 (less 1). 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.

Dim srdFile As System.IO.StreamReader
intRecTotal = -1
lblTitle.Text = "Student Input Data"
rtbOutput.Clear()
rtbOutput.AppendText("StudentID Mod1 Mod2 Mod3 Mod4 Mod5 Mod6 Candidate" & vbNewLine)
rtbOutput.AppendText("--------- ---- ---- ---- ---- ---- ---- ---------" & vbNewLine)
srdFile = New System.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) & vbNewLine)
Next
lblNumStudents.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 -1. 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 For ... Next loop that follows 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). He 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 label lblNumStudents, incrementing the value of incrRecTotal by one to get the actual number of student records read.

The last four lines 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 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()
    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 + 1) to get the average mark for each examination and assigns the result to the dblAverages() array.

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 subrouting 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 plus 0.5, and intRecTotal divided by two minus 0.5. If you are not sure how this works, assume there are 20 values in which case intRecTotal will be 19. We want the average of the 10th and 11th values to get the median, so we need to add together the values with the subscripts (9) and (10), and then divide the result by two. Dividing 19 by 2 gives us 9.5, so adding 0.5 gives us 10 while subtracting 0.5 gives us 9. Once the median for each set of examination marks has been determined, it is assigned to the dblMedians() array.

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 less one. Finally, we find the square root of the result to arrive at the standard deviation. The mathematical formula for standard deviation is shown below.


The formula for standard deviation

The formula for standard deviation


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()
    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
                strHiScoreStudents(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
                strHiScoreStudents(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 strHiScoreStudents() 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.

lblTitle.Text = "Examination Statistics"
rtbOutput.Clear()
rtbOutput.AppendText("Module No Average Median Std Dev Hi Score Candidate(s)" & vbNewLine)
rtbOutput.AppendText("--------- ------- ------ ------- -------- ------------" & vbNewLine)
getAverages()
getMedians()
getStdDevs()
getHiScores()
For i = 0 To 5
    rtbOutput.AppendText("Module " & i + 1)
    rtbOutput.AppendText(Convert.ToString(Format(dblAverages(i), "Fixed")).PadLeft(12))
    rtbOutput.AppendText(Convert.ToString(Format(dblMedians(i), "Fixed")).PadLeft(10))
    rtbOutput.AppendText(Convert.ToString(Format(dblstdDevs(i), "Fixed")).PadLeft(11))
    rtbOutput.AppendText(Convert.ToString(intHiScores(i)).PadLeft(12))
    rtbOutput.AppendText(" " & strHiScoreStudents(i) & vbNewLine)
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 and 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 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()
    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()
    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:

lblTitle.Text = "Student Performance"
rtbOutput.Clear()
rtbOutput.AppendText("StudentID".PadRight(12) & "Student Name".PadRight(15))
rtbOutput.AppendText("Average Mark".PadRight(15) & "Level Achieved" & vbNewLine)
rtbOutput.AppendText("---------".PadRight(12) & "------------".PadRight(15))
rtbOutput.AppendText("------------".PadRight(15) & "--------------" & vbNewLine)
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(vbNewLine)
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 and click on the "Student Performance" button to display the output, which should look something like the screenshot below.


The formatted student performance data

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.

Dim swrFile As System.IO.StreamWriter
Dim strOutput As String
swrFile = System.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.")


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). Once you have entered the code, run the program and 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

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 of type 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 may note that in this example, the code is not significantly reduced in volume, but may be slightly easier to follow. Note also 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 strHiScoreStudents(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
                    strHiScoreStudents(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
                    strHiScoreStudents(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(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdLoad.Click
        Dim srdFile As System.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" & vbNewLine)
        rtbOutput.AppendText("--------- ---- ---- ---- ---- ---- ---- ---------" & vbNewLine)
        srdFile = New System.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) & vbNewLine)
        Loop
        srdFile.Close()
        lblNumStudents.Text = intRecTotal + 1
        cmdLoad.Enabled = False
        cmdStats.Enabled = True
        cmdPerformance.Enabled = True
        cmdCreateFile.Enabled = True
    End Sub

    Private Sub cmdStats_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdStats.Click
        lblTitle.Text = "Examination Statistics"
        rtbOutput.Clear()
        rtbOutput.AppendText("Module No Average Median Std Dev Hi Score Candidate(s)" & vbNewLine)
        rtbOutput.AppendText("--------- ------- ------ ------- -------- ------------" & vbNewLine)
        getAverages()
        getMedians()
        getstdDevs()
        getHiScores()
        For i = 0 To 5
            rtbOutput.AppendText("Module " & i + 1)
            rtbOutput.AppendText(Convert.ToString(Format(dblAverages(i), "Fixed")).PadLeft(12))
            rtbOutput.AppendText(Convert.ToString(Format(dblMedians(i), "Fixed")).PadLeft(10))
            rtbOutput.AppendText(Convert.ToString(Format(dblstdDevs(i), "Fixed")).PadLeft(11))
            rtbOutput.AppendText(Convert.ToString(intHiScores(i)).PadLeft(12))
            rtbOutput.AppendText(" " & strHiScoreStudents(i) & vbNewLine)
        Next
    End Sub

    Private Sub cmdPerformance_Click(ByVal sender As System.Object, ByVal e As System.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" & vbNewLine)
        rtbOutput.AppendText("---------".PadRight(12) & "------------".PadRight(15))
        rtbOutput.AppendText("------------".PadRight(15) & "--------------" & vbNewLine)
        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(vbNewLine)
        Next
    End Sub

    Private Sub cmdCreateFile_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdCreateFile.Click
        Dim swrFile As System.IO.StreamWriter
        Dim strOutput As String
        swrFile = System.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(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdExit.Click
        End
    End Sub

End Class