{ This program is a solution to exercise #1 for CSC 206, ``Fundamentals of
computer science II,'' offered at Grinnell College in fall semester,
1996.
In this exercise, the student is asked to write a program that prepares
a list of the counties of Iowa, in descending order by population
density. The name, population, and area of each county in Iowa is to be
read in from the text file /u2/stone/datasets/Iowa-counties.dat, which
contains one line for each county, each line containing exactly
twenty-eight characters: Columns 1 through 13 contain the county name,
left-justified; columns 14 through 16 are spaces; columns 17 through 22
contain the population, right-justified; columns 23 through 25 are
spaces; and columns 26 through 28 contain the area in square miles,
right-justified. Here's a sample line:
Poweshiek 19018 586
The program's output is to be written to a text file named
Iowa-counties-by-density.dat, with one line for each county, each line
containing exactly twenty-six characters: Columns 1 and 2 will contain
the county's rank in descending order by population density; column 3
will contain a period and column 4 a space; columns 5 through 17 will
contain the name of the county, left-justified; columns 18 and 19 will be
blank; columns 20 through 26 will contain the county's population
density, in inhabitants per square mile, rounded to the nearest
hundredth, right-justified. Here's a sample line from the desired output
file:
36. Poweshiek 32.45
This program follows a straightforward plan: Read in all the data from
the source file; compute all the densities; sort the counties into the
appropriate order; write out the results. However, both the source file
and the output file are exhaustively checked to ensure that any errors
in format are detected and reported.
Programmer: John Stone, Grinnell College.
Original version: August 25 - September 3, 1996.
}
program IowaCountyDensities (Source, Target, Output);
const
NameStringLength = 13;
{ Every county in Iowa has a name of thirteen or fewer characters. }
NumberOfCounties = 99;
{ Iowa has 99 counties. }
SourceFileName = '/u2/stone/datasets/Iowa-counties.dat';
{ the name of the file containing the data used in the computation }
TargetFileName = 'Iowa-counties-by-density.dat';
{ the name of the file to which the sorted list is to be written }
Space = ' ';
{ a more legible name for the space character }
type
NameString = packed array [1 .. NameStringLength] of Char;
{ a string long enough to hold the name of any Iowa county }
Natural = 0 .. MaxInt;
{ a non-negative integer, checked at input or at assignment }
County = record
Name: NameString;
Population: Natural;
Area: Real;
Density: Real
end;
{ the four items of information about each county that the program
deals with }
State = array [1 .. NumberOfCounties] of County;
{ an entry for each county in the state }
var
Source: Text;
{ the file containing the data used in the computation }
Target: Text;
{ the file to which the sorted list is to be written }
Iowa: State;
{ the state of Iowa, analyzed as an array of counties }
SuccessfulRead: Boolean;
{ indicates whether the data was successfully read in from the source
file }
{ The ReadName procedure tries to read in, from a specified source file,
exactly NameStringLength characters, without encountering either the
end of a line or the end of the file. If it succeeds, the characters
are stored in the parameter Legend and the parameter Success is set to
True; otherwise, Success is set to False and the contents of Legend
are undefined. }
procedure ReadName (var Source: Text; var Legend: NameString;
var Success: Boolean);
var
Position: Natural;
{ counts off characters as they are inserted into the NameString }
begin
Position := 0;
Success := True;
while Success and (Position < NameStringLength) do
if EOF (Source) then
Success := False
else if EOLn (Source) then
Success := False
else begin
Position := Position + 1;
Read (Source, Legend[Position])
end
end;
{ The Match procedure tries to read in, from a specified source file, a
specified number of copies of a specified character. It indicates
whether it has succeeded by setting the parameter Success. The
characters read are discarded. }
procedure Match (var Source: Text; Sought: Char; Copies: Natural;
var Success: Boolean);
var
Count: Natural;
{ the number of matching characters so far detected }
begin
Count := 0;
Success := True;
while Success and (Count < Copies) do
if EOF (Source) then
Success := False
else if EOLn (Source) then
Success := False
else if Source^ <> Sought then
Success := False
else begin
Get (Source);
Count := Count + 1
end
end;
{ The ReadFixedWidthNatural tries to read in a natural number, which must
be right-justified in a field of specified width that is otherwise
occupied by spaces. If it succeeds, it stores the natural number in
the Legend parameter and sets the Success parameter to True; otherwise,
it sets Success to False and the contents of Legend are undefined.
The procedure can fail for any of several reasons:
* The end of the input file is encountered.
* The end of an input line is encountered.
* The value of the numeral being read exceeds MaxInt.
* A character that is neither a space nor a digit is encountered.
* A space is encountered after the numeral has begun.
The procedure will stop as soon as any of these conditions is detected,
without consuming any erroneous character. }
procedure ReadFixedWidthNatural (var Source: Text; Width: Natural;
var Legend: Natural; var Success: Boolean);
var
Position: Natural;
{ counts off characters as they are read in }
DigitEncountered: Boolean;
{ indicates whether any digit characters have so far been
encountered in the input (if so, no more spaces should be
seen) }
Digit: Natural;
{ the numeric value of the next character of the source file, known
to be a digit character }
{ The IsDigit function determines whether the character it is given
is a digit character. }
function IsDigit (Ch: Char): Boolean;
begin
if (Ch < '0') then
IsDigit := False
else
IsDigit := (Ch <= '9')
end;
{ The DigitValue function takes a character that has been determined
to be a digit and returns its numerical value. }
function DigitValue (Ch: Char): Natural;
begin
DigitValue := Ord (Ch) - Ord ('0')
end;
{ The CanBeExtended function determines whether the natural number
that would result from an attempt to add an extra digit to a given
natural number exceeds MaxInt. It returns True if the resulting
number would not exceed MaxInt and so would still fit in the Natural
type defined above; it returns False if the computation would cause
an overflow. }
function CanBeExtended (Foundation: Natural; Extension: Natural):
Boolean;
begin
if Foundation < MaxInt div 10 then
CanBeExtended := True
else if MaxInt div 10 < Foundation then
CanBeExtended := False
else
CanBeExtended := (Extension <= MaxInt mod 10)
end;
begin { procedure ReadFixedWidthNatural }
Position := 0;
DigitEncountered := False;
Legend := 0;
Success := True;
while (Position < Width) and Success do
if EOF (Source) then
Success := False
else if EOLn (Source) then
Success := False
else if IsDigit (Source^) then begin
DigitEncountered := True;
Digit := DigitValue (Source^);
if CanBeExtended (Legend, Digit) then begin
Legend := Legend * 10 + Digit;
Get (Source);
Position := Position + 1
end
else
Success := False
end
else if Source^ <> Space then
Success := False
else if DigitEncountered then
Success := False
else begin
Get (Source);
Position := Position + 1
end;
if not DigitEncountered then
Success := False
end;
{ The ReadCountyData procedure collects information about one county
from one line of the source file and stores it in a County variable,
leaving the Density field uninitialized. If a format error is
encountered in the line, the Success parameter is set to False and
processing of the source file stops; if no format error is encountered,
Success is set to True. }
procedure ReadCountyData (var Source: Text; var Legend: County;
var Success: Boolean);
label
99;
{ Exit from the procedure when an error has been detected. }
const
PopulationWidth = 6;
{ the number of columns occupied by the population of a county,
on one line of the source file }
GutterWidth = 3;
{ the number of spaces separating adjacent fields on a line of
the source file }
AreaWidth = 3;
{ the number of columns occupied by the area of a county, on one line
of the source file }
var
AreaAsNatural: Natural;
{ the area of a county, as read from the source file in the
natural-number (unsigned integer) format }
begin
with Legend do begin
ReadName (Source, Name, Success); { columns 1-13 }
if not Success then
goto 99;
Match (Source, Space, GutterWidth, Success); { columns 14-16 }
if not Success then
goto 99;
ReadFixedWidthNatural (Source, PopulationWidth, Population, Success);
{ columns 17-22 }
if not Success then
goto 99;
Match (Source, Space, GutterWidth, Success); { columns 23-25 }
if not Success then
goto 99;
ReadFixedWidthNatural (Source, AreaWidth, AreaAsNatural, Success);
{ columns 26-28 }
if not Success then
goto 99;
if AreaAsNatural = 0 then { An area of 0 is an error. } Success := False
else if EOLn (Source) then begin
Area := AreaAsNatural; { Convert the area to a Real. }
ReadLn (Source)
end
else
Success := False { The line should end at column 28. }
end;
99:
end;
{ The ReadStateData procedure reads in the data about all the counties
in the state, one at a time. It reports any error that is encountered
in the source file, either in the format of the line for any county,
or in the total number of counties described. }
procedure ReadStateData (var Source: Text; var Legend: State;
var Success: Boolean);
var
Index: Natural;
begin
Index := 0;
Success := True;
while Success and not EOF (Source) and
(Index < NumberOfCounties) do begin
Index := Index + 1;
ReadCountyData (Source, Legend[Index], Success);
if not Success then
WriteLn ('An error was detected in line ', Index : 1,
' of the source file.')
end;
if Success then begin
if not EOF (Source) then begin
WriteLn ('The source file contained more than ',
NumberOfCounties : 1, ' lines.');
Success := False
end
else if Index < NumberOfCounties then begin
WriteLn ('The source file contained fewer than ',
NumberOfCounties : 1, ' lines.');
Success := False
end
end
end;
{ The ComputeDensities procedure traverses the array of counties,
computing the population density of each one and storing it in the
Density field. }
procedure ComputeDensities (var Dataset: State);
var
Index: Natural;
{ counts off the elements of the Dataset array }
begin
for Index := 1 to NumberOfCounties do
with Dataset[Index] do
Density := Population / Area
end;
{ The SortByDensity procedure rearranges the counties in the Dataset
array into descending order by population density, using the
selection sorting algorithm: Each position in the array in turn,
from first to last, is filled by whichever of the previously
unselected array elements has the highest population density. }
procedure SortByDensity (var Dataset: State);
var
ToFill: Natural;
{ the next array position to be filled }
GreatestDensity: Real;
{ the highest of the population densities of the counties so far
examined on one pass through the previously unselected elements }
PositionOfGreatest: Natural;
{ the position currently occupied by the element that has that
highest population density }
ToTest: Natural;
{ runs through the positions of the previously unselected array
elements }
Temporary: County;
{ temporary storage for an array element to be swapped with the
element of highest population density }
begin
for ToFill := 1 to NumberOfCounties - 1 do begin
{ Find the previously unselected element that has the highest
population density. }
GreatestDensity := Dataset[ToFill].Density;
PositionOfGreatest := ToFill;
for ToTest := ToFill + 1 to NumberOfCounties do
if GreatestDensity < Dataset[ToTest].Density then begin
GreatestDensity := Dataset[ToTest].Density;
PositionOfGreatest := ToTest
end;
{ Swap it with the element in position ToFill. }
Temporary := Dataset[ToFill];
Dataset[ToFill] := Dataset[PositionOfGreatest];
Dataset[PositionOfGreatest] := Temporary
end
end;
{ The WriteCountyData procedure writes out, to a specified output file,
the name and population density of a particular county, labelling it
with the county's rank in the state.
The prescribed format allocates only seven columns for the population
density, even though it may require as many as nine, given the weak
constraints on the input. (The population could be 999999 and the
area 1, leading to a population density of 999999.00.) This procedure
uses extra columns if necessary, but sets FormatError to True when
doing so (otherwise FormatError is set to False). }
procedure WriteCountyData (var Target: Text; Scribend: County;
Rank: Natural; var FormatError: Boolean);
const
RankWidth = 2;
{ the number of columns in which the county's rank is to be printed
in the output file }
GutterWidth = 2;
{ the number of spaces separating adjacent fields on a line of
the output file }
DensityWidth = 7;
{ the number of columns in which the county's population density is
to be printed in the output file }
DensityDecimals = 2;
{ the number of decimal places of the county's population density
that are to be printed in the output file }
DensityFormatBound = 9999.995;
{ A population density greater than or equal to this value cannot
be printed in a field of the specified format, so a format error
should be signalled. }
begin
with Scribend do begin
WriteLn (Target, Rank : RankWidth, { columns 1-2 }
'. ', { columns 3-4 }
Name, { columns 5-17 }
Space : GutterWidth, { columns 18-19 }
Density : DensityWidth : DensityDecimals);
{ columns 20-26 }
FormatError := (DensityFormatBound <= Density)
end
end;
{ The WriteStateData procedure writes out the desired facts about each
county in the state. }
procedure WriteStateData (var Target: Text; Scribend: State);
var
Index: Natural;
{ runs through the positions in the array of counties }
FormatError: Boolean;
{ indicates whether a formatting error occurred when a line was
printed }
begin
for Index := 1 to NumberOfCounties do begin
WriteCountyData (Target, Scribend[Index], Index, FormatError);
if FormatError then
WriteLn ('Line ', Index : 1, ' of the output file could not be ',
'printed in the specified format.')
end
end;
begin { main program }
Reset (Source, SourceFileName);
ReadStateData (Source, Iowa, SuccessfulRead);
if SuccessfulRead then begin
ComputeDensities (Iowa);
SortByDensity (Iowa);
Rewrite (Target, TargetFileName);
WriteStateData (Target, Iowa)
end
else
WriteLn ('Since the source file contained an error, no output ',
'file was constructed.')
end.
created September 13, 1996
last revised September 13, 1996
John David Stone
(stone@math.grin.edu)