Case Study 1: Building a Unit Converter Library

A collection of conversion procedures and functions demonstrating organizing related functionality, parameter design choices, and function composition.


The Scenario

Tomás Vieira is taking an introductory physics course alongside his computer science studies. His lab assignments require frequent conversions between units: Celsius to Fahrenheit, meters to feet, kilograms to pounds, and so on. He has been doing these conversions by hand with a calculator, but after learning about procedures and functions in Chapter 7, he decides to build a reusable converter in Pascal.

His goals: 1. Write conversion functions that are correct and easy to use 2. Organize related conversions together 3. Build a menu-driven interface so he can quickly convert any value 4. Design the code so he can easily add new conversions later


Phase 1: Individual Conversion Functions

Tomás starts by writing the simplest possible conversion functions — temperature:

{ --- Temperature Conversions --- }

function CelsiusToFahrenheit(const C: Real): Real;
begin
  Result := (C * 9.0 / 5.0) + 32.0;
end;

function FahrenheitToCelsius(const F: Real): Real;
begin
  Result := (F - 32.0) * 5.0 / 9.0;
end;

function CelsiusToKelvin(const C: Real): Real;
begin
  Result := C + 273.15;
end;

function KelvinToCelsius(const K: Real): Real;
begin
  Result := K - 273.15;
end;

Design decision: const parameters. Each function reads its input but never modifies it. Using const documents this clearly. Even though Real is a small type where the copy cost is trivial, the const serves as documentation of intent: "this function is pure — it computes a result from its input and nothing else."

Design decision: Real return type. Temperature conversions naturally produce fractional results. Using Real (which maps to Double in Free Pascal, giving approximately 15 significant digits) provides more than enough precision for any practical measurement.

Composing Functions

Now Tomás needs Fahrenheit-to-Kelvin and Kelvin-to-Fahrenheit conversions. He could write the formulas from scratch, but he realizes he already has the building blocks:

function FahrenheitToKelvin(const F: Real): Real;
begin
  Result := CelsiusToKelvin(FahrenheitToCelsius(F));
end;

function KelvinToFahrenheit(const K: Real): Real;
begin
  Result := CelsiusToFahrenheit(KelvinToCelsius(K));
end;

This is function composition — using the output of one function as the input to another. Each function does one thing, and complex conversions are built by chaining simple ones. If Tomás later discovers a bug in CelsiusToKelvin, fixing it automatically fixes FahrenheitToKelvin as well. The DRY principle in action.


Phase 2: Distance and Weight Conversions

Tomás adds distance conversions:

{ --- Distance Conversions --- }

function MetersToFeet(const M: Real): Real;
const
  FeetPerMeter = 3.28084;
begin
  Result := M * FeetPerMeter;
end;

function FeetToMeters(const Ft: Real): Real;
const
  MetersPerFoot = 0.3048;
begin
  Result := Ft * MetersPerFoot;
end;

function KilometersToMiles(const Km: Real): Real;
const
  MilesPerKm = 0.621371;
begin
  Result := Km * MilesPerKm;
end;

function MilesToKilometers(const Mi: Real): Real;
const
  KmPerMile = 1.60934;
begin
  Result := Mi * KmPerMile;
end;

function InchesToCentimeters(const Inches: Real): Real;
begin
  Result := Inches * 2.54;
end;

function CentimetersToInches(const Cm: Real): Real;
begin
  Result := Cm / 2.54;
end;

Design decision: local constants. Tomás declares conversion factors as local constants (FeetPerMeter, MetersPerFoot) rather than magic numbers. This makes the formulas self-documenting. Anyone reading Result := M * FeetPerMeter understands exactly what the computation does.

Design decision: inverse functions use independent constants. Notice that MetersPerFoot (0.3048) is not computed as 1 / FeetPerMeter (which would be 1 / 3.28084). Tomás uses the standard defined value for each direction. This avoids compounding floating-point rounding errors.

And weight conversions:

{ --- Weight Conversions --- }

function KilogramsToPounds(const Kg: Real): Real;
const
  PoundsPerKg = 2.20462;
begin
  Result := Kg * PoundsPerKg;
end;

function PoundsToKilograms(const Lbs: Real): Real;
const
  KgPerPound = 0.453592;
begin
  Result := Lbs * KgPerPound;
end;

function OuncesToGrams(const Oz: Real): Real;
begin
  Result := Oz * 28.3495;
end;

function GramsToOunces(const G: Real): Real;
begin
  Result := G / 28.3495;
end;

Phase 3: A Reusable Input Procedure

Every conversion follows the same pattern: prompt the user for a value, convert it, display the result. Tomás writes a general-purpose input procedure:

procedure ReadValue(const Prompt: string; var Value: Real);
var
  InputOK: Boolean;
begin
  InputOK := False;
  repeat
    Write(Prompt);
    {$I-}
    ReadLn(Value);
    {$I+}
    if IOResult <> 0 then
      WriteLn('  Invalid number. Please try again.')
    else
      InputOK := True;
  until InputOK;
end;

Design decision: const for Prompt, var for Value. The prompt is a string we only display — const prevents accidental modification and avoids copying. The value is the output of the procedure — var is essential because the whole purpose is to fill in the caller's variable.

A Display Procedure

procedure DisplayConversion(const FromValue: Real; const FromUnit: string;
                            const ToValue: Real; const ToUnit: string);
begin
  WriteLn;
  WriteLn('  ', FromValue:0:4, ' ', FromUnit, ' = ', ToValue:0:4, ' ', ToUnit);
  WriteLn;
end;

All four parameters are const — this procedure only reads and displays.


Phase 4: The Menu-Driven Interface

Tomás organizes the conversions into a hierarchical menu. He writes a procedure for each sub-menu:

procedure ShowMainMenu;
begin
  WriteLn;
  WriteLn('=== Unit Converter ===');
  WriteLn('1. Temperature');
  WriteLn('2. Distance');
  WriteLn('3. Weight');
  WriteLn('4. Quit');
  WriteLn('======================');
  Write('Choice: ');
end;

procedure ShowTemperatureMenu;
begin
  WriteLn;
  WriteLn('--- Temperature ---');
  WriteLn('1. Celsius to Fahrenheit');
  WriteLn('2. Fahrenheit to Celsius');
  WriteLn('3. Celsius to Kelvin');
  WriteLn('4. Kelvin to Celsius');
  WriteLn('5. Fahrenheit to Kelvin');
  WriteLn('6. Kelvin to Fahrenheit');
  WriteLn('7. Back to main menu');
  WriteLn('-------------------');
  Write('Choice: ');
end;

Then a handler procedure for each category:

procedure HandleTemperature;
var
  Choice: Char;
  InputVal, OutputVal: Real;
begin
  repeat
    ShowTemperatureMenu;
    ReadLn(Choice);
    case Choice of
      '1': begin
             ReadValue('  Enter Celsius: ', InputVal);
             OutputVal := CelsiusToFahrenheit(InputVal);
             DisplayConversion(InputVal, 'C', OutputVal, 'F');
           end;
      '2': begin
             ReadValue('  Enter Fahrenheit: ', InputVal);
             OutputVal := FahrenheitToCelsius(InputVal);
             DisplayConversion(InputVal, 'F', OutputVal, 'C');
           end;
      '3': begin
             ReadValue('  Enter Celsius: ', InputVal);
             OutputVal := CelsiusToKelvin(InputVal);
             DisplayConversion(InputVal, 'C', OutputVal, 'K');
           end;
      '4': begin
             ReadValue('  Enter Kelvin: ', InputVal);
             OutputVal := KelvinToCelsius(InputVal);
             DisplayConversion(InputVal, 'K', OutputVal, 'C');
           end;
      '5': begin
             ReadValue('  Enter Fahrenheit: ', InputVal);
             OutputVal := FahrenheitToKelvin(InputVal);
             DisplayConversion(InputVal, 'F', OutputVal, 'K');
           end;
      '6': begin
             ReadValue('  Enter Kelvin: ', InputVal);
             OutputVal := KelvinToFahrenheit(InputVal);
             DisplayConversion(InputVal, 'K', OutputVal, 'F');
           end;
      '7': { do nothing — will exit loop };
    else
      WriteLn('  Invalid choice.');
    end;
  until Choice = '7';
end;

Distance and weight handlers follow the same pattern.


Phase 5: The Main Program

With all the pieces in place, the main program is remarkably simple:

var
  MainChoice: Char;
begin
  repeat
    ShowMainMenu;
    ReadLn(MainChoice);
    case MainChoice of
      '1': HandleTemperature;
      '2': HandleDistance;
      '3': HandleWeight;
      '4': WriteLn('Goodbye!');
    else
      WriteLn('  Invalid choice. Please enter 1-4.');
    end;
  until MainChoice = '4';
end.

Five lines of logic in the main body. The entire program is perhaps 250 lines, but no single procedure is longer than 40 lines. Each piece can be understood, tested, and modified independently.


Analysis: What Makes This Design Good?

1. Pure Functions for All Conversions

Every conversion function is pure: it takes input, computes a result, and returns it without side effects. This means: - Tomás can test any function by calling it with known values and checking the result - Functions compose naturally (FahrenheitToKelvin chains two other functions) - No hidden state — the same input always produces the same output

2. Consistent Parameter Conventions

Every conversion function uses const for its input parameter. Every input procedure uses const for prompts and var for outputs. Every display procedure uses const for all parameters. The pattern is so consistent that you can predict a procedure's parameter modes from its name.

3. Separation of Concerns

  • Conversion functions know math but not I/O
  • Input procedures know I/O but not math
  • Display procedures know formatting but not conversion
  • Handler procedures coordinate between them
  • The main program coordinates between handlers

No single procedure knows everything. This separation means Tomás can change the display format without touching the conversion logic, or fix a formula without touching the user interface.

4. Easy to Extend

Adding a new conversion category (say, volume) requires: 1. Write the conversion functions (pure math) 2. Write a ShowVolumeMenu procedure (copy an existing menu, change the text) 3. Write a HandleVolume procedure (copy an existing handler, change the function calls) 4. Add one line to ShowMainMenu and one case to the main CASE statement

The existing code does not need to change at all. This is the hallmark of well-decomposed code.


Testing the Conversions

Tomás writes a quick sanity check by computing round-trip conversions:

procedure RunSanityChecks;
var
  Original, RoundTrip: Real;
begin
  WriteLn('=== Sanity Checks ===');

  Original := 100.0;
  RoundTrip := FahrenheitToCelsius(CelsiusToFahrenheit(Original));
  WriteLn('100 C -> F -> C = ', RoundTrip:0:6, ' (expect 100.000000)');

  Original := 1.0;
  RoundTrip := FeetToMeters(MetersToFeet(Original));
  WriteLn('1 m -> ft -> m  = ', RoundTrip:0:6, ' (expect 1.000000)');

  Original := 1.0;
  RoundTrip := PoundsToKilograms(KilogramsToPounds(Original));
  WriteLn('1 kg -> lb -> kg = ', RoundTrip:0:6, ' (expect 1.000000)');

  WriteLn('=====================');
end;

The round-trip values are not exactly 1.000000 due to floating-point representation — they might be 0.999999 or 1.000001. This is expected behavior with Real arithmetic. The deviations are in the sixth decimal place, far below the precision of any physical measurement.


Lessons Learned

  1. Functions for computation, procedures for action. Every conversion is a function (produces a value). Every I/O operation is a procedure (performs an action). This distinction keeps the code clean and testable.

  2. Function composition reduces redundancy. Rather than writing six independent temperature formulas, Tomás wrote four and composed them into six. Any bug fix propagates automatically.

  3. Consistent parameter conventions make code predictable. When every read-only parameter is const and every output parameter is var, readers can quickly understand data flow from the procedure heading alone.

  4. Decomposition enables extension. Adding a new category requires writing new code, not modifying existing code. This principle — open for extension, closed for modification — will resurface as a formal design principle in Part III.

  5. The main program body tells the story. Five lines that say: show menu, get choice, handle temperature / distance / weight, or quit. Anyone can understand the program's structure in seconds.


Exercises for This Case Study

  1. Add a volume conversion category with at least four conversions (liters/gallons, milliliters/fluid ounces, etc.).

  2. Add an "all temperature conversions" option that takes a single temperature in any unit and displays it in all three scales simultaneously. Hint: Write a procedure that calls three functions.

  3. Tomás notices that the distance and weight handlers contain a lot of repeated structure. Can you refactor them to reduce duplication? Hint: Think about what varies between cases and what stays the same.

  4. Add input validation: reject negative Kelvin values (absolute zero is 0 K) and display a meaningful error message. Where should this validation live — in the conversion function, in the input procedure, or in the handler? Justify your answer.