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

8.7 Problem Solving: Using Arrays

We have written programs that accumulate the sum of all input data items in a single variable. Often, we have different categories of data items, and we might want to accumulate a separate total for each category rather than lump all items together. The problem that follows uses an array to accomplish this. The problem illustrates both random and sequential access of array elements.


Case Study: Home Budget Problem

PROBLEM SPECIFICATION

Your parents want a program that keeps track of their monthly expenses in each of several categories. The program should read each expense amount, add it to the appropriate category total, and print the total expenditure by category. The input data consists of the category number and amount of each purchase made during the past month.

ANALYSIS

Your parents have selected these budget categories: entertainment, food, clothing, rent, tuition, insurance, and miscellaneous. Seven separate totals are to be accumulated; each total can be associated with a different element of a seven-element array. The program must read each expenditure, determine to which category it belongs, and then add that expenditure to the appropriate array element. When done with all expenditures, the program can print a table showing each category and its accumulated total. As in all programs that accumulate a sum, each total must be initialized to zero. The problem inputs and outputs follow.

Data Type

    TYPE BudgetCat IS ( Entertainment, Food, Clothing, Rent,
	Tuition, Insurance, Miscellaneous);

Problem Inputs

each expenditure and its category

Problem Outputs

the array of seven expenditure totals (Budget)

DESIGN

Algorithm

1. Initialize all category totals to zero.

2. Read each expenditure and add it to the appropriate total.

3. Display the accumulated total for each category.

The structure chart in Figure 8.8 shows the relationship between the three steps.

Figure 8.8
Structure Chart for Home Budget Problem

Figure 8.8

The array Budget is manipulated by all three procedures in the program solution. Procedure Initialize sets up the array; procedure Post stores expenditure information in this array (in accounting terminology, registering a transaction is often called posting); this information is displayed by procedure Report.

TEST PLAN

Testing the program with the following transactions:

C	25.00
M	25.00
C	15.00
E	675.00
Q
should give the following output:
 Category      Expense

ENTERTAINMENT    675.00
FOOD               0.00
CLOTHING          40.00
RENT               0.00
TUITION            0.00
INSURANCE          0.00
MISCELLANEOUS     25.00
Also you need to design more test cases; we leave it to you to develop and justify these.

IMPLEMENTATION

The main program is shown, along with several procedures in Program 8.9.

Program 8.9
Home Budget Program

WITH Ada.Text_IO;
WITH Ada.Float_Text_IO;
WITH Screen;
PROCEDURE Home_Budget IS
------------------------------------------------------------------------
--| Prints a summary of all expenses by budget category.   
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: July 1995                                     
------------------------------------------------------------------------

  MaxExpense :  CONSTANT Float := 10_000.00       -- max expense amount

  TYPE Categories IS (Entertainment, Food, Clothing, Rent,
                      Tuition, Insurance, Miscellaneous);
  TYPE Commands IS (E, F, C, R, T, I, M, Q);

  PACKAGE Category_IO IS 
    NEW Ada.Text_IO.Enumeration_IO(Enum => Categories);
  PACKAGE Command_IO  IS 
    NEW Ada.Text_IO.Enumeration_IO(Enum => Commands);

  SUBTYPE Expenses IS Float RANGE 0.00..MaxExpense;     -- expense type   
  TYPE BudgetArray IS ARRAY (Categories) OF Expenses;   -- array type   

  Budget : BudgetArray;                 -- array of ten totals   

  PROCEDURE Initialize (Budget : OUT BudgetArray) IS
  -- Pre:  None
  -- Post: Each array element Budget(Category) is 0.00
     
  BEGIN  -- Initialize   

    Budget := (OTHERS => 0.00);

  END Initialize;


  PROCEDURE DisplayTitles IS
  -- Pre:  None
  -- Post: Displays a list of expense categories with abbreviations

    WhichRow: Screen.Depth;

  BEGIN

    Screen.ClearScreen;

    Screen.MoveCursor(Row => 3, Column => 20);
    Ada.Text_IO.Put(Item => "Expense Categories");
    Ada.Text_IO.New_Line;
    Ada.Text_IO.New_Line;

    WhichRow := 5;
    FOR C IN Commands'First..Commands'Pred(Commands'Last) LOOP
      Screen.MoveCursor(Row => WhichRow, Column => 20);
      Command_IO.Put(Item => C, Width => 3);
      Category_IO.Put(Item => Categories'Val(Commands'Pos(C)));
      WhichRow := WhichRow + 1;
    END LOOP;

    Screen.MoveCursor(Row => WhichRow, Column => 20);
    Command_IO.Put(Item => Commands'Last, Width => 3);
    Ada.Text_IO.Put(Item => "when data entry is completed");

  END DisplayTitles;

  PROCEDURE GetCommand(Command: OUT Commands) IS
  -- Pre:  None
  -- Post: A valid Command is returned in Command

  BEGIN

    LOOP
      BEGIN    -- exception handler block
        Screen.MoveCursor(Row => 18, Column => 15);
        Ada.Text_IO.Put("Please enter first letter of category > ");
        Command_IO.Get(Item => Command);
        Screen.MoveCursor(Row => 19, Column => 15);
        Ada.Text_IO.Put("Category accepted, thank you");
        EXIT;
      EXCEPTION
        WHEN Ada.Text_IO.Data_Error =>
          Screen.Beep;
          Screen.MoveCursor(Row => 19, Column => 15);
          Ada.Text_IO.Put("Sorry, invalid category!    ");
          Ada.Text_IO.Skip_Line;
      END;     -- exception handler block
    END LOOP;
    -- assert: valid command input received

  END GetCommand;

  PROCEDURE GetExpense(Expense: OUT Expenses) IS
  -- Pre:  None
  -- Post: a valid Expense is returned in Expense

  BEGIN

    LOOP
      BEGIN    -- exception handler block
        Screen.MoveCursor(Row => 20, Column => 15);
        Ada.Text_IO.Put
          ("Please enter expense as floating point number > ");
        Ada.Float_Text_IO.Get(Item => Expense);
        Screen.MoveCursor(Row => 21, Column => 15);
        Ada.Text_IO.Put("Expense accepted, thank you");
        EXIT;
      EXCEPTION
        WHEN Ada.Text_IO.Data_Error =>
          Screen.Beep;
          Screen.MoveCursor(Row => 21, Column => 15);
          Ada.Text_IO.Put("Sorry, invalid expense!    ");
          Ada.Text_IO.Skip_Line;
      END;     -- exception handler block
    END LOOP;
    -- assert: valid expense received

  END GetExpense;

  PROCEDURE Post (Budget : IN OUT BudgetArray) IS
  -- Pre:  Each array element Budget(c) is 0.0
  -- Post: Each array element Budget(c) is the sum of expense
  --        amounts for category c.
     
    Sentinel : CONSTANT Commands := Q; -- sentinel command    

    NextCommand :  Commands;          -- command
    NextCategory : Categories;        -- expenditure category   
    NextExpense :  Expenses;          -- expenditure amount   

  BEGIN  -- Post   

    LOOP
      -- invariant:
      -- no prior value of NextCommand  is Sentinel
         
      GetCommand(Command => NextCommand);
      EXIT WHEN NextCommand = Sentinel;

      NextCategory := Categories'Val(Commands'Pos(NextCommand));
      GetExpense(Expense => NextExpense);
      Budget(NextCategory) := Budget(NextCategory) + NextExpense;
    END LOOP;   

  END Post;

  PROCEDURE Report (Budget : IN BudgetArray) IS
  -- Pre:  Each array element Budget(c) is assigned a value.
  -- Post: Each array element Budget(c) is displayed.

    WhichRow: Screen.Depth;
     
  BEGIN  -- Report   

    Screen.ClearScreen;

    Screen.MoveCursor(Row => 3, Column => 20);
    Ada.Text_IO.Put(Item => "Category      Expense");
    Ada.Text_IO.New_Line;
    Ada.Text_IO.New_Line;

    WhichRow := 5;
    FOR Category IN Categories LOOP
      Screen.MoveCursor(Row => WhichRow, Column => 20);
      Category_IO.Put(Item => Category, Width=>13);   -- Print row   
      Ada.Float_Text_IO.Put(Item=>Budget(Category), 
                    Fore=>7, Aft=>2, Exp=>0);  
      WhichRow := WhichRow + 1;
    END LOOP;

    Screen.MoveCursor(Row => 23,Column => 1);

  END Report;

BEGIN  -- Home_Budget   

  -- prepare terminal screen for data entry
  DisplayTitles;

  -- Initialize array Budget to all zeros.   
  Initialize (Budget);

  -- Read and process each expenditure.   
  Post (Budget);

  -- Print the expenditures in each category.   
  Report (Budget);

END Home_Budget;

The array Budget (type BudgetArray) appears in each parameter list shown in Program 8.9 and is passed between each procedure and the main program. When passing an entire array, no subscript is used. We will have more to say about the use of arrays as parameters later.

In procedure Initialize, the array Budget is set to zeroes using an aggregate. In procedure Report, the statements

    Category_IO.Put(Item=>Category, Width=>13);
    Ada.Float_Text_IO.Put(Item=>Budget(Category), Fore=>10, Aft=>2, Exp=>0);
are used to print each category and its associated total. Category_IO is, as usual, an instantiation of Ada.Text_IO.Enumeration_IO.

Procedure Post must read each expenditure and add it to the appropriate array element. To read a category, we create an enumeration type for the first letters of the categories so that the user does not have to type the entire category name:

    TYPE Commands IS (E, F, C, R, T, I, M, Q);
where the type includes an extra command Q which will signal the user's desire to quit the program. Post uses the procedures GetCommand and GetExpense to read valid commands and expenditure values into NextCommand and NextExpense, respectively. The package Screen is used to position the screen cursor properly to require the user to enter input at the bottom of the screen and to send a beep in case of invalid input.

A valid command (other than Q) needs to be converted into a category in order to determine the proper array element to update. This is done with the statement

    NextCategory := Categories'Val(Commands'Pos(NextCommand));
The assignment statement
    Budget(NextCategory) := Budget(NextCategory) + Expense;
adds the expense amount to whichever element of array Budget is selected by the subscript NextCategory.

TESTING

As illustrated in the test transactions, it is not necessary for the data to be in order by category. Otherwise, we leave testing to you as an exercise.


PROGRAM STYLE
Allowing for Array Expansion; Designing for Reuse

The enumeration types Categories and Commands are used throughout Program 8.9 to represent the budget categories. This enables us to extend the program easily to handle more budget categories, or to change the entire set of categories, by just rewriting the two types and recompiling the program. Nothing else in the program needs to be changed.

Sequential Versus Random Access to Arrays

Program 8.9 illustrates two common ways of selecting array elements for processing. Often, we need to manipulate all elements of an array in some uniform manner (as in Example 8.8). In situations like this, it makes sense to process the array elements in sequence (sequential access), starting with the first and ending with the last. In procedures Initialize and Report, this is accomplished by using a FOR loop whose loop control variable is also the array subscript.

In procedure Post the order in which the array elements are accessed is completely dependent on the order of the data. The value assigned to NextCategory determines which element is incremented. This is called random access because the order is not predictable beforehand.

Copying and Comparing Arrays

A third way of manipulating an array--accessing the entire array at once--is provided by the assignment (:=), equality (=), and inequality (/=) operations. As in the case of records, it is possible to assign the entire contents of one array to another array provided the arrays are compatible. Arrays follow the same compatibility rules as scalars and records do: Two arrays are compatible if their type names are identical or they are subtypes of the same type. Given the declarations

    MaxSize : CONSTANT Positive := 100;
    SUBTYPE Index IS Positive RANGE 1..MaxSize;
    TYPE TestArray IS ARRAY (Index) OF Float;
    W : TestArray;
    X : TestArray;
    Y : TestArray;
the assignment statement
    X := Y;
copies each value in array Y to the corresponding element of array X (i.e. Y( 1) is copied to X( 1), Y( 2) to X( 2), etc.). Furthermore, the use of = and /= in the fragment
    IF X = Y THEN
    	Ada.Text_IO.Put(Item => "Arrays X and Y are equal");
    ELSIF W /= Y THEN
    	Ada.Text_IO.Put(Item => "Arrays W and Y are unequal");
    END IF;
is quite correct. The additional declaration
    Z : ARRAY (Index) OF Float;
happens to be correct Ada, although we recommend against its use. Z is declared directly as an array, instead of as a variable of an array type as X and Y were. The Ada compiler, which tries to establish the type name of every variable, will give Z an internal type name, which our program cannot know. The assignment statements
    Z := Y;                  -- invalid array copy 
    X := Z;                  -- invalid array copy
and the IF fragment
    IF Z = X THEN ...
are illegal and result in compilation errors. Even though array Z has the same structure as arrays X and Y, the type of array Z is anonymous and is not compatible with the type of arrays X and Y (type TestArray). Note that the elements of Z are compatible with the elements of X and Y (they are all Float), and therefore assignments such as
    Z( 3) := Y( 5);
    X( 9) := Z( 1);
and comparisons such as
    IF X( 9) = Z( 1) THEN...
    IF Y( 3) /= Z( 5) THEN ...
are legal.

PROGRAM STYLE
Avoiding Anonymous Array Types

Because using anonymous (unnamed) array types causes difficulties with array assignment and comparison, the use of anonymous (unnamed) types should be avoided, and we avoid it in this book.

Arrays as Parameters

If several elements of an array are being manipulated by a procedure, it is generally better to pass the entire array of data instead of individual array elements. In Program 8.9, the procedure call statements

    Initialize ( Budget);
    Post ( Budget); 
    Report ( Budget);
pass the entire array Budget to each procedure. Budget is declared as an OUT parameter in procedure Initialize, an IN OUT parameter in Post, and an IN parameter in procedure Report. The rules for manipulating array parameters in a procedure are similar to those for manipulating scalar parameters: IN parameters may not be altered by the procedure.

The next two examples illustrate the use of arrays as parameters assuming the declarations below.

    MaxSize : CONSTANT Positive := 5;
    SUBTYPE Index IS Positive RANGE 1..MaxSize;
    TYPE TestArray IS ARRAY (Index) OF Float;
    X, Y, Z : TestArray;

Example 8.10

Although it is possible to use a single assignment statement to copy one array to another, no arithmetic on entire arrays is defined in Ada. For example, the assignment statement

    Z := X + Y;           -- illegal addition of arrays

is invalid because Ada has no predefined operator + that acts on array operands.. Procedure AddArray in Program 8.10 can be used to add two arrays of type TestArray.

Program 8.10
Procedure AddArray

    PROCEDURE AddArray ( A, B  : TestArray; C : OUT TestArray) IS
      -- Stores the sum of A( I) and B( I) in C( I).  Array elements
      -- with subscripts in the range Index are summed, element by element.
      -- Pre:  A( I) and B( I) ( I in range Index) are assigned values
      -- Post: C( I) := A( I) + B( I) ( I in range Index).
      
    BEGIN  -- AddArray   
    
      -- Add corresponding elements of each array   
      FOR I IN Index LOOP
        C( I) := A( I) + B( I);
      END  LOOP;
    
    END AddArray;
The parameter correspondence established by the procedure call statement
    AddArray ( A => X, B => Y, C => Z);

is shown in Fig. 8.9.

Figure 8.9
Parameter Correspondence for AddArray( X, Y, Z)

Figure 8.9

Formal parameter arrays A and B in the procedure correspond to actual arrays X and Y; formal parameter array C corresponds to actual array Z. The procedure results are stored in array Z. After execution of the procedure, Z( 1) will contain the sum of X( 1) and Y( 1), or 3.5; Z( 2) will contain 6.7, and so on. Arrays X and Y will be unchanged.

More on the Rules of Parameter Passing

Recall from Section 6.6 that the values of actual scalar IN OUT parameters are always copied into the corresponding formal parameters in the procedure's data area, and that all the OUT and IN OUT results are copied back into the calling program just before the procedure terminates normally and returns to the caller. The rules for structured parameters are somewhat different. The Ada standard allows structured parameters to be copied, as for scalars, but also allows them to be passed more efficiently, simply by copying the address of the actual parameter into the location of the corresponding formal parameter in the subprogram's data area. The Ada compiler writer can choose which method to use.

If the latter method (usually called call by reference) is used, a modification to an IN OUT parameter will be effective immediately in the calling program, instead of waiting until the procedure returns to its caller. This is because in the latter method, the formal parameter and the actual parameter refer to exactly the same set of locations. Why are the rules different for the scalar and structured cases?

Suppose that an exception is raised in the execution of the procedure and is not handled by an exception handler in that procedure. Ada requires that the procedure terminate abnormally and that the exception be propagated (passed back) to the calling program, which could then handle it with its own handler. In this case the scalar OUT or IN OUT parameters will not be copied back, because the procedure didn't terminate normally. This is usually a good thing: The program writer is sure that the original parameter values, not the new ones, remain in the calling program if the procedure does not run to normal completion.

Why not do the same thing with array and record parameters? Arrays and records can be large, and so copying them to and from subprograms could take a large amount of time and space. Ada therefore gives the compiler the option of just passing the address of a structured parameter. Most Ada compilers pass structured parameters by reference; some compilers pass small ones by copying and large ones by reference to try to get the best of both worlds. Although in theory a user does not know which method is being used, it is reasonably safe to assume that a compiler will use reference passing for large structured parameters, and therefore:

Example 8.11

Procedure Exchange in Program 8.11 exchanges the values of its two type Float parameters.

Program 8.11
Procedure Exchange

PROCEDURE Exchange (P, Q : IN OUT Float) IS
  -- Exchanges the values of P and Q.
  -- Pre:  P and Q are assigned values.
  -- Post: P has the value passed into Q and vice-versa.
  
  Temp : Float;      -- temporary variable for the exchange   

BEGIN  -- Exchange   

  Temp := P;
  P := Q;
  Q := Temp;

END Exchange;

The procedure call statement

    Exchange ( X( 1), X( 2));

uses this procedure to exchange the contents of the first two elements (type Float) of array X. The actual parameter X(1) corresponds to formal parameter P; the actual parameter X(2) corresponds to formal parameter Q. This correspondence is shown in Fig. 8.10 for the array X.

Figure 8.10
Parameter Correspondence for Exchange(X(1),X(2))

Figure 8.10

It is not permitted to use a subscripted variable as a formal parameter. For example, the procedure declaration

    PROCEDURE Exchange ( X( i), X( j) : IN OUT Float);
would cause a compilation error.

PROGRAM STYLE
Choosing the Best Parameter Mode

It is very important to keep in mind that all Ada compilers pass all scalar parameters by copying, and most compilers pass structured parameters by reference, regardless of their mode. This is quite different from the rules of other languages, especially Pascal.

In Pascal, programmers often pass arrays by reference (VAR parameters) to save time and space, even if the array is not going to be modified and is used as a read-only parameter. Therefore, programmers who have Pascal experience often think they should pass all Ada arrays as IN OUT, to be sure they are passed by reference. This is a misunderstanding that leads to poor design: In Ada, arrays are (almost always) passed by reference even if they are IN parameters, and scalars are never passed by reference even if they are IN OUT.

Because of this rule, there is absolutely no efficiency gain in passing an array as an IN OUT parameter when its use is as an IN parameter. It is therefore best to choose the parameter mode that best describes the use of the parameter, not the method by which it is passed.

Exercises for Section 8.7

Self-Check

  1. When is it better to pass an entire array of data to a procedure rather than individual elements?
  2. When is a copy of an entire array made for an array that is a function procedure parameter? What happens to the copy after the procedure executes?
  3. Is it acceptable to modify, within a function, an element of an array that is passed as a parameter to that function? What about a procedure?

Programming

  1. Write a procedure that assigns a value of True to element I of the output array if element I of one input array has the same value as element I of the other input array; otherwise assign a value of False. If the input arrays have subscript type IndexType, the output array should have the type below:
    TYPE BoolArray IS ARRAY(IndexType) OF Boolean;
    


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

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