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

10.3 System Structures: Writing an ADT for Calendar Dates

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.

Specification for the Improved Dates Package

The specification for our improved package appears in Program 10.2. We represent a date using the same record form as in 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
Specification for Improved Dates Package

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

Form:
PACKAGE PackageName IS
...
  TYPE TypeName IS PRIVATE;
...
PRIVATE
  TYPE TypeName IS
    full type definition (usually a record)
END PackageName;

Example:
PACKAGE Rationals IS
...
  TYPE Rational IS PRIVATE;
...
PRIVATE
  TYPE Rational IS RECORD
    Numerator: Integer;
    Denominator: POsitive;
  END RECORD;
END PackageName;

Interpretation:
A private type can be defined only in a package specification. The first occurrence of TypeName defines it as a private type; the full type definition appears at the end of the specification, in the private section.

SYNTAX DISPLAY
User-defined Exception

Form:
ExceptionName : EXCEPTION;

Example:
ZeroDenominator: EXCEPTION;

Interpretation:
Exceptions are usually defined in a package specification. The exception can be raised by an operation in the corresponding package body by the statement
RAISE ExceptionName ;

A client program can have an exception handler for this exception, of the form

WHEN ExceptionName =>

Body of the Improved Dates Package

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
Body of Improved 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;

The Child Package Dates.IO

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
Specification for 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

It is worth noting that the three procedures 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
Test of Improved 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

Exercises for Section 10.3

Self-Check

  1. Explain the advantages of making the data record a private type.

Programming

  1. Write a short program that attempts to access a field of a date record directly. Explain the result you get.
  2. Expand Program 10.6 so that the user has a chance to enter a number of dates. Use this to test the dates package with a number of test cases that will show whether Dates is behaving correctly for all inputs.
  3. Suppose that package 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.


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

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