Section 10.2 illustrated the use of an ADT. It is now time to consider how we might write an ADT of our own.
In Chapter 8 we developed a package SimpleDates for representing, reading, and displaying calendar dates. A difficulty with that package is that the user can enter and store a meaningless date (February 30, for example). In this section we improve the package so that it is more robust and offers more capabilities.
SimpleDates
, but now it is a private type so that a client program
does not manipulate the fields directly. This prevents the user from storing an
invalid date in a date variable. We shall also move the input/output operations
into a child package Dates.IO
. This is a style we shall use in
other ADTs as well.
Program 10.2
WITH Ada.Calendar; PACKAGE Dates IS ------------------------------------------------------------------------ --| Specification for package to represent calendar dates --| Author: Michael B. Feldman, The George Washington University --| Last Modified: November 1995 ------------------------------------------------------------------------ TYPE Months IS (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec); SUBTYPE Year_Number IS Ada.Calendar.Year_Number; SUBTYPE Day_Number IS Ada.Calendar.Day_Number; TYPE Date IS PRIVATE; Date_Error : EXCEPTION; -- constructors FUNCTION Today RETURN Date; -- Pre: None -- Post: Returns today's date; analogous to Ada.Calendar.Clock FUNCTION Date_Of(Year : Year_Number; Month : Months; Day : Day_Number) RETURN Date; -- Pre: Year, Month, and Day are defined -- Post: Returns a Date value -- Raises: Date_Error if the year, month, day triple do not -- form a valid date (Feb. 30, for example) -- Analogous to Ada.Calendar.Time_Of -- selectors FUNCTION Year (D: Date) RETURN Year_Number; FUNCTION Month(D: Date) RETURN Months; FUNCTION Day (D: Date) RETURN Day_Number; -- Pre: D is defined -- Post: Return the year, month, or day component, respectively PRIVATE TYPE Date IS RECORD Month: Months := Months'First; Day: Day_Number := Day_Number'First; Year: Year_Number := Year_Number'First; END RECORD; END Dates;We define two subtypes
Year_Number
and Month_Number
as
"nicknames" for the ones provided by Ada.Calendar
. Because
Date
is a private type, a client program has no direct access to
its fields. Therefore we need to supply constructors Today
, as in
Simple_Dates
, and Date_Of
by analogy with the
Time_Of
constructor in Ada.Calendar
. Further, we need
selectors Year
, Month
, and Day
, by
analogy with the corresponding ones in Ada.Calendar
, each of which
selects and returns the given component of the date record. Also by analogy
with Ada.Calendar
, we provide an exception
Date_Error
, raised when Date_Of
would produce a
meaningless date like February 30 or June 31.
Finally, there are two Get
and two Put
procedures.
By analogy with Ada.Text_IO
, there are terminal-oriented and
file-oriented versions of each.
SYNTAX DISPLAY
Private Type Definition
PACKAGE PackageName IS ... TYPE TypeName IS PRIVATE; ... PRIVATE TYPE TypeName IS full type definition (usually a record) END PackageName;
PACKAGE Rationals IS ... TYPE Rational IS PRIVATE; ... PRIVATE TYPE Rational IS RECORD Numerator: Integer; Denominator: POsitive; END RECORD; END PackageName;
SYNTAX DISPLAY
User-defined Exception
ExceptionName : EXCEPTION;
ZeroDenominator: EXCEPTION;
RAISE ExceptionName ;
A client program can have an exception handler for this exception, of the form
WHEN ExceptionName =>
Program 10.3 shows the body of package Dates
. Because
Ada.Calendar
already knows how to validate a date, the constructor
function Date_Of
just uses Ada.Calendar.Time_Of
to do
this. If Time_Of
does not raise Time_Error
, the date
is valid. The selectors Year
, Month
, and
Day
should be obvious, and Today
works just as it did
in SimpleDates
, calling the appropriate Ada.Calendar
operations to produce the date.
Program 10.3
Dates
Package
WITH Ada.Calendar; PACKAGE BODY Dates IS ------------------------------------------------------------------------ --| Body for package to represent calendar dates --| Author: Michael B. Feldman, The George Washington University --| Last Modified: July 1995 ------------------------------------------------------------------------ FUNCTION Today RETURN Date IS -- Finds today's date and returns it as a record of type Date -- Today's date is gotten from PACKAGE Ada.Calendar Right_Now : Ada.Calendar.Time; -- holds internal clock value Temp : Date; BEGIN -- Today -- Get the current time value from the computer's clock Right_Now := Ada.Calendar.Clock; -- Extract the current month, day, and year from the time value Temp.Month := Months'Val(Ada.Calendar.Month(Right_Now)- 1); Temp.Day := Ada.Calendar.Day (Right_Now); Temp.Year := Ada.Calendar.Year (Date => Right_Now); RETURN Temp; END Today; FUNCTION Date_Of(Year : Year_Number; Month : Months; Day : Day_Number) RETURN Date IS -- constructs a date given year, month, and day. Temp: Ada.Calendar.Time; BEGIN -- Date_Of Temp := Ada.Calendar.Time_Of(Year=>Year, Month=>Months'Pos(Month)+1, Day=>Day); -- assert: M, D, and Y form a sensible date if Time_error not raised RETURN (Month => Month, Year => Year, Day => Day); -- assert: a valid date is returned EXCEPTION WHEN Ada.Calendar.Time_Error => RAISE Date_Error; END Date_Of; FUNCTION Year (D: Date) RETURN Year_Number IS BEGIN RETURN D.Year; END Year; FUNCTION Month (D: Date) RETURN Months IS BEGIN RETURN D.Month; END Month; FUNCTION Day (D: Date) RETURN Day_Number IS BEGIN RETURN D.Day; END Day; END Dates;
As mentioned earlier, it is a good idea to separate construction of dates, and
selection of date fields, from input and output of dates, and so we provide a
child package Dates.IO
to handle the Get
and
Put
operations. Recall that a child package can be thought of as
an extension of its parent package.
Program 10.4 shows the specification of the child package.
Program 10.4
Dates
Child Package
WITH Ada.Text_IO; PACKAGE Dates.IO IS ------------------------------------------------------------------------ --| Specification for child package to read and display dates --| Author: Michael B. Feldman, The George Washington University --| Last Modified: November 1995 ------------------------------------------------------------------------ TYPE Formats IS (Full, -- February 7, 1991 Short, -- 07 FEB 91 Numeric); -- 2/7/91 PROCEDURE Get(Item: OUT Date); PROCEDURE Get(File: IN Ada.Text_IO.File_Type; Item: OUT Date); -- Pre: File is open -- Post: Reads a date in mmm dd yyyy form from standard or input -- or an external file, respectively PROCEDURE Put(Item: IN Date; Format: IN Formats); PROCEDURE Put(File: IN Ada.Text_IO.File_Type; Item: IN Date; Format: IN Formats); -- Pre: File is open; Item and Format are defined -- Post: Writes a date in the desired format to standard output -- or an external file, respectively END Dates.IO;In this specification we define an enumeration type,
Formats
, as
follows:
TYPE Format IS (Full, Short, Numeric);which we will use in the output procedure to determine which of the four following forms will be used to display a date:
February 4, 1995 04 FEB 95 2/4/95
Program 10.5 gives the body of the child package.
Program 10.5
Body of Dates
Child Package
WITH Ada.Calendar; WITH Ada.Text_IO; WITH Ada.Integer_Text_IO; PACKAGE BODY Dates.IO IS ------------------------------------------------------------------------ --| Body for child package to read and display calendar dates --| Author: Michael B. Feldman, The George Washington University --| Last Modified: July 1995 ------------------------------------------------------------------------ PACKAGE Month_IO IS NEW Ada.Text_IO.Enumeration_IO(Enum => Months); PROCEDURE Get(File: IN Ada.Text_IO.File_Type; Item: OUT Date) IS M: Months; D: Day_Number; Y: Year_Number; BEGIN -- Get Month_IO.Get (File => File, Item => M); Ada.Integer_Text_IO.Get(File => File, Item => D); Ada.Integer_Text_IO.Get(File => File, Item => Y); -- assert: M, D, and Y are well-formed and in range -- otherwise one of the Get's would raise an exception Item := Date_Of (Month => M, Year => Y, Day => D); -- assert: Item is a valid date if Date_Error not raised EXCEPTION WHEN Ada.Text_IO.Data_Error => RAISE Date_Error; WHEN Constraint_Error => RAISE Date_Error; WHEN Date_Error => RAISE Date_Error; END Get; PROCEDURE WriteShort(File: IN Ada.Text_IO.File_Type; Item: IN Date) IS -- Pre: Item is assigned a value -- Post: Writes a date in dd MMM yy form Last2Digits : Natural; BEGIN -- WriteShort Last2Digits := Item.Year MOD 100; IF Item.Day < 10 THEN Ada.Text_IO.Put(File => File, Item => '0'); END IF; Ada.Integer_Text_IO.Put(File => File, Item => Item.Day, Width => 1); Ada.Text_IO.Put(File => File, Item => ' '); Month_IO.Put (File => File, Item => Item.Month, Width => 1); Ada.Text_IO.Put(File => File, Item => ' '); IF Last2Digits < 10 THEN Ada.Text_IO.Put(File => File, Item => '0'); END IF; Ada.Integer_Text_IO.Put (File => File, Item => Last2Digits, Width => 1); END WriteShort; PROCEDURE WriteFull(File: IN Ada.Text_IO.File_Type; Item: IN Date) IS -- Pre: Item is assigned a value -- Post: Writes a date in Monthname dd, yyyy form BEGIN CASE Item.Month IS WHEN Jan => Ada.Text_IO.Put(File => File, Item => "January"); WHEN Feb => Ada.Text_IO.Put(File => File, Item => "February"); WHEN Mar => Ada.Text_IO.Put(File => File, Item => "March"); WHEN Apr => Ada.Text_IO.Put(File => File, Item => "April"); WHEN May => Ada.Text_IO.Put(File => File, Item => "May"); WHEN Jun => Ada.Text_IO.Put(File => File, Item => "June"); WHEN Jul => Ada.Text_IO.Put(File => File, Item => "July"); WHEN Aug => Ada.Text_IO.Put(File => File, Item => "August"); WHEN Sep => Ada.Text_IO.Put(File => File, Item => "September"); WHEN Oct => Ada.Text_IO.Put(File => File, Item => "October"); WHEN Nov => Ada.Text_IO.Put(File => File, Item => "November"); WHEN Dec => Ada.Text_IO.Put(File => File, Item => "December"); END CASE; Ada.Text_IO.Put(File => File, Item => ' '); Ada.Integer_Text_IO.Put(File => File, Item => Item.Day, Width => 1); Ada.Text_IO.Put(File => File, Item => ", "); Ada.Integer_Text_IO.Put (File => File, Item => Item.Year, Width => 1); END WriteFull; PROCEDURE WriteNumeric(File: IN Ada.Text_IO.File_Type; Item: IN Date) IS -- Pre: Item is assigned a value -- Post: Writes a date in mm/dd/yy form Last2Digits : Natural; BEGIN Last2Digits := Item.Year MOD 100; Ada.Integer_Text_IO.Put (File => File, Item => Months'Pos(Item.Month)+1, Width => 1); Ada.Text_IO.Put(File => File, Item => '/'); Ada.Integer_Text_IO.Put(File => File, Item => Item.Day, Width => 1); Ada.Text_IO.Put(File => File, Item => '/'); IF Last2Digits < 10 THEN Ada.Text_IO.Put(File => File, Item => '0'); END IF; Ada.Integer_Text_IO.Put (File => File, Item => Last2Digits, Width => 1); END WriteNumeric; PROCEDURE Put(File: IN Ada.Text_IO.File_Type; Item: IN Date; Format: IN Formats) IS BEGIN -- Put CASE Format IS WHEN Short => WriteShort(File => File, Item => Item); WHEN Full => WriteFull(File => File, Item => Item); WHEN Numeric => WriteNumeric(File => File, Item => Item); END CASE; END Put; PROCEDURE Get(Item: OUT Date) IS BEGIN -- Get Get(File => Ada.Text_IO.Standard_Input, Item => Item); END Get; PROCEDURE Put(Item: IN Date; Format: IN Formats) IS BEGIN -- Put Put (File => Ada.Text_IO.Standard_Output, Item => Item, Format => Format); END Put; END Dates.IO;The procedure
Dates.IO.Get
reads a date a bit more robustly than its
counterpart in SimpleDates
. If the date read is ill-formed (month,
day, or year is not of the proper form), or if the combination would yield a
meaningless date, Date_Error
is raised and must be handled by the
client program. This is analogous to the way in which the various
Get
procedures in Ada.Text_IO
raise
Data_Error
for ill-formed or out-of-range input.
The procedure Dates.IO.Put
displays a date in one of the three
forms given above, depending upon the value of the parameter
Format
. Put
calls one of three local procedures
WriteFull
, WriteShort
, and WriteNumeric
,
depending on a CASE
statement to select the appropriate one.
WriteShort
and WriteNumeric
are based on
Todays_Date
(
Program
3.6) and Todays_Date_2
(
Program
3.7); the third needs explanation.
WriteFull
uses a CASE
statement to write the
appropriate month name, depending on the month field of the date record. It
would have been nice to use an enumeration type for the full names of the
months, because Enumeration_IO
is so easy to use. Unfortunately,
the Put
procedure in Enumeration_IO
displays or
writes the enumeration literal either in uppercase letters or in lowercase
ones; there is no way to get it to display just the first letter as a capital.
Because in American correspondence we always capitalize just the first letter
of the month, we need to use the CASE
statement to control the
precise form of the string displayed.
PROGRAM STYLE
Procedures in a Package Body but Not in the Specification
WriteFull
,
WriteShort
, and WriteNumeric
appear only in
the package body; they are not given in the specification. This is quite
intentional: these procedures are not intended for use by the client program;
their only purpose is to refine the procedure Put
, which is indeed
intended for the client.
When you design a package, you should consider very carefully just which
operations to give to the client, list these in the specification, and
implement them in the body. It is, of course, a compilation error to list a
procedure or function in the specification and not put a corresponding
body in the package body. This is because the specification is a contract that
makes promises to the client that the body must fulfill. However, it is
not an error to write procedures or functions in the body but not in the
specification. Indeed, it is often quite desirable to do this, as the
Dates
example illustrates.
Program
10.6 shows a test of the Dates
and Dates.IO
packages. The program displays the current date in all three formats, then asks
the user to enter a date and displays that date all three ways.
Program 10.6
Dates
Package
WITH Ada.Text_IO; WITH Dates; WITH Dates.IO; PROCEDURE Test_Dates IS ------------------------------------------------------------------------ --| Demonstration of Dates package --| Author: Michael B. Feldman, The George Washington University --| Last Modified: July 1995 ------------------------------------------------------------------------ D: Dates.Date; BEGIN -- Test_Dates -- first test the function Today D := Dates.Today; Ada.Text_IO.Put(Item => "Today is "); Ada.Text_IO.New_Line; Dates.IO.Put(Item => D, Format => Dates.IO.Short); Ada.Text_IO.New_Line; Dates.IO.Put(Item => D, Format => Dates.IO.Full); Ada.Text_IO.New_Line; Dates.IO.Put(Item => D, Format => Dates.IO.Numeric); Ada.Text_IO.New_Line; LOOP BEGIN -- block for exception handler Ada.Text_IO.Put("Please enter a date in MMM DD YYYY form > "); Dates.IO.Get(Item => D); EXIT; -- only if no exception is raised EXCEPTION WHEN Dates.Date_Error => Ada.Text_IO.Skip_Line; Ada.Text_IO.Put(Item => "Badly formed date; try again, please."); Ada.Text_IO.New_Line; END; END LOOP; -- assert: at this point, D contains a correct date record Ada.Text_IO.Put(Item => "You entered "); Ada.Text_IO.New_Line; Dates.IO.Put(Item => D, Format => Dates.IO.Short); Ada.Text_IO.New_Line; Dates.IO.Put(Item => D, Format => Dates.IO.Full); Ada.Text_IO.New_Line; Dates.IO.Put(Item => D, Format => Dates.IO.Numeric); Ada.Text_IO.New_Line; END Test_Dates;Sample Run
Today is 03 NOV 95 November 3, 1995 11/3/95 Please enter a date in MMM DD YYYY form > Dec 15 1944 You entered 15 DEC 44 December 15, 1944 12/15/44
Dates
is behaving correctly for all inputs.
Ada.Calendar
did not have a
date-validating operation. Rewrite the body of Dates
so that a
date supplied to Date_Of
is validated by your package, raising
Date_Error
if the date would be meaningless. Do not use
Ada.Calendar.Time_Of
to do this.
Copyright © 1996 by Addison-Wesley Publishing Company, Inc.