Previous | Next | Table of Contents | Index | Program List | Copyright

12.5 System Structures: Geometric Figures

In this section we introduce a package to represent, read, and display various geometric figures, including their areas and perimeters. We need to provide first a representation scheme for geometric figures, with a useful set of operations and second, a means for interactive users to read and display these figures. As in other ADTs we have developed, it is useful to separate these two concerns.

We first develop an abstract data type that allows a client program to construct a geometric figure. The characteristics for a circle are different from those for a rectangle (a square is a rectangle whose width and height are equal), so we use a record with a variant part. In this case, the fixed part of the record will contain its area and perimeter, which are computed automatically as the figure is constructed. Here is the variant type Figure:

    SUBTYPE NonNegFloat IS Float RANGE 0.0 .. Float'Last;
    TYPE FigKind IS ( Rectangle, Square, Circle);
    
    TYPE Figure ( FigShape : FigKind := Rectangle) IS RECORD
      Area : NonNegFloat := 0.0;
      Perimeter : NonNegFloat := 0.0;
      CASE FigShape IS
        WHEN Rect | Square =>
          Width : NonNegFloat := 0.0;
          Height : NonNegFloat := 0.0;
        WHEN Circle =>
          Radius : NonNegFloat := 0.0;
      END CASE;
    END RECORD;

Implementing the Specification of Geometry

The package specification appears as Program 12.4.

Program 12.4
Specification for Geometry Package

PACKAGE Geometry IS
------------------------------------------------------------------------
--| Defines an abstract data type for a geometric figure. 
--| Operations include constructors for rectangles, circles, 
--| and squares, and selectors for width, height, side,
--| area and perimeter.
--| Author: Michael B. Feldman, The George Washington University
--| Last Modified: September 1995
------------------------------------------------------------------------
  
  -- Data Types
  
  SUBTYPE NonNegFloat IS Float RANGE 0.0 .. Float'Last;
  TYPE FigKind IS (Rectangle, Square, Circle);

  TYPE Figure (FigShape : FigKind := Rectangle) IS PRIVATE;

  -- Exported Exception

  ShapeError: EXCEPTION;
 
  -- Constructor Operations

  FUNCTION MakeRectangle (Width, Height : NonNegFloat) RETURN Figure;
  -- Pre : Width and Height are defined
  -- Post: returns a rectangle

  FUNCTION MakeCircle (Radius : NonNegFloat) RETURN Figure;
  -- Pre : Radius is defined
  -- Post: returns a circle

  FUNCTION MakeSquare (Side : NonNegFloat) RETURN Figure;
  -- Pre : Side is defined
  -- Post: returns a square

  -- selectors
  FUNCTION Shape     (OneFig : Figure) RETURN FigKind;
  FUNCTION Height    (OneFig : Figure) RETURN NonNegFloat;
  FUNCTION Width     (OneFig : Figure) RETURN NonNegFloat;
  FUNCTION Radius    (OneFig : Figure) RETURN NonNegFloat;
  FUNCTION Side      (OneFig : Figure) RETURN NonNegFloat;
  FUNCTION Perimeter (OneFig : Figure) RETURN NonNegFloat;
  FUNCTION Area      (OneFig : Figure) RETURN NonNegFloat;
  -- Pre   : OneFig is defined.
  -- Post  : Returns the appropriate characteristic
  -- Raises: ShapeError if the requested characteristic is
  --         undefined for the shape of OneFig

PRIVATE

  TYPE Figure (FigShape : FigKind := Rectangle) IS RECORD
    Area : NonNegFloat := 0.0;
    Perimeter : NonNegFloat := 0.0;
    CASE FigShape IS
      WHEN Rectangle | Square =>
        Width : NonNegFloat := 0.0;
        Height : NonNegFloat := 0.0;
      WHEN Circle =>
        Radius : NonNegFloat := 0.0;
    END CASE;
  END RECORD;

END Geometry;

We have defined the data type Figure as a PRIVATE type. Why? If the client program had access to the details of the record representing the figure, it could, for example, change the Perimeter field by simply plugging in a new number. Because the figure would no longer make geometric sense, this action would violate the abstraction. Note the syntax for declaring a PRIVATE type with a variant: The discriminant appears first in the partial declaration and later in the complete declaration in the PRIVATE part of the specification.

The following design decisions make the data type safe from accidental misuse:

  1. The data type is declared PRIVATE to keep client programs from prying into, and changing, fields of the record, such as the area and the perimeter, or changing the length of the side without changing the area and perimeter fields accordingly.
  2. All fields of the type are initialized to 0.0 by default so that every variable of the type is automatically well defined (a figure with sides of 0.0 also has area and perimeter of 0.0).
  3. The area and perimeter are calculated automatically when the figure is constructed because these are uniquely determined by the other characteristics.

The operations in the package are three constructors, MakeRecangle, MakeCircle, and MakeSquare, which construct the appropriate variant given the relevant characteristics, and a set of selectors Shape, Width, Height, Side, Radius, Area, and Perimeter, which return these characteristics of the figure. Note that even though a square and a rectangle use the same variant, the constructors and selectors are different for them. Also, we export an exception ShapeError to prevent a client from applying an inappropriate selector (e.g., finding the radius of a square).

A client program can declare variables of type Figure in either constrained or unconstrained form. The variable

    SomeShape : Figure;
can hold, at different moments, a circle, a square, or a rectangle; it is unconstrained. However,
    BigSquare : Figure (FigShape => Square);
can hold only a square, because it is constrained; that is, we plugged a discriminant value into the declaration of the variable and are now "locked in" to that value.

Implementing the Package Body

Program 12.5 shows the package body for Geometry.

Program 12.5
Body of Geometry Package

WITH Ada.Numerics; USE Ada.Numerics;
PACKAGE BODY Geometry IS
------------------------------------------------------------------------
--| Body of abstract data type package for geometric figures.
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: September 1995                                     
------------------------------------------------------------------------

-- Body of abstract data type package for geometric figures.

  -- internal functions, not exported to client. ComputePerimeter
  -- and ComputeArea are used to ensure that all figures are
  -- constructed with these attributes automatically inserted.
  -- The exported selectors Perimeter and Area assume that these
  -- fields have been set by the internal functions.

  FUNCTION ComputePerimeter (OneFig : Figure) RETURN NonNegFloat IS
  -- Pre : The discriminant and characteristics of OneFig are defined.
  -- Post: Returns Perimeter of OneFig.

  BEGIN -- ComputePerimeter   
    
    CASE OneFig.FigShape IS
      WHEN Rectangle =>
        RETURN 2.0 * (OneFig.Width + OneFig.Height);
      WHEN Square =>
        RETURN 4.0 * OneFig.Width;
      WHEN Circle =>
        RETURN 2.0 * Pi * OneFig.Radius;
    END CASE;
    
  END ComputePerimeter;

  FUNCTION ComputeArea (OneFig : Figure) RETURN NonNegFloat IS
  -- Pre : The discriminant and characteristics of OneFig are defined.
  -- Post: Returns Area of OneFig.
    
  BEGIN -- ComputeArea   
    
    CASE OneFig.FigShape IS
      WHEN Rectangle =>
        RETURN OneFig.Width * OneFig.Height;
      WHEN Square =>
        RETURN OneFig.Width ** 2;
      WHEN Circle =>
        RETURN Pi * OneFig.Radius ** 2 ;
    END CASE;

  END ComputeArea;

  -- Exported Operations  

  FUNCTION MakeRectangle (Width, Height : NonNegFloat) RETURN Figure IS

    Result : Figure(FigShape => Rectangle);

  BEGIN -- MakeRectangle

    Result.Height    := Height;
    Result.Width     := Width; 
    Result.Area      := ComputeArea(Result);
    Result.Perimeter := ComputePerimeter(Result);

    RETURN Result;

  END MakeRectangle;

  FUNCTION MakeCircle (Radius : NonNegFloat) RETURN Figure IS

    Result: Figure (FigShape => Circle);

  BEGIN -- MakeCircle

    Result.Radius    := Radius;
    Result.Area      := ComputeArea(Result);
    Result.Perimeter := ComputePerimeter(Result);

    RETURN Result;

  END MakeCircle;

  FUNCTION MakeSquare (Side : NonNegFloat) RETURN Figure IS

    Result: Figure (FigShape => Square);

  BEGIN -- MakeSquare

    Result.Height    := Side;
    Result.Width     := Side;
    Result.Area      := ComputeArea(Result);
    Result.Perimeter := ComputePerimeter(Result);

    RETURN Result;

  END MakeSquare;

  FUNCTION Shape (OneFig : Figure) RETURN FigKind IS

  BEGIN -- Perimeter   
    RETURN OneFig.FigShape; 
  END Shape;    

  FUNCTION Perimeter (OneFig : Figure) RETURN NonNegFloat IS

  BEGIN -- Perimeter   
    RETURN OneFig.Perimeter;
  END Perimeter;

  FUNCTION Area (OneFig : Figure) RETURN NonNegFloat IS

  BEGIN -- Area   
    RETURN OneFig.Area;
  END Area;

  FUNCTION Height (OneFig : Figure) RETURN NonNegFloat IS

  BEGIN -- Height
    CASE OneFig.FigShape IS
      WHEN Rectangle | Square =>
        RETURN OneFig.Height;
      WHEN OTHERS =>
        RAISE ShapeError;
    END CASE;
  END Height;

  FUNCTION Width (OneFig : Figure) RETURN NonNegFloat IS

  BEGIN -- Width 
    CASE OneFig.FigShape IS
      WHEN Rectangle | Square =>
        RETURN OneFig.Width; 
      WHEN OTHERS =>
        RAISE ShapeError;
    END CASE;
  END Width; 

  FUNCTION Side (OneFig : Figure) RETURN NonNegFloat IS

  BEGIN -- Side  
    CASE OneFig.FigShape IS
      WHEN  Square =>
        RETURN OneFig.Height;
      WHEN OTHERS =>
        RAISE ShapeError;
    END CASE;
  END Side;  

  FUNCTION Radius (OneFig : Figure) RETURN NonNegFloat IS

  BEGIN -- Radius
    CASE OneFig.FigShape IS
      WHEN Circle =>
        RETURN OneFig.Radius;
      WHEN OTHERS =>
        RAISE ShapeError;
    END CASE;
  END Radius;

END Geometry;

The constructor functions create the appropriate variant of the record from the relevant components, then calculate the area and perimeter. Local functions ComputeArea and ComputePerimeter are used to assist. These are not given in the specification. The user can find out the area and perimeter by calling the appropriate selector, whose code is straightforward. Note that even though a square is also a rectangle, we distinguish between them in many of the operations. Note in many of these operations how a CASE statement is used to control the processing of the variant data.

The Child Package Geometry.IO

As in earlier ADTs, we separate the input/output operations into a child package. Program 12.6 and Program 12.7 give the specification and body for Geometry.IO. Procedure Get reads in the enumeration value denoting the kind of figure, reads the data required for the kind of figure indicated by the discriminant field, and calls the appropriate constructor. This procedure serves as a good example of how to read a variant record from the interactive user. As before, in the Get and Put procedures, a CASE statement controls the processing of the data in the variant part.

Program 12.6
Specification for Geometry.IO Child Package

PACKAGE Geometry.IO IS
------------------------------------------------------------------------
--| Child Package: Input/Output for Geometric Figures
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: September 1995                                     
------------------------------------------------------------------------

  PROCEDURE Get (Item : OUT Geometry.Figure);
  -- Pre : None
  -- Post: Item  contains a geometric figure.

  PROCEDURE Put (Item : IN Geometry.Figure);
  -- Pre : Item is defined.
  -- Post: Item is displayed.

END Geometry.IO;

Program 12.7
Body of Geometry.IO Child Package

WITH Ada.Float_Text_IO;
WITH Ada.Text_IO;
WITH Robust_Input;
PACKAGE BODY Geometry.IO IS
------------------------------------------------------------------------
--| Body of Input/Output Child Package for Geometric Figures
--| Author: Michael B. Feldman, The George Washington University
--| Last Modified: September 1995            
------------------------------------------------------------------------

  MaxSize: CONSTANT NonNegFloat := 1_000_000.0;
  
  PACKAGE FigKind_IO IS 
    NEW Ada.Text_IO.Enumeration_IO (Enum => FigKind);

  -- Local procedure ReadShape and RobustGet are used only within 
  -- the package, therefore not exported.

  PROCEDURE ReadShape (Item : OUT FigKind) IS 
  -- Pre:  none
  -- Post: Item contains a figure kind. ReadShape reads robustly.
  
    TempItem: FigKind;

  BEGIN -- ReadShape
            
    LOOP
      BEGIN
        Ada.Text_IO.Put
          (Item => "Enter a shape: rectangle, circle, square > ");
        FigKind_IO.Get(Item => TempItem);
        Item := TempItem;
        EXIT;
      EXCEPTION
        WHEN Ada.Text_IO.Data_Error =>
          Ada.Text_IO.Put 
            ("Value not a valid shape. Please try again.");
          Ada.Text_IO.New_Line;
          Ada.Text_IO.Skip_Line;
      END;
    END LOOP;
    -- assert: Item is rect, circle, or square

  END ReadShape;

  PROCEDURE Get (Item : OUT Figure) IS

    Shape  : FigKind;
    Height : NonNegFloat;
    Width  : NonNegFloat;
    Side   : NonNegFloat;
    Radius : NonNegFloat;

  BEGIN  -- Get   

    -- Read the shape character and define the discriminant
    ReadShape(Shape);

    -- Select the proper variant and read pertinent data   
    CASE Shape IS
      WHEN Rectangle =>
        Ada.Text_IO.Put(Item => "Enter width.");
        Ada.Text_IO.New_Line;
        Robust_Input.Get
          (Item => Width, MinVal => 0.0, MaxVal => MaxSize);   
        Ada.Text_IO.Put(Item => "Enter height.");
        Ada.Text_IO.New_Line;
        Robust_Input.Get
          (Item => Height, MinVal => 0.0, MaxVal => MaxSize);
        Item := MakeRectangle(Width, Height);
   
      WHEN Square    =>
        Ada.Text_IO.Put(Item => "Enter length of side.");
        Ada.Text_IO.New_Line;
        Robust_Input.Get
          (Item => Side, MinVal => 0.0, MaxVal => MaxSize);   
        Item := MakeSquare(Side);

      WHEN Circle    =>
        Ada.Text_IO.Put(Item => "Enter circle radius.");
        Ada.Text_IO.New_Line;
        Robust_Input.Get
          (Item => Radius, MinVal => 0.0, MaxVal => MaxSize);
        Item := MakeCircle(Radius);

    END CASE;

  END Get;
    
  PROCEDURE Put (Item: IN Figure) IS
    
  BEGIN -- DisplayFigure   
    
    -- Display shape and characteristics   
    Ada.Text_IO.Put(Item => "Figure shape: ");
    FigKind_IO.Put(Item => Shape(Item), Width => 1);
    Ada.Text_IO.New_Line;

    CASE Item.FigShape IS
      WHEN Rectangle =>
        Ada.Text_IO.Put(Item => "height = ");
        Ada.Float_Text_IO.Put
          (Item => Height(Item), Fore=>1, Aft=>2, Exp=>0);
        Ada.Text_IO.Put(Item => "; width = ");
        Ada.Float_Text_IO.Put
          (Item => Width(Item), Fore=>1, Aft=>2, Exp=>0);

      WHEN Square =>
        Ada.Text_IO.Put(Item => "side = ");
        Ada.Float_Text_IO.Put
          (Item => Height(Item), Fore=>1, Aft=>2, Exp=>0);
        
      WHEN Circle =>
        Ada.Text_IO.Put(Item => "radius = ");
        Ada.Float_Text_IO.Put
          (Item => Radius(Item), Fore=>1, Aft=>2, Exp=>0);
        
    END CASE;

    Ada.Text_IO.Put(Item => "; perimeter = ");
    Ada.Float_Text_IO.Put
      (Item => Perimeter(Item), Fore=>1, Aft=>2, Exp=>0);
    Ada.Text_IO.Put(Item => "; area = ");
    Ada.Float_Text_IO.Put(Item => Area(Item), Fore=>1, Aft=>2, Exp=>0);
    Ada.Text_IO.New_Line;

  END Put;

END Geometry.IO;

Program 12.8 shows a brief and straightforward test program for the package.

Program 12.8
Demonstration of Geometry Package

WITH Ada.Text_IO;
WITH Ada.Integer_Text_IO;
WITH Geometry;
WITH Geometry.IO;
PROCEDURE Test_Geometry IS
------------------------------------------------------------------------
--| Program to test package Geometry
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: September 1995                                     
------------------------------------------------------------------------

  MyFig : Geometry.Figure;                 -- a figure   

BEGIN -- Test_Geometry   

  FOR TestTrial IN 1..3 LOOP

    Ada.Text_IO.New_Line;
    Ada.Text_IO.Put(Item => "     Trial #");
    Ada.Integer_Text_IO.Put(Item => TestTrial, Width => 1);
    Ada.Text_IO.New_Line;
    Geometry.IO.Get (Item => MyFig);
    Geometry.IO.Put (Item => MyFig);

  END LOOP;

END Test_Geometry;
Sample Run
     Trial #1
Enter a shape: rectangle, circle, square > triangle
Value not a valid shape. Please try again.
Enter a shape: rectangle, circle, square > rect
Value not a valid shape. Please try again.
Enter a shape: rectangle, circle, square > rectangle
Enter width.
Enter a floating-point value between 0.00 and 1000000.00 > 3
Enter height.
Enter a floating-point value between 0.00 and 1000000.00 > 5
Figure shape: RECTANGLE
height = 5.00; width = 3.00; perimeter = 16.00; area = 15.00

     Trial #2
Enter a shape: rectangle, circle, square > circle
Enter circle radius.
Enter a floating-point value between 0.00 and 1000000.00 > 4
Figure shape: CIRCLE
radius = 4.00; perimeter = 25.13; area = 50.27

     Trial #3
Enter a shape: rectangle, circle, square > square
Enter length of side.
Enter a floating-point value between 0.00 and 1000000.00 > 5
Figure shape: SQUARE
side = 5.00; perimeter = 20.00; area = 25.00

Exercises for Section 12.5

Programming

  1. Add the variant
        RightTriangle : (Base, Height : Real); 
    
    to Figure and modify the operators to include triangles. Use the formulas
        area = 1/2 base × height
                       _______________
        hypotenuse = \/base2 + height2
    

    where base and height are the two sides that form the right angle.


Previous | Next | Table of Contents | Index | Program List | Copyright

Copyright © 1996 by Addison-Wesley Publishing Company, Inc.