Case Study 2: GradeBook Pro — First Version

The Scenario

Ms. Alvarez teaches Introduction to Biology at Riverside Community College. She has a class of up to 30 students and needs a program that can:

  1. Store each student's name, numeric grade (0-100), and days of attendance (out of 40 class sessions).
  2. Calculate the class average, highest grade, and lowest grade.
  3. Display an honor roll (students with grades 90 or above).
  4. Show a grade distribution: how many A's, B's, C's, D's, and F's.
  5. Identify students at risk of failing due to poor attendance (below 75% of sessions).
  6. Display a formatted class report.

This is the first version of GradeBook Pro, our anchor example for Part II. In this chapter, we use parallel arrays. In Chapter 10, we will refactor it to use records. In later chapters, we will add file I/O, sorting, and a menu-driven interface.


Data Architecture: Parallel Arrays

We represent the class using three parallel arrays plus a count:

program GradeBookPro;

const
  MAX_STUDENTS = 30;
  TOTAL_SESSIONS = 40;
  HONOR_THRESHOLD = 90;
  ATTENDANCE_RISK = 0.75;  { 75% attendance threshold }

type
  TNameArray    = array[1..MAX_STUDENTS] of String;
  TGradeArray   = array[1..MAX_STUDENTS] of Integer;
  TAttendArray  = array[1..MAX_STUDENTS] of Integer;  { days present }

var
  names:      TNameArray;
  grades:     TGradeArray;
  attendance: TAttendArray;
  count:      Integer;  { actual number of students }

The parallel structure means that names[5], grades[5], and attendance[5] all refer to the same student — the fifth one entered. This correspondence is critical and fragile. If any operation rearranges one array without rearranging the others, the data becomes corrupted.


Input: Reading Student Data

procedure ReadStudents(var n: TNameArray;
                        var g: TGradeArray;
                        var a: TAttendArray;
                        var cnt: Integer);
var
  inputName: String;
  inputGrade, inputAttend: Integer;
begin
  cnt := 0;
  WriteLn('Enter student data. Type an empty name to stop.');
  WriteLn;

  repeat
    Write('Student name (or Enter to stop): ');
    ReadLn(inputName);

    if inputName = '' then
      Break;

    if cnt >= MAX_STUDENTS then
    begin
      WriteLn('Maximum of ', MAX_STUDENTS, ' students reached.');
      Break;
    end;

    { Validate grade }
    repeat
      Write('  Grade (0-100): ');
      ReadLn(inputGrade);
      if (inputGrade < 0) or (inputGrade > 100) then
        WriteLn('  Invalid. Please enter a value between 0 and 100.');
    until (inputGrade >= 0) and (inputGrade <= 100);

    { Validate attendance }
    repeat
      Write('  Days attended (0-', TOTAL_SESSIONS, '): ');
      ReadLn(inputAttend);
      if (inputAttend < 0) or (inputAttend > TOTAL_SESSIONS) then
        WriteLn('  Invalid. Please enter a value between 0 and ', TOTAL_SESSIONS, '.');
    until (inputAttend >= 0) and (inputAttend <= TOTAL_SESSIONS);

    Inc(cnt);
    n[cnt] := inputName;
    g[cnt] := inputGrade;
    a[cnt] := inputAttend;
    WriteLn;

  until False;  { loop exits via Break }
end;

Notice the input validation loops. Real programs cannot assume the user will provide valid data. Each nested repeat..until ensures the value is within the acceptable range before storing it.


Analysis Functions

Class Average

function ClassAverage(const g: TGradeArray; cnt: Integer): Real;
var
  i, total: Integer;
begin
  if cnt = 0 then
  begin
    ClassAverage := 0.0;
    Exit;
  end;
  total := 0;
  for i := 1 to cnt do
    total := total + g[i];
  ClassAverage := total / cnt;
end;

Finding Extremes

procedure FindExtremes(const g: TGradeArray;
                        cnt: Integer;
                        var highIdx, lowIdx: Integer);
var
  i: Integer;
begin
  highIdx := 1;
  lowIdx := 1;
  for i := 2 to cnt do
  begin
    if g[i] > g[highIdx] then
      highIdx := i;
    if g[i] < g[lowIdx] then
      lowIdx := i;
  end;
end;

This returns indices rather than values, so we can look up the corresponding student name. This is the key advantage of FindMaxIndex over FindMax.

Honor Roll

procedure DisplayHonorRoll(const n: TNameArray;
                            const g: TGradeArray;
                            cnt: Integer);
var
  i, honorCount: Integer;
begin
  WriteLn;
  WriteLn('--- HONOR ROLL (', HONOR_THRESHOLD, '+) ---');
  honorCount := 0;
  for i := 1 to cnt do
    if g[i] >= HONOR_THRESHOLD then
    begin
      WriteLn('  ', n[i]:20, '  ', g[i]:3);
      Inc(honorCount);
    end;
  if honorCount = 0 then
    WriteLn('  No students qualify for the honor roll.')
  else
    WriteLn('  (', honorCount, ' student(s) on honor roll)');
end;

Grade Distribution

procedure DisplayDistribution(const g: TGradeArray; cnt: Integer);
var
  i: Integer;
  dist: array['A'..'F'] of Integer;
  ch: Char;
  maxCount: Integer;
  bar: String;
begin
  for ch := 'A' to 'F' do
    dist[ch] := 0;

  for i := 1 to cnt do
    case g[i] of
      90..100: Inc(dist['A']);
      80..89:  Inc(dist['B']);
      70..79:  Inc(dist['C']);
      60..69:  Inc(dist['D']);
    else
      Inc(dist['F']);
    end;

  { Find maximum for scaling the bar chart }
  maxCount := 0;
  for ch := 'A' to 'F' do
    if (ch <> 'E') and (dist[ch] > maxCount) then
      maxCount := dist[ch];

  WriteLn;
  WriteLn('--- GRADE DISTRIBUTION ---');
  for ch := 'A' to 'F' do
  begin
    if ch = 'E' then Continue;  { skip E — not a letter grade }
    Write('  ', ch, ' (');
    case ch of
      'A': Write('90-100');
      'B': Write('80-89 ');
      'C': Write('70-79 ');
      'D': Write('60-69 ');
      'F': Write(' 0-59 ');
    end;
    Write('): ');

    { Draw bar }
    bar := '';
    for i := 1 to dist[ch] do
      bar := bar + '#';
    Write(bar:15);

    WriteLn(' [', dist[ch], ']');
  end;
end;

This combines the grade distribution from Section 9.9 with a simple horizontal bar chart. The character-indexed dist array makes the code natural and readable.

Attendance Risk Report

procedure DisplayAttendanceRisk(const n: TNameArray;
                                  const a: TAttendArray;
                                  cnt: Integer);
var
  i, riskCount: Integer;
  pct: Real;
begin
  WriteLn;
  WriteLn('--- ATTENDANCE RISK REPORT ---');
  WriteLn('  Students below ', (ATTENDANCE_RISK * 100):0:0,
          '% attendance (', Round(ATTENDANCE_RISK * TOTAL_SESSIONS),
          '/', TOTAL_SESSIONS, ' sessions):');
  WriteLn;

  riskCount := 0;
  for i := 1 to cnt do
  begin
    pct := a[i] / TOTAL_SESSIONS;
    if pct < ATTENDANCE_RISK then
    begin
      WriteLn('  ', n[i]:20, '  ',
              a[i], '/', TOTAL_SESSIONS, ' sessions (',
              (pct * 100):5:1, '%)');
      Inc(riskCount);
    end;
  end;

  if riskCount = 0 then
    WriteLn('  All students meet the attendance threshold.')
  else
    WriteLn('  (', riskCount, ' student(s) at risk)');
end;

Full Report

procedure DisplayFullReport(const n: TNameArray;
                              const g: TGradeArray;
                              const a: TAttendArray;
                              cnt: Integer);
var
  avg: Real;
  highIdx, lowIdx, i: Integer;
begin
  WriteLn;
  WriteLn('========================================');
  WriteLn('     GRADEBOOK PRO — CLASS REPORT');
  WriteLn('========================================');
  WriteLn;
  WriteLn('Students enrolled: ', cnt);

  if cnt = 0 then
  begin
    WriteLn('No students to report.');
    Exit;
  end;

  { Class roster }
  WriteLn;
  WriteLn('--- CLASS ROSTER ---');
  WriteLn('  Name':22, 'Grade':7, 'Attend':8, 'Pct':7);
  WriteLn('  ', StringOfChar('-', 42));
  for i := 1 to cnt do
    WriteLn('  ', n[i]:20, g[i]:7,
            a[i]:5, '/', TOTAL_SESSIONS,
            (a[i] / TOTAL_SESSIONS * 100):6:1, '%');

  { Statistics }
  avg := ClassAverage(g, cnt);
  FindExtremes(g, cnt, highIdx, lowIdx);

  WriteLn;
  WriteLn('--- CLASS STATISTICS ---');
  WriteLn('  Class average:  ', avg:0:1);
  WriteLn('  Highest grade:  ', g[highIdx], ' (', n[highIdx], ')');
  WriteLn('  Lowest grade:   ', g[lowIdx], ' (', n[lowIdx], ')');
  WriteLn('  Grade range:    ', g[highIdx] - g[lowIdx], ' points');

  { Sections }
  DisplayHonorRoll(n, g, cnt);
  DisplayDistribution(g, cnt);
  DisplayAttendanceRisk(n, a, cnt);
end;

Sample Session

Enter student data. Type an empty name to stop.

Student name (or Enter to stop): Alice Chen
  Grade (0-100): 95
  Days attended (0-40): 38

Student name (or Enter to stop): Bob Martinez
  Grade (0-100): 78
  Days attended (0-40): 35

Student name (or Enter to stop): Carol Washington
  Grade (0-100): 62
  Days attended (0-40): 22

Student name (or Enter to stop): David Kim
  Grade (0-100): 88
  Days attended (0-40): 39

Student name (or Enter to stop): Eva Johansson
  Grade (0-100): 91
  Days attended (0-40): 37

Student name (or Enter to stop):

========================================
     GRADEBOOK PRO — CLASS REPORT
========================================

Students enrolled: 5

--- CLASS ROSTER ---
                  Name  Grade  Attend    Pct
  ------------------------------------------
            Alice Chen     95   38/40  95.0%
         Bob Martinez     78   35/40  87.5%
      Carol Washington     62   22/40  55.0%
            David Kim     88   39/40  97.5%
        Eva Johansson     91   37/40  92.5%

--- CLASS STATISTICS ---
  Class average:  82.8
  Highest grade:  95 (Alice Chen)
  Lowest grade:   62 (Carol Washington)
  Grade range:    33 points

--- HONOR ROLL (90+) ---
            Alice Chen    95
        Eva Johansson    91
  (2 student(s) on honor roll)

--- GRADE DISTRIBUTION ---
  A (90-100): ##              [2]
  B (80-89 ): #               [1]
  C (70-79 ): #               [1]
  D (60-69 ): #               [1]
  F ( 0-59 ):                 [0]

--- ATTENDANCE RISK REPORT ---
  Students below 75% attendance (30/40 sessions):

      Carol Washington  22/40 sessions ( 55.0%)
  (1 student(s) at risk)

Design Analysis

What Works Well

  1. Separation of concerns. Each procedure handles one task. ClassAverage only computes the average; DisplayHonorRoll only displays the honor roll. This makes each piece testable and reusable.

  2. Constants for configuration. MAX_STUDENTS, TOTAL_SESSIONS, HONOR_THRESHOLD, and ATTENDANCE_RISK are all constants. Changing the honor threshold from 90 to 85 requires editing one line.

  3. Named types for safety. TNameArray, TGradeArray, and TAttendArray prevent accidentally passing grades where names are expected.

  4. Input validation. The program rejects invalid grades and attendance values immediately, before they can corrupt the data.

What We Will Improve

  1. Parallel arrays are fragile. We must always pass names, grades, and attendance together — and ensure they stay synchronized. In Chapter 10, we will replace them with an array of TStudent, where each record groups a student's name, grade, and attendance.

  2. No persistent storage. When the program ends, all data is lost. Chapter 12 (Files) will add the ability to save and load gradebook data.

  3. No sorting. We cannot display students sorted by grade or by name. Chapter 11 or a later chapter on sorting algorithms will add this capability.

  4. Fixed capacity. The 30-student limit wastes memory for small classes and cannot accommodate large ones. Dynamic arrays of records will solve this in Chapter 10.


Key Takeaways from GradeBook Pro v1

  • Parallel arrays work for prototyping but become a liability as programs grow. They are a stepping stone to records.
  • The algorithms are independent of the application. FindExtremes, ClassAverage, and the distribution histogram are generic patterns that apply to any dataset.
  • Character-indexed arrays (like dist['A'..'F']) produce remarkably clean code when the problem domain maps naturally to characters.
  • Constants and named types make the program configurable and type-safe — two qualities that scale with program complexity.
  • Input validation is not optional. Real users will enter "abc" when you expect a number, -5 when you expect a positive value, and 200 when the maximum is 100. Handle it.

GradeBook Pro will return in every chapter of Part II, evolving from this parallel-array prototype into a polished, record-based, file-backed application with sorting and search capabilities.