Solution to exercise #1

{ 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)