📂 Pascal Programming: Files, Arrays, and Memory Management

Andrew J. Pounds, Ph.D.

🎯 Learning Objectives

After completing this exercise, you will be able to:

1Command-Line Arguments

Command-line arguments allow users to pass information to a program when it is executed. In Pascal, command-line arguments are accessed using the built-in ParamCount function and ParamStr function.

Basic Command-Line Argument Access

Pascal provides two functions to access command-line arguments:

Function Description
ParamCount Returns the number of arguments passed (excluding the program name)
ParamStr(n) Returns the nth argument as a string (0 = program name)
program CmdArgs;
var
  i: integer;
begin
  writeln('Number of arguments: ', ParamCount);
  for i := 0 to ParamCount do
    writeln('ParamStr(', i, ') = ', ParamStr(i));
end.

Compile and run with arguments:

fpc cmdargs.pas
./cmdargs hello world 123
Number of arguments: 3
ParamStr(0) = ./cmdargs
ParamStr(1) = hello
ParamStr(2) = world
ParamStr(3) = 123
Note: ParamStr(0) is always the program name itself. User-provided arguments start at ParamStr(1). Unlike C/C++'s argc, ParamCount does not include the program name in its count.

Processing Command-Line Arguments

Here's an example that expects a filename as a command-line argument:

program ProcessArgs;
uses
  SysUtils;  { For StrToInt }

var
  filename: string;
  count: integer;

begin
  { Check if correct number of arguments provided }
  if ParamCount < 1 then
  begin
    writeln('Usage: ', ParamStr(0), ' <filename> [count]');
    Halt(1);  { Exit with error code }
  end;

  filename := ParamStr(1);
  writeln('Processing file: ', filename);

  { If a second argument is provided, treat it as a number }
  if ParamCount >= 2 then
  begin
    count := StrToInt(ParamStr(2));
    writeln('Count parameter: ', count);
  end;
end.
Note: The Halt procedure terminates the program immediately. You can pass an exit code as an argument (0 typically means success, non-zero indicates an error).

2Reading from Files

Pascal provides robust file handling capabilities through the TextFile type (for text files) and related procedures. File operations follow a specific pattern: assign, open, read/write, close.

Opening and Reading a File

The basic approach uses Assign, Reset, and Close:

program ReadFileDemo;
var
  f: TextFile;
  line: string;
begin
  if ParamCount < 1 then
  begin
    writeln('Usage: ', ParamStr(0), ' <filename>');
    Halt(1);
  end;

  { Associate file variable with filename }
  Assign(f, ParamStr(1));

  { Open file for reading }
  Reset(f);

  { Read line by line until end of file }
  while not Eof(f) do
  begin
    ReadLn(f, line);
    writeln(line);
  end;

  { Close the file }
  Close(f);
end.

File Operation Procedures

Procedure Description
Assign(f, name) Associates a file variable with a filename
Reset(f) Opens an existing file for reading
Rewrite(f) Creates a new file for writing (overwrites if exists)
Append(f) Opens a file for appending (writes to end)
ReadLn(f, var) Reads a line from file into variable
WriteLn(f, data) Writes data to file followed by newline
Eof(f) Returns true if at end of file
Close(f) Closes the file

Error Handling with File Operations

It's good practice to handle potential file errors using the {$I} directive and IOResult:

program SafeFileRead;
var
  f: TextFile;
  line: string;
  filename: string;
begin
  if ParamCount < 1 then
  begin
    writeln('Usage: ', ParamStr(0), ' <filename>');
    Halt(1);
  end;

  filename := ParamStr(1);
  Assign(f, filename);

  { Disable automatic I/O error checking }
  {$I-}
  Reset(f);
  {$I+}

  { Check if file opened successfully }
  if IOResult <> 0 then
  begin
    writeln('Error: File ''', filename, ''' not found.');
    Halt(1);
  end;

  writeln('File contents:');
  while not Eof(f) do
  begin
    ReadLn(f, line);
    writeln(line);
  end;

  Close(f);
end.
Note: The {$I-} directive disables automatic I/O checking, allowing you to handle errors manually with IOResult. Always re-enable it with {$I+} after checking. If IOResult returns 0, the operation succeeded.

Using FileExists (Modern Approach)

The SysUtils unit provides a cleaner way to check if a file exists:

program CheckFileExists;
uses
  SysUtils;

var
  f: TextFile;
  line: string;
  filename: string;

begin
  if ParamCount < 1 then
  begin
    writeln('Usage: ', ParamStr(0), ' <filename>');
    Halt(1);
  end;

  filename := ParamStr(1);

  { Check if file exists before opening }
  if not FileExists(filename) then
  begin
    writeln('Error: File ''', filename, ''' not found.');
    Halt(1);
  end;

  Assign(f, filename);
  Reset(f);

  while not Eof(f) do
  begin
    ReadLn(f, line);
    writeln(line);
  end;

  Close(f);
end.

3Tokenizing Strings

Tokenization is the process of breaking a string into smaller pieces (tokens) based on delimiters. Pascal provides several ways to accomplish this using string manipulation functions.

Basic String Functions

Function Description Example
Pos(substr, str) Returns position of substring (1-based), or 0 if not found Pos(',', 'a,b,c') → 2
Copy(str, start, len) Returns a substring starting at position start Copy('hello', 2, 3) → 'ell'
Length(str) Returns the length of the string Length('hello') → 5
Delete(str, start, len) Removes characters from string (modifies in place) Delete(s, 1, 3)
Trim(str) Removes leading and trailing whitespace (SysUtils) Trim(' hi ') → 'hi'

Manual Tokenization with Pos and Copy

Here's how to split a string using a delimiter:

program TokenizeDemo;
var
  sentence: string;
  token: string;
  delimPos: integer;
  tokenCount: integer;
begin
  sentence := 'The quick brown fox jumps';
  tokenCount := 0;

  writeln('Original: ', sentence);
  writeln('Tokens:');

  { Process tokens while string is not empty }
  while Length(sentence) > 0 do
  begin
    delimPos := Pos(' ', sentence);

    if delimPos > 0 then
    begin
      { Extract token before delimiter }
      token := Copy(sentence, 1, delimPos - 1);
      { Remove token and delimiter from string }
      Delete(sentence, 1, delimPos);
    end
    else
    begin
      { No more delimiters - rest of string is last token }
      token := sentence;
      sentence := '';
    end;

    Inc(tokenCount);
    writeln('  Token ', tokenCount, ': ', token);
  end;

  writeln('Total tokens: ', tokenCount);
end.
Original: The quick brown fox jumps
Tokens:
  Token 1: The
  Token 2: quick
  Token 3: brown
  Token 4: fox
  Token 5: jumps
Total tokens: 5

Creating a Reusable Tokenizer Function

It's helpful to create a function that extracts the next token from a string:

program TokenizerFunction;
uses
  SysUtils;

{ Extracts and returns the next token, modifying the source string }
function GetNextToken(var source: string; delimiter: char): string;
var
  delimPos: integer;
begin
  source := Trim(source);  { Remove leading/trailing whitespace }

  if Length(source) = 0 then
  begin
    GetNextToken := '';
    exit;
  end;

  delimPos := Pos(delimiter, source);

  if delimPos > 0 then
  begin
    GetNextToken := Copy(source, 1, delimPos - 1);
    Delete(source, 1, delimPos);
  end
  else
  begin
    GetNextToken := source;
    source := '';
  end;
end;

var
  csvLine: string;
  field: string;
begin
  csvLine := 'Alice,25,Computer Science,3.85';
  writeln('Parsing CSV: ', csvLine);
  writeln;

  field := GetNextToken(csvLine, ',');
  writeln('Name: ', field);

  field := GetNextToken(csvLine, ',');
  writeln('Age: ', field);

  field := GetNextToken(csvLine, ',');
  writeln('Major: ', field);

  field := GetNextToken(csvLine, ',');
  writeln('GPA: ', field);
end.

Tokenizing File Contents

Combining file reading with tokenization:

program TokenizeFile;
uses
  SysUtils;

function GetNextToken(var source: string; delimiter: char): string;
var
  delimPos: integer;
begin
  source := Trim(source);

  if Length(source) = 0 then
  begin
    GetNextToken := '';
    exit;
  end;

  delimPos := Pos(delimiter, source);

  if delimPos > 0 then
  begin
    GetNextToken := Copy(source, 1, delimPos - 1);
    Delete(source, 1, delimPos);
  end
  else
  begin
    GetNextToken := source;
    source := '';
  end;
end;

var
  f: TextFile;
  line: string;
  token: string;
  tokenCount: integer;
begin
  if ParamCount < 1 then
  begin
    writeln('Usage: ', ParamStr(0), ' <filename>');
    Halt(1);
  end;

  if not FileExists(ParamStr(1)) then
  begin
    writeln('Error: File not found.');
    Halt(1);
  end;

  tokenCount := 0;
  Assign(f, ParamStr(1));
  Reset(f);

  while not Eof(f) do
  begin
    ReadLn(f, line);
    { Tokenize each line on spaces }
    while Length(line) > 0 do
    begin
      token := GetNextToken(line, ' ');
      if Length(token) > 0 then
        Inc(tokenCount);
    end;
  end;

  Close(f);
  writeln('Total tokens in file: ', tokenCount);
end.
Note: When tokenizing on whitespace, you may encounter multiple consecutive spaces. The Trim function helps clean up the string, and checking for empty tokens prevents counting blank entries.

4Arrays in Pascal

Pascal supports both static arrays (fixed size, determined at compile time) and dynamic arrays (size can change at runtime). Understanding both is essential for effective Pascal programming.

Static Arrays

Static arrays have a fixed size that must be known at compile time:

program StaticArrayDemo;
var
  { Array with indices 1 to 5 }
  numbers: array[1..5] of integer;

  { Array with indices 0 to 4 (more common in modern usage) }
  grades: array[0..4] of real;

  { Array initialized at declaration }
  days: array[1..7] of string = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun');

  i: integer;
begin
  { Assign values }
  for i := 1 to 5 do
    numbers[i] := i * 10;

  { Access values }
  writeln('Numbers:');
  for i := 1 to 5 do
    writeln('  numbers[', i, '] = ', numbers[i]);

  writeln;
  writeln('Days of the week:');
  for i := 1 to 7 do
    writeln('  ', days[i]);
end.
Note: Pascal arrays can use any ordinal type as indices, not just integers starting at 0. You can have array[1..100], array['A'..'Z'], or even arrays indexed by enumerated types!

Dynamic Arrays

Dynamic arrays can change size at runtime using SetLength:

program DynamicArrayDemo;
var
  { Dynamic array - no size specified }
  numbers: array of integer;
  words: array of string;
  i: integer;
begin
  { Set initial size }
  SetLength(numbers, 5);

  { Dynamic arrays are always 0-indexed }
  for i := 0 to 4 do
    numbers[i] := (i + 1) * 10;

  writeln('Initial array (length ', Length(numbers), '):');
  for i := 0 to High(numbers) do
    writeln('  numbers[', i, '] = ', numbers[i]);

  { Grow the array }
  SetLength(numbers, 8);
  numbers[5] := 60;
  numbers[6] := 70;
  numbers[7] := 80;

  writeln;
  writeln('After growing (length ', Length(numbers), '):');
  for i := 0 to High(numbers) do
    writeln('  numbers[', i, '] = ', numbers[i]);

  { String array example }
  SetLength(words, 3);
  words[0] := 'Hello';
  words[1] := 'Dynamic';
  words[2] := 'World';

  writeln;
  writeln('Words:');
  for i := 0 to High(words) do
    writeln('  ', words[i]);
end.

Useful Array Functions

Function Description
Length(arr) Returns the number of elements in the array
High(arr) Returns the highest valid index (Length - 1 for dynamic arrays)
Low(arr) Returns the lowest valid index (always 0 for dynamic arrays)
SetLength(arr, n) Sets the size of a dynamic array (preserves existing elements)
Note: Dynamic arrays in Pascal are always 0-indexed, regardless of how static arrays are declared. Use High(arr) instead of Length(arr) - 1 for cleaner code.

Building an Array by Appending Elements

Since Pascal doesn't have a built-in append operator like C++'s push_back, you need to resize the array manually. Important: When passing dynamic arrays to procedures that need to resize them, you must define a type alias:

program AppendToArray;

type
  { Define type aliases for dynamic arrays }
  TIntArray = array of integer;

var
  numbers: TIntArray;
  i: integer;

{ Procedure to append a value to a dynamic array }
procedure AppendInt(var arr: TIntArray; value: integer);
begin
  SetLength(arr, Length(arr) + 1);
  arr[High(arr)] := value;
end;

begin
  { Start with empty array }
  SetLength(numbers, 0);

  { Append elements one at a time }
  AppendInt(numbers, 10);
  AppendInt(numbers, 20);
  AppendInt(numbers, 30);
  AppendInt(numbers, 40);
  AppendInt(numbers, 50);

  writeln('Array after appending (length ', Length(numbers), '):');
  for i := 0 to High(numbers) do
    writeln('  [', i, '] = ', numbers[i]);
end.
Important: In Free Pascal, array of integer in a procedure parameter declaration creates an "open array parameter," which cannot be resized with SetLength. To pass dynamic arrays that need resizing, you must declare a named type (like TIntArray = array of integer) and use that type in both the variable declaration and procedure parameter.

Storing Tokens in an Array

A complete example that reads a file, tokenizes it, and stores tokens in a dynamic array:

program TokensToArray;
uses
  SysUtils;

type
  TStringArray = array of string;

var
  tokens: TStringArray;
  f: TextFile;
  line, token: string;
  delimPos: integer;
  i: integer;

procedure AppendToken(var arr: TStringArray; value: string);
begin
  SetLength(arr, Length(arr) + 1);
  arr[High(arr)] := value;
end;

begin
  if ParamCount < 1 then
  begin
    writeln('Usage: ', ParamStr(0), ' <filename>');
    Halt(1);
  end;

  if not FileExists(ParamStr(1)) then
  begin
    writeln('Error: File not found.');
    Halt(1);
  end;

  { Initialize empty array }
  SetLength(tokens, 0);

  Assign(f, ParamStr(1));
  Reset(f);

  while not Eof(f) do
  begin
    ReadLn(f, line);
    line := Trim(line);

    while Length(line) > 0 do
    begin
      delimPos := Pos(' ', line);

      if delimPos > 0 then
      begin
        token := Copy(line, 1, delimPos - 1);
        Delete(line, 1, delimPos);
        line := Trim(line);
      end
      else
      begin
        token := line;
        line := '';
      end;

      if Length(token) > 0 then
        AppendToken(tokens, token);
    end;
  end;

  Close(f);

  { Display results }
  writeln('=== File Token Analysis ===');
  writeln('Total tokens: ', Length(tokens));

  if Length(tokens) > 0 then
  begin
    writeln('First token: ', tokens[0]);
    writeln('Last token: ', tokens[High(tokens)]);
  end;

  writeln;
  writeln('Tokens longer than 5 characters:');
  for i := 0 to High(tokens) do
  begin
    if Length(tokens[i]) > 5 then
      writeln('  [', i, ']: ', tokens[i], ' (length: ', Length(tokens[i]), ')');
  end;
end.

5Memory Management in Pascal

Unlike languages with garbage collection (like Java or Python), Pascal requires more awareness of memory management. Understanding how Pascal handles memory is crucial for writing correct and efficient programs.

Automatic vs. Manual Memory Management

Pascal uses different strategies depending on the type of data:

Data Type Memory Management Notes
Static variables Automatic Allocated on stack, freed when scope ends
Strings Automatic (reference counted) FPC handles memory automatically
Dynamic arrays Automatic (reference counted) Freed when no longer referenced
Pointers (New/GetMem) Manual Must call Dispose/FreeMem
Objects/Classes Manual Must call Free or destructor

Reference Counting for Dynamic Arrays

Free Pascal uses reference counting for dynamic arrays and strings. When the last reference to an array goes away, the memory is automatically freed:

program RefCountDemo;

type
  TIntArray = array of integer;
  TStringArray = array of string;

procedure CreateArrays;
var
  arr1: TIntArray;
  arr2: TStringArray;
begin
  { Memory is allocated automatically }
  SetLength(arr1, 1000);
  SetLength(arr2, 100);

  writeln('Arrays created inside procedure');
  writeln('arr1 length: ', Length(arr1));
  writeln('arr2 length: ', Length(arr2));

  { When this procedure returns, arr1 and arr2 go out of scope }
  { Reference count drops to 0, memory is automatically freed }
end;

var
  i: integer;
begin
  writeln('=== Reference Counting Demo ===');

  for i := 1 to 5 do
  begin
    writeln;
    writeln('Iteration ', i);
    CreateArrays;
    { Memory from previous call has been freed }
  end;

  writeln;
  writeln('Program completed - all memory cleaned up automatically');
end.
Note: Free Pascal's reference counting for dynamic arrays provides automatic memory management similar to garbage collection, but with deterministic deallocation — memory is freed immediately when the last reference disappears, not at some later time chosen by a garbage collector.

Manual Memory Management with Pointers

When using pointers directly, you must manually allocate and free memory:

program ManualMemoryDemo;
type
  PStudent = ^TStudent;
  TStudent = record
    name: string;
    age: integer;
    gpa: real;
  end;

var
  student: PStudent;
begin
  writeln('=== Manual Memory Management Demo ===');

  { Allocate memory for the record }
  New(student);
  writeln('Memory allocated with New()');

  { Use the allocated memory }
  student^.name := 'Alice';
  student^.age := 20;
  student^.gpa := 3.85;

  writeln('Student: ', student^.name);
  writeln('Age: ', student^.age);
  writeln('GPA: ', student^.gpa:0:2);

  { IMPORTANT: Free the memory when done }
  Dispose(student);
  writeln('Memory freed with Dispose()');

  { Failing to call Dispose() would cause a memory leak! }
end.
Important: When using New() to allocate memory, you must call Dispose() to free it. Forgetting to free memory causes memory leaks — your program will consume more and more memory over time.

Comparison: Pascal vs. Garbage-Collected Languages

Feature Pascal (FPC) Garbage-Collected (Java, Python)
Dynamic arrays Automatic (reference counted) Automatic (GC)
Strings Automatic (reference counted) Automatic (GC)
Pointer/object allocation Manual (New/Dispose) Automatic (GC)
When memory is freed Immediately when ref count = 0 When GC runs (unpredictable)
Memory leaks possible Yes (if Dispose forgotten) Rare (GC prevents most)
Performance overhead Minimal GC pauses possible

Best Practices for Memory Management

program MemoryBestPractices;
uses
  SysUtils;

type
  TIntArray = array of integer;
  
  PData = ^TData;
  TData = record
    values: TIntArray;
    name: string;
  end;

var
  data: PData;
begin
  data := nil;  { Initialize pointer to nil }

  try
    { Allocate memory }
    New(data);

    { Use the memory }
    data^.name := 'Test Data';
    SetLength(data^.values, 5);
    data^.values[0] := 100;

    writeln('Data name: ', data^.name);
    writeln('First value: ', data^.values[0]);

  finally
    { Always free memory, even if an error occurred }
    if data <> nil then
    begin
      { Dynamic array inside record is freed automatically }
      Dispose(data);
      data := nil;  { Set to nil after freeing }
    end;
  end;

  writeln('Memory properly cleaned up');
end.
Best Practice: Use try...finally blocks to ensure memory is freed even if an error occurs. Always set pointers to nil after freeing to avoid "dangling pointer" bugs where you accidentally use freed memory.

Practical Example: Processing Large Data

This example shows how dynamic arrays handle memory automatically when processing files:

program ProcessLargeData;
uses
  SysUtils;

type
  TStringArray = array of string;

procedure AnalyzeFile(filename: string);
var
  f: TextFile;
  line: string;
  tokens: TStringArray;
  longWords: TStringArray;
  delimPos: integer;
  token: string;
begin
  writeln('Analyzing: ', filename);

  { Initialize dynamic arrays - memory allocated automatically }
  SetLength(tokens, 0);
  SetLength(longWords, 0);

  Assign(f, filename);
  Reset(f);

  while not Eof(f) do
  begin
    ReadLn(f, line);
    line := Trim(line);

    while Length(line) > 0 do
    begin
      delimPos := Pos(' ', line);
      if delimPos > 0 then
      begin
        token := Copy(line, 1, delimPos - 1);
        Delete(line, 1, delimPos);
        line := Trim(line);
      end
      else
      begin
        token := line;
        line := '';
      end;

      if Length(token) > 0 then
      begin
        SetLength(tokens, Length(tokens) + 1);
        tokens[High(tokens)] := token;

        if Length(token) > 6 then
        begin
          SetLength(longWords, Length(longWords) + 1);
          longWords[High(longWords)] := token;
        end;
      end;
    end;
  end;

  Close(f);

  writeln('  Tokens found: ', Length(tokens));
  writeln('  Long words: ', Length(longWords));

  { When this procedure returns:
    - tokens array reference count drops to 0 -> memory freed
    - longWords array reference count drops to 0 -> memory freed
    - All strings in arrays are also freed (reference counted)
    No manual cleanup needed! }
end;

var
  i: integer;
begin
  if ParamCount < 1 then
  begin
    writeln('Usage: ', ParamStr(0), ' <file1> [file2] [file3] ...');
    Halt(1);
  end;

  writeln('=== Multi-File Analyzer ===');
  writeln;

  for i := 1 to ParamCount do
  begin
    if FileExists(ParamStr(i)) then
    begin
      AnalyzeFile(ParamStr(i));
      writeln;
    end
    else
      writeln('Skipping (not found): ', ParamStr(i));
  end;

  writeln('Done! All temporary memory has been reclaimed.');
end.
Key Insight: For most Pascal programs using dynamic arrays and strings, you don't need to worry about memory management — FPC handles it automatically through reference counting. Manual memory management is only needed when using pointers with New/GetMem or when working with objects/classes.

6Complete Example: Word Frequency Analyzer

This comprehensive example combines all the concepts: command-line arguments, file reading, tokenization, arrays, and demonstrates Pascal's memory management:

program WordFrequencyAnalyzer;
uses
  SysUtils;

type
  TStringArray = array of string;
  TIntArray = array of integer;

var
  f: TextFile;
  line, token, word: string;
  filename: string;
  delimPos: integer;

  { Dynamic arrays for storing data }
  tokens: TStringArray;
  uniqueWords: TStringArray;
  wordCounts: TIntArray;

  i, j: integer;
  found: boolean;
  maxCount: integer;
  mostCommon: string;

{ Remove punctuation from beginning and end of a word }
function StripPunctuation(s: string): string;
const
  Punctuation = '.,!?;:"''()-';
begin
  while (Length(s) > 0) and (Pos(s[1], Punctuation) > 0) do
    Delete(s, 1, 1);

  while (Length(s) > 0) and (Pos(s[Length(s)], Punctuation) > 0) do
    Delete(s, Length(s), 1);

  StripPunctuation := s;
end;

begin
  { Check command-line arguments }
  if ParamCount < 1 then
  begin
    writeln('Word Frequency Analyzer');
    writeln('Usage: ', ParamStr(0), ' <filename>');
    Halt(1);
  end;

  filename := ParamStr(1);

  { Check if file exists }
  if not FileExists(filename) then
  begin
    writeln('Error: File ''', filename, ''' not found.');
    Halt(1);
  end;

  { Initialize dynamic arrays }
  SetLength(tokens, 0);
  SetLength(uniqueWords, 0);
  SetLength(wordCounts, 0);

  { Read and tokenize the file }
  Assign(f, filename);
  Reset(f);

  while not Eof(f) do
  begin
    ReadLn(f, line);
    line := LowerCase(Trim(line));

    while Length(line) > 0 do
    begin
      delimPos := Pos(' ', line);
      if delimPos > 0 then
      begin
        token := Copy(line, 1, delimPos - 1);
        Delete(line, 1, delimPos);
        line := Trim(line);
      end
      else
      begin
        token := line;
        line := '';
      end;

      if Length(token) > 0 then
      begin
        SetLength(tokens, Length(tokens) + 1);
        tokens[High(tokens)] := token;
      end;
    end;
  end;

  Close(f);

  { Count word frequencies }
  for i := 0 to High(tokens) do
  begin
    word := StripPunctuation(tokens[i]);

    if Length(word) = 0 then
      continue;

    { Check if word already exists }
    found := false;
    for j := 0 to High(uniqueWords) do
    begin
      if uniqueWords[j] = word then
      begin
        Inc(wordCounts[j]);
        found := true;
        break;
      end;
    end;

    { Add new word if not found }
    if not found then
    begin
      SetLength(uniqueWords, Length(uniqueWords) + 1);
      SetLength(wordCounts, Length(wordCounts) + 1);
      uniqueWords[High(uniqueWords)] := word;
      wordCounts[High(wordCounts)] := 1;
    end;
  end;

  { Display results }
  writeln('=== Word Frequency Analysis ===');
  writeln('File: ', filename);
  writeln('Total tokens: ', Length(tokens));
  writeln('Unique words: ', Length(uniqueWords));
  writeln;

  { Display words with count > 1 }
  writeln('Words appearing more than once:');
  writeln('--------------------------------');
  for i := 0 to High(uniqueWords) do
  begin
    if wordCounts[i] > 1 then
      writeln('  ', uniqueWords[i]:15, ' : ', wordCounts[i]);
  end;

  { Find most common word }
  maxCount := 0;
  mostCommon := '';
  for i := 0 to High(uniqueWords) do
  begin
    if wordCounts[i] > maxCount then
    begin
      maxCount := wordCounts[i];
      mostCommon := uniqueWords[i];
    end;
  end;

  writeln;
  writeln('Most common word: ''', mostCommon, ''' (', maxCount, ' occurrences)');

  { Memory note: All dynamic arrays (tokens, uniqueWords, wordCounts)
    will be automatically freed when the program ends because their
    reference counts drop to 0. No manual cleanup needed! }
end.

7Quick Reference

Task Pascal Syntax
Get argument count ParamCount
Get argument n ParamStr(n)
Exit program with code Halt(exitCode);
Associate file variable Assign(f, filename);
Open file for reading Reset(f);
Read line from file ReadLn(f, line);
Check end of file Eof(f)
Close file Close(f);
Check file exists FileExists(filename)
Find substring position Pos(substr, str)
Extract substring Copy(str, start, length)
Delete from string Delete(str, start, length);
Declare dynamic array type type TMyArray = array of Type;
Declare dynamic array variable var arr: TMyArray;
Set array size SetLength(arr, n);
Get array length Length(arr)
Get highest index High(arr)
Allocate pointer memory New(ptr);
Free pointer memory Dispose(ptr);

📝 Programming Exercise: CSV Data Processor

Write a Pascal program that processes a CSV (Comma-Separated Values) file containing student grade data. Your program should:

  1. Accept the filename as a command-line argument
    • Display a usage message if no filename is provided
    • Display an error if the file doesn't exist
  2. Read and parse the CSV file where each line has the format:
    StudentName,Score1,Score2,Score3
    Example: Alice,85,92,78
  3. Store the data in arrays:
    • A string array for student names
    • A real array for each student's average score
  4. Calculate and display:
    • Each student's name and average score
    • The class average (average of all student averages)
    • The highest and lowest scoring students
  5. Demonstrate understanding of memory management by adding a comment in your code explaining where memory is automatically freed

Sample Input File (grades.csv)

Alice,85,92,78
Bob,90,88,95
Charlie,72,68,75
Diana,95,98,92

Sample Output

=== Student Grade Analyzer ===
File: grades.csv
Students loaded: 4

Student Averages:
  Alice           : 85.00
  Bob             : 91.00
  Charlie         : 71.67
  Diana           : 95.00

Class Statistics:
  Class Average  : 85.67
  Highest Score  : Diana (95.00)
  Lowest Score   : Charlie (71.67)

Hints

Submission

Save your program as grade_analyzer.pas and compile with:

fpc grade_analyzer.pas
./grade_analyzer grades.csv

📋 Student Starter Code

Use the following template to get started. Fill in each blank marked with ________ with the appropriate code. Read the comments carefully for hints!

Instructions

  1. Save this code as grade_analyzer.pas
  2. Read through all the comments to understand the program structure
  3. Fill in each blank with the appropriate code
  4. Compile and test your program with the sample grades.csv file
program GradeAnalyzer;
{
  CSV Grade Analyzer - Student Starter Code
  =========================================
  Complete the sections marked with TODO to finish this program.

  This program will:
  1. Accept a CSV filename as a command-line argument
  2. Read and parse the CSV file (Name,Score1,Score2,Score3)
  3. Store student names and calculated averages in arrays
  4. Display individual averages and class statistics

  Author: [Your Name]
  Date:   [Date]
}

uses
  SysUtils;  { For StrToFloat, FileExists, Trim }

type
  { IMPORTANT: Define type aliases for dynamic arrays so they can be
    passed to procedures and resized with SetLength }
  TStringArray = array of string;
  TRealArray = array of real;

var
  { File handling }
  f: TextFile;
  line: string;
  filename: string;

  { Dynamic arrays for storing student data }
  names: TStringArray;
  averages: TRealArray;

  { Variables for parsing and calculations }
  token: string;
  score1, score2, score3: real;
  studentAvg, classTotal, classAvg: real;
  highestAvg, lowestAvg: real;
  highestName, lowestName: string;
  i: integer;

{
  ===========================================================================
  Function: GetNextToken
  ===========================================================================
  Extracts the next token from a string based on a delimiter.
  Modifies the source string by removing the extracted token.
  
  Parameters:
    source    - the string to extract from (modified by this function)
    delimiter - the character to split on
    
  Returns:
    The extracted token (text before the delimiter)
    
  HINT: Use Pos() to find the delimiter, Copy() to extract,
        and Delete() to remove the token from source.
}
function GetNextToken(var source: string; delimiter: char): string;
var
  delimPos: integer;
begin
  source := Trim(source);

  if Length(source) = 0 then
  begin
    GetNextToken := '';
    exit;
  end;

  { TODO: Find the position of the delimiter in source
          Use: Pos(whatToFind, whereToLook) }
  delimPos := ________(delimiter, ________);

  if delimPos > 0 then
  begin
    { TODO: Extract token (everything before the delimiter)
            Use: Copy(sourceString, startPosition, length) }
    GetNextToken := ________(source, 1, delimPos - 1);

    { TODO: Remove the token and delimiter from source
            Use: Delete(stringVar, startPosition, count) }
    ________(source, 1, delimPos);
  end
  else
  begin
    { No delimiter found - return entire string }
    GetNextToken := source;
    source := '';
  end;
end;

{
  ===========================================================================
  Procedure: AppendStudent
  ===========================================================================
  Adds a student's name and average to the dynamic arrays.
  
  HINT: Use SetLength to grow the arrays by 1,
        then assign to the new last element using High().
        
  NOTE: We use TStringArray and TRealArray types here (not "array of string")
        so that SetLength can resize the arrays.
}
procedure AppendStudent(var nameArr: TStringArray;
                        var avgArr: TRealArray;
                        studentName: string;
                        studentAvg: real);
begin
  { TODO: Grow the name array by 1
          Use: SetLength(arrayVar, newSize) }
  ________(nameArr, Length(nameArr) + 1);
  
  { TODO: Grow the average array by 1 }
  ________(avgArr, ________(avgArr) + 1);

  { TODO: Add the student name at the end of nameArr
          Use: High(arrayVar) to get the last index }
  nameArr[________(nameArr)] := studentName;
  
  { TODO: Add the student average at the end of avgArr }
  avgArr[________(avgArr)] := studentAvg;
end;


{ =========================================================================== }
{ ===== MAIN PROGRAM ======================================================== }
{ =========================================================================== }
begin

  { ================================================ }
  { PART 1: Command-Line Argument Handling           }
  { ================================================ }

  { TODO: Check if a filename was provided
          HINT: Use ParamCount to get number of arguments }
  if ________ < 1 then
  begin
    writeln('=== Student Grade Analyzer ===');
    { TODO: Display the program name using ParamStr(0) }
    writeln('Usage: ', ________(0), ' <filename>');
    Halt(1);
  end;

  { TODO: Get the filename from command-line argument #1 }
  filename := ________(1);

  { TODO: Check if file exists
          HINT: Use FileExists() function from SysUtils }
  if not ________(filename) then
  begin
    writeln('Error: File ''', filename, ''' not found.');
    Halt(1);
  end;


  { ================================================ }
  { PART 2: Initialize Arrays and Open File          }
  { ================================================ }

  { TODO: Initialize dynamic arrays to empty (length 0)
          Use: SetLength(arrayVar, 0) }
  ________(names, 0);
  ________(averages, 0);

  { TODO: Associate the file variable with the filename
          Use: Assign(fileVar, filename) }
  ________(f, filename);
  
  { TODO: Open the file for reading
          Use: Reset(fileVar) }
  ________(f);


  { ================================================ }
  { PART 3: Read and Parse Each Line                 }
  { ================================================ }

  { TODO: Loop while not at end of file
          Use: while not Eof(fileVar) do }
  while not ________(f) do
  begin
    { TODO: Read a line from the file into 'line' variable
            Use: ReadLn(fileVar, stringVar) }
    ________(f, line);

    { Skip empty lines }
    if Length(Trim(line)) = 0 then
      continue;

    { TODO: Extract student name (first token before comma)
            Use the GetNextToken function you completed above }
    token := ________(line, ',');

    { TODO: Extract score1 - get next token and convert to real
            Use: StrToFloat(stringValue) to convert string to real }
    score1 := StrToFloat(________(line, ','));
    
    { TODO: Extract score2 }
    score2 := ________(GetNextToken(line, ','));
    
    { TODO: Extract score3 }
    score3 := ________(GetNextToken(line, ','));

    { TODO: Calculate the student's average
            Formula: (score1 + score2 + score3) / 3 }
    studentAvg := (________ + ________ + ________) / 3;

    { TODO: Add student to arrays using AppendStudent procedure }
    ________(names, averages, token, studentAvg);
  end;

  { TODO: Close the file
          Use: Close(fileVar) }
  ________(f);


  { ================================================ }
  { PART 4: Calculate Statistics                     }
  { ================================================ }

  { Initialize statistics variables }
  classTotal := 0;
  highestAvg := 0;
  lowestAvg := 999999;
  highestName := '';
  lowestName := '';

  { TODO: Loop through all students to calculate statistics
          Use: for i := 0 to High(arrayVar) do }
  for i := 0 to ________(names) do
  begin
    { Add this student's average to class total }
    classTotal := classTotal + averages[i];

    { TODO: Check if this is the highest average so far }
    if averages[i] > ________ then
    begin
      highestAvg := averages[i];
      { TODO: Save the name of the highest-scoring student }
      highestName := ________[i];
    end;

    { TODO: Check if this is the lowest average so far }
    if averages[i] < ________ then
    begin
      { TODO: Save the lowest average }
      lowestAvg := ________[i];
      lowestName := names[i];
    end;
  end;

  { TODO: Calculate class average
          Formula: classTotal / numberOfStudents
          Use: Length(arrayVar) to get number of students }
  classAvg := classTotal / ________(names);


  { ================================================ }
  { PART 5: Display Results                          }
  { ================================================ }

  writeln('=== Student Grade Analyzer ===');
  writeln('File: ', filename);
  { TODO: Display the number of students loaded }
  writeln('Students loaded: ', ________(names));
  writeln;

  writeln('Student Averages:');
  for i := 0 to High(names) do
    writeln('  ', names[i]:15, ': ', averages[i]:0:2);

  writeln;
  writeln('Class Statistics:');
  writeln('  Class Average  : ', classAvg:0:2);
  writeln('  Highest Score  : ', highestName, ' (', highestAvg:0:2, ')');
  writeln('  Lowest Score   : ', lowestName, ' (', lowestAvg:0:2, ')');

  {
    ===========================================================================
    MEMORY MANAGEMENT EXPLANATION (TODO: Complete this section)
    ===========================================================================
    
    TODO: Answer these questions in your own words:
    
    1. Why don't we need to manually free the 'names' and 'averages' arrays?
    
       YOUR ANSWER: ___________________________________________________
       
       ________________________________________________________________
    
    2. When exactly is the memory for these arrays released?
    
       YOUR ANSWER: ___________________________________________________
       
       ________________________________________________________________
    
    3. How is this different from languages like Java or Python?
    
       YOUR ANSWER: ___________________________________________________
       
       ________________________________________________________________
  }
end.

Blank Reference Guide

Use this table to check your work. Each blank corresponds to a specific Pascal concept:

Section Blank Location Concept Needed
GetNextToken delimPos := ________(delimiter, ________); Pos function and source string
GetNextToken GetNextToken := ________(source, 1, delimPos - 1); Copy function
GetNextToken ________(source, 1, delimPos); Delete procedure
AppendStudent ________(nameArr, Length(nameArr) + 1); SetLength procedure
AppendStudent ________(avgArr) + 1 Length function
AppendStudent nameArr[________(nameArr)] High function
Part 1 if ________ < 1 ParamCount
Part 1 ________(0) and ________(1) ParamStr function
Part 1 if not ________(filename) FileExists function
Part 2 ________(names, 0) SetLength procedure
Part 2 ________(f, filename) Assign procedure
Part 2 ________(f) Reset procedure
Part 3 while not ________(f) Eof function
Part 3 ________(f, line) ReadLn procedure
Part 3 token := ________(line, ',') GetNextToken function
Part 3 ________(GetNextToken(...)) StrToFloat function
Part 3 (________ + ________ + ________) / 3 score1, score2, score3 variables
Part 3 ________(names, averages, token, studentAvg) AppendStudent procedure
Part 3 ________(f) (at end) Close procedure
Part 4 for i := 0 to ________(names) High function
Part 4 if averages[i] > ________ highestAvg variable
Part 4 highestName := ________[i] names array
Part 4 if averages[i] < ________ lowestAvg variable
Part 4 lowestAvg := ________[i] averages array
Part 4 classTotal / ________(names) Length function
Part 5 'Students loaded: ', ________(names) Length function

Testing Your Program

Create a file named grades.csv with this content:

Alice,85,92,78
Bob,90,88,95
Charlie,72,68,75
Diana,95,98,92

Then test with these scenarios:

Test Command Expected Result
No arguments ./grade_analyzer Usage message displayed
Missing file ./grade_analyzer nofile.csv Error message about file not found
Valid file ./grade_analyzer grades.csv All statistics displayed correctly

Expected Values for Sample Data

Student Average
Alice 85.00
Bob 91.00
Charlie 71.67
Diana 95.00
Class Average 85.67

Common Errors to Watch For