In this section we develop an ADT for monetary quantities, which we shall call
Currency
. What is important about this ADT is that in writing
operations for Currency
values, we discover that not all
operations make sense. An advantage of the ADT approach is that we can control
the set of operations to allow only meaningful ones to be done.
REQUIREMENTS
We require a way to represent monetary values so as to ensure that calculations with these quantities make sense and are exact. Only sensible operations should be allowed. It is meaningful to compare, add, subtract, and divide monetary quantities but not to multiply them--$4.00/$2.00 is a dimensionless ratio 2.0, but $2.00 x $3.00 has no meaning. On the other hand, it is certainly sensible to multiply a currency value by a "normal" dimensionless quantity, for example, to find 25% of $150.00.
To understand the exact-result requirement, you must realize that not every fractional decimal value can be represented exactly as a binary floating-point quantity, and sometimes operations like addition and subtraction cause the result to be rounded off. While this approximation to the real numbers is often acceptable, it is unacceptable in monetary calculations--you would not be happy if the bank approximated your account balance.
ANALYSIS
We are asked to construct a software component providing a type and a set of operations. There are no specific problem inputs and outputs, but we shall need to provide input and output operations so that our user--again, another programmer--can write client programs that read and display currency values.
To ensure exact operations, we cannot simply use floating-point values. Because
integer arithmetic is exact, we will represent currency as a pair of two
nonnegative integer values, Dollars
and Cents
, and a
Boolean
value to indicate whether the currency value is positive
or not. We will then be able to write an ADT that provides exact operations.
DESIGN
We now look at the important algorithms in currency calculations. We are
allowing both positive and negative values and representing a currency value as
a pair of integers. Given a currency quantity Q
, denote its
dollars and cents parts by Q.Dollars
and Q.Cents
,
respectively; we carry the sign separately as a flag Q.Positive
.
First let us see how to convert a float value to a currency value:
Algorithm for Converting a Float F
to a Currency Quantity
Q
1. Q.Dollars
is the integer part of ABS F
;
ABS
means absolute value, as usual.
2. Q.Cents
is 100 x (ABS F
-
Q.Dollars
)
3. Q.Positive
is True
if and only if F >=
0.0
Note how the cents part of a currency value is calculated as the fractional
part of the Float
value, multiplied by 100.
Now let us look at key algorithms for adding and subtracting two positive currency values.
To Add Two Positive Currency Values Q1
and Q2
to produce Result
:
1. Set TempCents
to the sum of Q1.Cents
and
Q2.Cents
2. IF
TempCents >
99, THEN
we have
a carry:
3. Result.Cents
is TempCents - 100
4. Result.Dollars
is Q1.Dollars + Q2.Dollars + 1
5. ELSE
no carry:
6. Result.Cents
is TempCents
7. Result.Dollars
is Q1.Dollars + Q2.Dollars
END IF;
To Subtract Q2
from Q1
to Produce
Result
1. IF Q1 < Q2 THEN
2. Result
is negative:
3. Interchange Q1
and Q2
END IF;
4. IF
Q1.Cents < Q2.Cents
THEN
we
need a borrow:
5. Result.Cents
is (100 + Q1.Cents) - Q2.Cents
6. Result.Dollars
is (Q1.Dollars - 1) - Q2.Dollars
7. ELSE
no borrow:
8. Result.Cents
is Q1.Cents - Q2.Cents
9. Result.Dollars
is Q1.Dollars - Q2.Dollars
END IF;
Make sure you understand these algorithms; try some examples by hand to test yourself.
Program
10.7 shows the specification for this ADT package. The type
Quantity
is declared to be PRIVATE
so that we can
control all operations on values of this type. Note that we are also providing
a subtype CentsType
, which has range 0..99.
Program 10.7
Currency
Package
PACKAGE Currency IS ------------------------------------------------------------------------ --| Specification of the abstract data type for representing --| and manipulating Currency numbers. --| All values of type Currency.Quantity are initialized to 0.0. --| Author: Michael B. Feldman, The George Washington University --| Last Modified: July 1995 ------------------------------------------------------------------------ SUBTYPE CentsType IS Integer RANGE 0..99; TYPE Quantity IS PRIVATE; -- Operations FUNCTION MakeCurrency (F : Float) RETURN Quantity; -- constructor: -- Pre : F is defined -- Post: returns a Currency Quantity FUNCTION MakeFloat (Q : Quantity) RETURN Float; -- constructor: -- Pre: Q is defined -- Post: returns the value of Q in Float form FUNCTION Dollars (Q : Quantity) RETURN Natural; FUNCTION Cents (Q : Quantity) RETURN CentsType; FUNCTION IsPositive(Q : Quantity) RETURN Boolean; -- selectors: -- Pre: Q is defined -- Post: Dollars returns the Dollars part of Q; Cents the Cents part FUNCTION "<" (Q1 : Quantity; Q2 : Quantity) RETURN Boolean; FUNCTION ">" (Q1 : Quantity; Q2 : Quantity) RETURN Boolean; FUNCTION "<="(Q1 : Quantity; Q2 : Quantity) RETURN Boolean; FUNCTION ">="(Q1 : Quantity; Q2 : Quantity) RETURN Boolean; -- inquiry operators: -- Pre : Q1 and Q2 are defined -- Post: return Q1 < Q2, Q1 > Q2, Q1 <= Q2, and Q1 >= Q2, respectively FUNCTION "+" (Q : Quantity) RETURN Quantity; FUNCTION "-" (Q : Quantity) RETURN Quantity; FUNCTION "ABS"(Q : Quantity) RETURN Quantity; -- monadic arithmetic constructors: -- Pre: Q is defined -- Post: return Q, -Q, ABS Q respectively FUNCTION "+" (Q1 : Quantity; Q2 : Quantity) RETURN Quantity; FUNCTION "-" (Q1 : Quantity; Q2 : Quantity) RETURN Quantity; FUNCTION "*" (F : Float; Q : Quantity) RETURN Quantity; FUNCTION "*" (Q : Quantity; F : Float ) RETURN Quantity; FUNCTION "/" (Q1 : Quantity; Q2 : Quantity) RETURN Float; FUNCTION "/" (Q : Quantity; F : Float ) RETURN Quantity; -- dyadic arithmetic constructors: -- Pre : Q1 and Q2 are defined -- Post: these are the sensible arithmetic operators on Quantity. -- Note that multiplying two monetary values is not sensible. PRIVATE -- A record of type Quantity consists of a pair of Natural values -- such that the first number represents the Dollars part -- and the second number represents the Cents part. -- The sign of a Quantity value is indicated by a Boolean field -- called Positive. TYPE Quantity IS RECORD Positive: Boolean := True; Dollars : Natural := 0; Cents : CentsType := 0; END RECORD; -- Quantity END Currency;
Looking
at the operations on the currency type, we see first that operators are
provided to produce a currency quantity from its dollars and cents components
and to convert in both directions between our currency type and
Float
values. The next group of operations are selectors to return
the Dollars
and Cents
parts and an inquiry operator
to determine whether or not a currency value is positive.
The next four operators are the usual comparison operations we saw in
Ada.Calendar
. Note that we can use predefined equality/inequality
with no problem because two currency values are equal if and only if their
dollars, cents, and signs are respectively equal. The comparison operators are
followed by the three monadic arithmetic operators we saw in
Rationals
. Their meaning should be obvious.
The final six operators are interesting ones. Note that addition and
subtraction are defined for currency values, as one would expect. But
multiplication is defined only for a currency value and a Float
value, not for two currency values. This is because the product of two currency
values is meaningless, but finding, for example, 0.25 (which might represent
25%) of a currency value is indeed meaningful. The two multiplication
operations allow the mixed operands to be presented in either order. Similarly,
the division operations are meaningful ones: dividing one currency value by
another gives a normal Float
; dividing a currency value by a
Float
gives a currency value.
Defining operators as we have done here is called operator
overloading. Recall the similar group of operators in
Ada.Calendar;
it makes no difference whether the operators are provided by a predefined
package like Ada.Calendar
or by a user-defined package like
Currency
. Operators are really nothing more than functions with an
unusual syntax, appearing between their parameters instead of preceding them.
Because function names can be overloaded, so can operator names. Operator
overloading allows us to write operations that are mathematical in nature using
the familiar mathematical symbols.
It is important to understand that Ada allows us to overload only
those operator symbols already available in the language; we cannot, for
example, define a new operator "?"
because "?"
is not
already an operator in Ada. Also bear in mind that, for reasons beyond the
scope of this book to explain, it is not possible under most
circumstances to define our own operator "="
. It is similarly
prohibited (and will cause a compilation error) to overload "/="
and the two membership operators "IN"
and "NOT IN"
.
The last part of the specification is, as usual, the PRIVATE
part, in which the currency type is defined in full. Note that it is just a
record with three fields and that all three fields are initialized as before.
IMPLEMENTATION
Now
Program
10.8 gives the body for Currency
. The key to understanding the
operations is the first four function bodies. The first two, Add
and Subtract
, are not provided to client programs; they are there
only to make writing the other operators more convenient for us.
Program 10.8
Currency
Package
PACKAGE BODY Currency IS ------------------------------------------------------------------------ --| Body of the abstract data type for representing --| and manipulating Currency numbers. --| All values of type Currency.Quantity are initialized to 0.0. --| Author: Michael B. Feldman, The George Washington University --| Last Modified: July 1995 ------------------------------------------------------------------------ -- internal operations, not exported to the client SUBTYPE NonNegFloat IS Float RANGE 0.0 .. Float'Last; FUNCTION Add (Q1: Quantity; Q2: Quantity) RETURN Quantity IS -- Pre: Q1 >= 0.0 and Q2 >= 0.0. -- Post: Returns the sum of Q1 and Q2. -- This is just an auxiliary routine used in "+" and "-" below. Result : Quantity; TempCents : Natural; BEGIN -- Add TempCents := Q1.Cents + Q2.Cents; IF TempCents > 99 THEN -- we had a carry Result.Cents := TempCents - 100; Result.Dollars := Q1.Dollars + Q2.Dollars + 1; ELSE Result.Cents := TempCents; Result.Dollars := Q1.Dollars + Q2.Dollars; END IF; RETURN Result; END Add; FUNCTION Subtract (Q1: Quantity; Q2: Quantity) RETURN Quantity IS -- Pre: Q1 >= 0.0 and Q2 >= 0.0. -- Post: Returns the difference of Q1 and Q2. -- This is just an auxiliary routine used in "+" and "-" below. Result : Quantity; TempCents : Natural; BEGIN -- Subtract IF Q1 > Q2 THEN -- Result is positive IF Q2.Cents > Q1.Cents THEN -- we need a borrow Result.Cents := (100 + Q1.Cents) - Q2.Cents; Result.Dollars := (Q1.Dollars - 1) - Q2.Dollars; ELSE Result.Cents := Q1.Cents - Q2.Cents; Result.Dollars := Q1.Dollars - Q2.Dollars; END IF; ELSE -- Result is negative Result.Positive := False; IF Q1.Cents > Q2.Cents THEN -- we need a borrow Result.Cents := (100 + Q2.Cents) - Q1.Cents; Result.Dollars := (Q2.Dollars - 1) - Q1.Dollars; ELSE Result.Cents := Q2.Cents - Q1.Cents; Result.Dollars := Q2.Dollars - Q1.Dollars; END IF; END IF; RETURN Result; END Subtract; -- Exported Operators FUNCTION "+"(Q1 : Quantity; Q2 : Quantity) RETURN Quantity IS BEGIN IF Q1.Positive AND Q2.Positive THEN RETURN Add(Q1,Q2); ELSIF (NOT Q1.Positive) AND (NOT Q2.Positive) THEN RETURN -Add(-Q1, -Q2); ELSIF Q1.Positive AND (NOT Q2.Positive) THEN RETURN Subtract(Q1, -Q2); ELSE -- NOT Q1.Positive AND Q2.Positive; RETURN Subtract(Q2, -Q1); END IF; END "+"; FUNCTION "-"(Q1 : Quantity; Q2 : Quantity) RETURN Quantity IS BEGIN RETURN Q1 + (-Q2); END "-"; FUNCTION MakeCurrency (F : Float) RETURN Quantity IS Result: Quantity; T: Float; BEGIN T := Float'Truncation(ABS F); -- get whole-number part Result := (Positive => True, Dollars => Natural(T), -- just a type change Cents => Natural(100.0 * (ABS F - T))); IF F < 0.0 THEN Result.Positive := False; END IF; RETURN Result; END MakeCurrency; FUNCTION MakeFloat (Q : Quantity) RETURN Float IS Result: Float; BEGIN Result := Float(100 * Q.Dollars + Q.Cents) / 100.0; IF Q.Positive THEN RETURN Result; ELSE RETURN -Result; END IF; END MakeFloat; FUNCTION Dollars (Q : Quantity) RETURN Natural IS BEGIN RETURN Q.Dollars; END Dollars; FUNCTION Cents (Q : Quantity) RETURN CentsType IS BEGIN RETURN Q.Cents; END Cents; FUNCTION IsPositive(Q : Quantity) RETURN Boolean IS BEGIN RETURN Q.Positive; END IsPositive; FUNCTION ">" (Q1 : Quantity; Q2 : Quantity) RETURN Boolean IS BEGIN RETURN MakeFloat(Q1) > MakeFloat(Q2); END ">"; FUNCTION "<" (Q1 : Quantity; Q2 : Quantity) RETURN Boolean IS BEGIN -- stub RETURN True; END "<"; FUNCTION "<=" (Q1 : Quantity; Q2 : Quantity) RETURN Boolean IS BEGIN -- stub RETURN True; END "<="; FUNCTION ">=" (Q1 : Quantity; Q2 : Quantity) RETURN Boolean IS BEGIN -- stub RETURN True; END ">="; FUNCTION "+"(Q : Quantity) RETURN Quantity IS BEGIN RETURN Q; END "+"; FUNCTION "-"(Q : Quantity) RETURN Quantity IS BEGIN RETURN (Positive => NOT Q.Positive, Dollars => Q.Dollars, Cents => Q.Cents); END "-"; FUNCTION "ABS"(Q : Quantity) RETURN Quantity IS BEGIN -- stub RETURN Q; END "ABS"; FUNCTION "*"(F : Float; Q : Quantity) RETURN Quantity IS BEGIN RETURN(MakeCurrency(F * MakeFloat(Q))); END "*"; FUNCTION "*"(Q : Quantity; F : Float ) RETURN Quantity IS BEGIN -- stub RETURN Q; END "*"; FUNCTION "/"(Q1 : Quantity; Q2 : Quantity) RETURN Float IS BEGIN RETURN MakeFloat(Q1) / MakeFloat(Q2); END "/"; FUNCTION "/"(Q : Quantity; F : Float ) RETURN Quantity IS BEGIN -- stub RETURN Q; END "/"; END Currency;
Add
and Subtract
are implemented following the algorithms above. The
exported addition operator "+"
, which can handle positive or
negative values, uses Add
or Subtract
according to
the signs of its operands; the exported operator "-"
just adds a
negated value.
The next two operations are our constructors to convert to and from currency
values. Note how these are written. In going from Float
to
currency, we need to find the whole-number part of the float quantity, because
this will be the Dollars
part of the currency quantity. We do this
by using the attribute function Float'Truncation
, which, as we saw
in Chapter 7, does just what we want.
Finally, the remaining operators are given, mostly as stubs. You can
complete the package and develop a program to test it, as an exercise.
Program
10.9 and
Program
10.10 give the specification and body for a child package
Currency.IO
. We do not show a test program; we leave its
development as an exercise.
Program 10.9
Currency.IO
Child Package
WITH Ada.Text_IO; PACKAGE Currency.IO IS ------------------------------------------------------------------------ --| Specification of the input/output child package for Currency --| Author: Michael B. Feldman, The George Washington University --| Last Modified: July 1995 ------------------------------------------------------------------------ -- input operations to read a Quantity from terminal or file PROCEDURE Get (Item : OUT Quantity); PROCEDURE Get (File: IN Ada.Text_IO.File_Type; Item : OUT Quantity); -- Pre : File is open -- Post: The currency quantity is read as a normal -- floating point value. -- output operations to display a Quantity on terminal or -- write it to an external file PROCEDURE Put (Item : IN Quantity; Width: IN Natural:=8); PROCEDURE Put (File : IN Ada.Text_IO.File_Type; Item : IN Quantity; Width: IN Natural:=8); -- Pre: File is open, Item is defined -- Post: Displays or writes the currency quantity. -- Width is used by analogy with Integer_IO END Currency.IO;
Program10.10
Currency.IO
Child Package
WITH Ada.Text_IO; WITH Ada.Integer_Text_IO; WITH Ada.Float_Text_IO; PACKAGE BODY Currency.IO IS ------------------------------------------------------------------------ --| Body of the input/output child package for Currency --| Author: Michael B. Feldman, The George Washington University --| Last Modified: July 1995 ------------------------------------------------------------------------ -- input procedures PROCEDURE Get (File: IN Ada.Text_IO.File_Type; Item : OUT Quantity) IS F: Float; BEGIN -- Get -- just read it as a Float quantity, then convert Ada.Float_Text_IO.Get(File => File, Item => F); Item := MakeCurrency(F); END Get; PROCEDURE Get (Item : OUT Quantity) IS BEGIN -- Get Get(File => Ada.Text_IO.Standard_Input, Item => Item); END Get; -- output procedures PROCEDURE Put (File : IN Ada.Text_IO.File_Type; Item : IN Quantity; Width: IN Natural:=8) IS BEGIN -- Put -- dollars first IF IsPositive(Item) THEN Ada.Integer_Text_IO.Put (File=>File, Item=>Dollars(Item),Width=>1); ELSE Ada.Integer_Text_IO.Put (File=>File, Item=>-Dollars(Item),Width=>1); END IF; -- then decimal point and cents Ada.Text_IO.Put(File => File, Item => '.'); IF Cents(Item) < 10 THEN Ada.Text_IO.Put(File => File, Item => '0'); END IF; Ada.Integer_Text_IO.Put (File => File, Item => Cents(Item),Width => 1); END Put; PROCEDURE Put (Item : IN Quantity; Width: IN Natural:=8) IS BEGIN -- Put Put(File => Ada.Text_IO.Standard_Output, Item => Item, Width => Width); END Put; END Currency.IO;This example has shown the advantage of using a
PRIVATE
type not just
to encapsulate representation details, but also to give us complete control
over the operations a client is permitted to do. As part of developing your
test program, you might wish to attempt some operations not provided in the
package, for example, multiplying two currency values. Attempting this will
result in a compilation error; this tells you that the compiler is aiding you
in controlling the client operations.
USE
clause allows unqualified references to package
capabilities. Given three Currency.Quantity
variables
C1
, C2
, and C3
, a currency addition
operation is ordinarily written
C3 := Currency."+"(C1,C2);that is, just writing
"+"
as a function. However, if a client program
were preceded by
USE Currency;it could be written
C1 := C2 + C3;One of the advantages of Ada's permitting operator symbols like
"+"
to
be defined as functions is that they can be used in expressions in infix form,
as in the above line. When the expressions get more complex, this makes
programs even more readable. Compare the line
Currency.IO.Put(Item => Currency."+"(D, Currency."*"(E,F)));with the line
Currency.IO.Put(Item => A + E * F);This is, however, possible only if a
USE
clause appears in the client
program. Otherwise, the operator must not only be qualified (as in
Currency."+"
) but also must be used as a prefix function call like
any other function call.
Many in industry recommend against using the USE
statement
because in a program that WITH
s and USE
s many
packages, the USE
s make so many types and operations directly
visible that it is very confusing to the reader. Ada 95 adds the USE
TYPE
statement as a compromise, so that USE
can in general
be avoided without losing the benefit of user-defined operators. Writing, for
example,
USE TYPE Currency.Quantity;gives direct visibility to only the infix operators declared in the package, but to nothing else, and specifically not to other operations like
MakeCurrency
, Dollars
, and Cents
.
SYNTAX DISPLAY
Operator Overloading
FUNCTION " OpSymbol "(Formal1: Type1; Formal2: Type2) RETURN ReturnType ;
P,
will be associated with the operator OpSymbol and can be
called from a client program in one of two ways. If X
is of type
ReturnType,
X := Actual1 OpSymbol Actual2;
USE
or USE TYPE
statement appears in the
client program; otherwise,
X := P."OpSymbol"(Actual1, Actual2);
2. The operators "="
, "/="
, "IN"
, and
"NOT IN"
cannot normally be overloaded. All other predefined
operators can be overloaded.
3. The precedence of the operator cannot be changed by an overload, for
example, any "+"
operator will have lower precedence than any
"*"
operator.
PROGRAM STYLE
The USE
Clause Again
USE
clause would also have allowed us to write unqualified
references to all the other operations in Rationals
, but we chose
to leave most of the qualified references (for example, the
Rationals.Put
statements) as they were. This shows that qualified
references are still permitted even though a USE
appears.
Most Ada experts advise that qualified references should be used wherever
possible because they clarify programs by always indicating the name of the
package whose operation is being called. These same experts often advocate
never writing a USE
clause because then qualified
references are optional. In this book, we use the USE
where
appropriate--for example, to make infix ADT operators possible--but we also use
qualified reference in most cases, even where a USE
is present and
the qualification is optional.
When you have an ADT that provides infix operators, the Ada 95 USE
TYPE
clause provides a nice compromise because it allows the infix
operators to be unqualified, but nothing else.
PROGRAM STYLE
Advantages of Private
Types
Rationals
does not need to
know the actual internal representation of data type Rational
(i.e., a record with two fields). The client can call an operator function of
ADT Rationals
to perform an operation (e.g., rational addition)
without having this knowledge. In fact, it is better to hide this information
from the client program to prevent the client from directly manipulating the
individual fields of a rational variable.
It is advantageous for a client program not to have direct access to the representation of a rational quantity for three reasons:
4
and 12
,
respectively, in these fields would violate the package's assumption that all
rationals are stored in reduced form.
There is a fourth advantage, which would apply if the type represented
something more sophisticated, say, a database record of some kind. Each record
might contain information for "internal use only," that is, for use only by the
data management program itself, not for use by clients. Making the record
PRIVATE
ensures that the entire record structure is not made
available to the client, only that information which the ADT designer chooses
to supply via the ADT operations. This is an important advantage for large,
complicated, and secure applications.
Copyright © 1996 by Addison-Wesley Publishing Company, Inc.