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
-
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.
-
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.
-
Consistent parameter conventions make code predictable. When every read-only parameter is
constand every output parameter isvar, readers can quickly understand data flow from the procedure heading alone. -
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.
-
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
-
Add a volume conversion category with at least four conversions (liters/gallons, milliliters/fluid ounces, etc.).
-
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.
-
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.
-
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.