Case Study 2: GradeBook Pro — Sorting Students by Name, GPA, and ID with Pluggable Comparisons

The Scenario

Professor Nakamura uses GradeBook Pro to manage a class of 150 students. She needs to view the roster in different orders depending on the task:

  • Alphabetical by last name for attendance.
  • Descending by GPA for honors nominations.
  • Ascending by student ID for matching to the registrar's records.
  • By GPA descending, with ties broken by last name ascending for the final grade report.

Rather than writing four separate sort routines, we use the comparison function pattern from Section 23.7 to write one sort that works with any ordering criterion.

The Implementation

program GradeBookSort;

uses
  SysUtils;

type
  TStudent = record
    ID: Integer;
    FirstName: string[30];
    LastName: string[30];
    GPA: Real;
  end;

  TStudentArray = array of TStudent;
  TStudentCompare = function(const A, B: TStudent): Integer;

{ === Comparison Functions === }

function CompareByLastName(const A, B: TStudent): Integer;
begin
  if A.LastName < B.LastName then CompareByLastName := -1
  else if A.LastName > B.LastName then CompareByLastName := 1
  else begin
    { Tie-break by first name }
    if A.FirstName < B.FirstName then CompareByLastName := -1
    else if A.FirstName > B.FirstName then CompareByLastName := 1
    else CompareByLastName := 0;
  end;
end;

function CompareByGPADesc(const A, B: TStudent): Integer;
begin
  if A.GPA > B.GPA then CompareByGPADesc := -1
  else if A.GPA < B.GPA then CompareByGPADesc := 1
  else CompareByGPADesc := 0;
end;

function CompareByID(const A, B: TStudent): Integer;
begin
  if A.ID < B.ID then CompareByID := -1
  else if A.ID > B.ID then CompareByID := 1
  else CompareByID := 0;
end;

function CompareByGPAThenName(const A, B: TStudent): Integer;
begin
  { Primary: GPA descending }
  Result := CompareByGPADesc(A, B);
  { Secondary: Last name ascending }
  if Result = 0 then
    Result := CompareByLastName(A, B);
end;

{ === Merge Sort (stable, preserves secondary ordering) === }

procedure MergeStudents(var Arr: TStudentArray;
                        Left, Mid, Right: Integer;
                        Compare: TStudentCompare);
var
  Temp: TStudentArray;
  I, J, K, Size: Integer;
begin
  Size := Right - Left + 1;
  SetLength(Temp, Size);
  I := Left; J := Mid + 1; K := 0;

  while (I <= Mid) and (J <= Right) do begin
    if Compare(Arr[I], Arr[J]) <= 0 then begin
      Temp[K] := Arr[I]; Inc(I);
    end else begin
      Temp[K] := Arr[J]; Inc(J);
    end;
    Inc(K);
  end;

  while I <= Mid do begin Temp[K] := Arr[I]; Inc(I); Inc(K); end;
  while J <= Right do begin Temp[K] := Arr[J]; Inc(J); Inc(K); end;

  for I := 0 to Size - 1 do
    Arr[Left + I] := Temp[I];
end;

procedure MergeSortStudents(var Arr: TStudentArray;
                            Left, Right: Integer;
                            Compare: TStudentCompare);
var
  Mid: Integer;
begin
  if Left >= Right then Exit;
  Mid := (Left + Right) div 2;
  MergeSortStudents(Arr, Left, Mid, Compare);
  MergeSortStudents(Arr, Mid + 1, Right, Compare);
  MergeStudents(Arr, Left, Mid, Right, Compare);
end;

{ === Display === }

procedure PrintRoster(const Students: TStudentArray;
                      Count: Integer; const Title: string);
var
  I: Integer;
begin
  WriteLn('--- ', Title, ' ---');
  WriteLn('  ID':6, 'Last Name':16, 'First Name':14, 'GPA':8);
  WriteLn(StringOfChar('-', 46));
  for I := 0 to Count - 1 do
    with Students[I] do
      WriteLn(ID:6, LastName:16, FirstName:14, GPA:8:2);
  WriteLn;
end;

{ === Build Sample Data === }

procedure BuildSampleClass(var Students: TStudentArray;
                           var Count: Integer);
begin
  Count := 10;
  SetLength(Students, Count);

  Students[0] := Default(TStudent);
  Students[0].ID := 1042; Students[0].FirstName := 'Alice';
  Students[0].LastName := 'Zhang'; Students[0].GPA := 3.85;

  Students[1] := Default(TStudent);
  Students[1].ID := 1015; Students[1].FirstName := 'Bob';
  Students[1].LastName := 'Martinez'; Students[1].GPA := 3.42;

  Students[2] := Default(TStudent);
  Students[2].ID := 1078; Students[2].FirstName := 'Carla';
  Students[2].LastName := 'Anderson'; Students[2].GPA := 3.91;

  Students[3] := Default(TStudent);
  Students[3].ID := 1023; Students[3].FirstName := 'David';
  Students[3].LastName := 'Kim'; Students[3].GPA := 3.67;

  Students[4] := Default(TStudent);
  Students[4].ID := 1056; Students[4].FirstName := 'Elena';
  Students[4].LastName := 'Okafor'; Students[4].GPA := 3.91;

  Students[5] := Default(TStudent);
  Students[5].ID := 1003; Students[5].FirstName := 'Fatima';
  Students[5].LastName := 'Hassan'; Students[5].GPA := 3.55;

  Students[6] := Default(TStudent);
  Students[6].ID := 1091; Students[6].FirstName := 'George';
  Students[6].LastName := 'Petrov'; Students[6].GPA := 3.42;

  Students[7] := Default(TStudent);
  Students[7].ID := 1034; Students[7].FirstName := 'Hana';
  Students[7].LastName := 'Nakamura'; Students[7].GPA := 3.78;

  Students[8] := Default(TStudent);
  Students[8].ID := 1067; Students[8].FirstName := 'Ivan';
  Students[8].LastName := 'Volkov'; Students[8].GPA := 3.33;

  Students[9] := Default(TStudent);
  Students[9].ID := 1008; Students[9].FirstName := 'Julia';
  Students[9].LastName := 'Anderson'; Students[9].GPA := 3.85;
end;

{ === Main === }

var
  Students: TStudentArray;
  Count: Integer;

begin
  WriteLn('=== GradeBook Pro: Multi-Criteria Sorting ===');
  WriteLn;

  BuildSampleClass(Students, Count);

  PrintRoster(Students, Count, 'Original Order');

  MergeSortStudents(Students, 0, Count - 1, @CompareByLastName);
  PrintRoster(Students, Count, 'Sorted by Last Name (Attendance)');

  MergeSortStudents(Students, 0, Count - 1, @CompareByGPADesc);
  PrintRoster(Students, Count, 'Sorted by GPA Descending (Honors)');

  MergeSortStudents(Students, 0, Count - 1, @CompareByID);
  PrintRoster(Students, Count, 'Sorted by Student ID (Registrar)');

  MergeSortStudents(Students, 0, Count - 1, @CompareByGPAThenName);
  PrintRoster(Students, Count, 'Sorted by GPA desc, then Name asc (Grade Report)');

  WriteLn('Notice: Carla Anderson and Elena Okafor both have 3.91 GPA.');
  WriteLn('In the GPA-then-Name sort, Anderson comes before Okafor');
  WriteLn('because the name comparison breaks the tie.');
  WriteLn;
  WriteLn('Also notice: Alice Zhang and Julia Anderson both have 3.85 GPA.');
  WriteLn('Anderson comes before Zhang alphabetically.');
end.

Key Design Decisions

Why Merge Sort?

We chose merge sort over quicksort for GradeBook Pro because stability matters. When students have equal GPAs, merge sort preserves the order from the previous sort. This means sorting by GPA after sorting by name keeps alphabetical order within each GPA group — exactly what Professor Nakamura wants.

Composed Comparisons

The CompareByGPAThenName function demonstrates composition: it delegates to CompareByGPADesc first, and only calls CompareByLastName if the GPAs are equal. This pattern scales to any number of criteria:

function CompareByGPAThenNameThenID(const A, B: TStudent): Integer;
begin
  Result := CompareByGPADesc(A, B);
  if Result = 0 then Result := CompareByLastName(A, B);
  if Result = 0 then Result := CompareByID(A, B);
end;

Reverse Ordering

To reverse any comparison, negate the result or swap the arguments:

function CompareByGPAAsc(const A, B: TStudent): Integer;
begin
  CompareByGPAAsc := -CompareByGPADesc(A, B);
end;

Discussion Questions

  1. Why is stability important when sorting by multiple criteria? What would go wrong if we used an unstable sort?

  2. Professor Nakamura wants to find the top 5 students by GPA. Is it necessary to sort the entire array? What alternative approach could be more efficient?

  3. If GradeBook Pro has 10,000 students, how many comparisons would merge sort make? How many would selection sort make?

  4. The comparison function pattern is sometimes called the "Strategy Pattern" in object-oriented design. How would you implement this using objects and polymorphism instead of function pointers?

Key Takeaways

  • Comparison functions separate the what (sorting logic) from the how (comparison criterion).
  • A single sort implementation can sort by any field when parameterized by a comparison function.
  • Composed comparisons handle multi-field sorting elegantly.
  • Merge sort's stability preserves secondary ordering, making it ideal for multi-criteria sorts.
  • This pattern is used throughout real-world software: database ORDER BY, spreadsheet column sorts, and file manager listings all use pluggable comparisons.