Code Capsule

A C++ Date Class, Part 2

Chuck Allison


Chuck Allison is a software architect for the Family History Department of Jesus Christ of Latter Day Saints Church Headquarters in Salt Lake City. He has a B.S. and M.S. in mathematics, has been programming since 1975, and has been teaching and developing in C since 1984. His current interest object-oriented technology and education. He is a member of X3J16, the ANSI C++ Standards Committee. Chuck can be reached on the Internet at allison@decus.org, or at (801) 240-4510.

In last month's capsule I presented the beginnings of a simple date class. In addition to providing a member function to calculate the interval between two dates, this class illustrated the following features of C++:

  • inline functions
  • references
  • constructors
  • controlled access to private data members
  • In this month's installment I will add relational operators, input/output operations, and the capability to get the current date, while demonstrating these features:

  • operator overloading
  • streams
  • friend functions
  • static members
  • When using dates you often need to determine if one precedes another. I will add the member function

    int compare(const Date& d2) const;
    to the date class (see Listing 1) .

    Date::compare behaves like strcmp — it returns a negative integer if the current object (*this) precedes d2, 0 if both represent the same date, and a positive integer otherwise (see Listing 2 for the implementation and Listing 3 for a sample program). For those of you familiar with qsort from the Standard C library, you can use Date::compare to sort dates just like you use strcmp to sort strings. Here is a compare function to pass to qsort:

    #include "date.h"
    int datecmp(const void *p1, const void *p2)
    {
       const Date
       *d1p = (const Date *) p1,
       *d2p = (const Date *) p2;
       return d1p->compare(*d2p);
    }
    (Next month's CODE CAPSULE will cover qsort).

    Operator Overloading

    Most of the time, it is more convenient to have relational operators, for example

    if (d1 < d2)
      // do something appropriate..
    Adding a less-than operator is trivial using Date::compare — just insert the following in-line member function into the class definition:

    int operator<(const Date& d2) const
    {return compare(d2) < 0);
    The compiler translates each occurrence of an expression such as

    d1 < d2
    into the function call

    d1.operator<(d2)
    Listing 4 has the class definition with all six relational operators and the updated sample program is in Listing 5.

    Since the functionality of Date::interval is like a subtraction (it gives us the difference between two dates), it would seem natural to rename it as Date::operator-. Before doing this, take a closer look at the semantics of the statement

    a = b - c;
    No matter the type of the variables, the following should hold:

    a is a distinct object created by the subtraction, and b - c == - (c - b)

    I will use the convention that a "positive" Date object has all positive data members, and conversely for a "negative" date (mixed signs are not allowed). In Listing 7, I have replaced Date::interval with Date::operator- (const Date&), which affixes the proper signs to the data members and returns a newly-constructed Date object. The new class definition in Listing 6 also contains a unary minus operator, which also has the name Date::operator-, but takes no arguments. The compiler will transform the statements

    d1 - d2;
    -d1;
    respectively into

    d1.operator-(d2); // Calls Date::operator-(const Date&)
    d1.operator-();   // Calls Date::operator-()
    A sample program using these new member functions is in Listing 8.

    Stream I/O

    One thing remains before I can say that a Date object has the look and feel of a built-in type — input/output support. C++ supplies stream objects that handle I/O for standard types. For example, the output from the program

    #include <iostream.h>
    
    main()
    {
      int i;
      cout << "Enter an integer: ";
      cin >> i;
      cout << "You typed " << i << endl;
      return 0;
    }
    will be something like

    Enter an integer: 5
    You typed 5
    cout is an output stream (class ostreom) supplied by the C++ streams library and cin is an input stream (class istream), which are associated by default with standard output and standard input, respectively. When the compiler sees the expression

    cout << "Enter an integer: "
    it replaces it with

    cout.operator<<("Enter an integer: ")
    which calls the member function ostream::operator<<(const char *). Likewise, the expression

    cout << i
    where i is an integer calls the function

    ostream::-operator<<(int).
    endl is a special stream directive (called a manipulator) which outputs a newline and flushes the output buffer. Output items can be chained together:

    cout << "You typed " << i
    because ostream::operator<< returns a reference to the stream itself. The above statement becomes

    (cout.operator<<("You typed ")).operator<<(i)
    To accommodate output of Date objects, then, you will need a global function that sends the printed representation to a given output stream, and then returns a reference to that stream:

    ostream& operator<<(ostream& os, const Date& d)
    {
       os << d.get_month() << '/'
       << d.get_day() << '/'
       << d.get_year();
       return os;
    }
    This of course can't be a member function, since the stream (not the object being output) always appears to the left of the stream insertion operator.

    Friends

    For efficiency, it is customary to give operator<< access to the private data members of an object. (Most implementations of a class provide the associated I/O operators too, so it seems safe to break the encapsulation boundary in this case.) To bypass the restriction on access to private members, you need to declare operator<< to be a friend of the Date class by adding the following statement to the class definition:

    friend ostream& operator<<(ostream&, const Date&);
    This declaration appears in the new class definition in Listing 9, along with the corresponding friend declaration for the input function, operator>>. The implementation for these functions is in Listing 10, and Listing 11 has a sample program.

    Static Members

    A class in C++ defines a scope. This is why the function Date::compare would not conflict with a global function named compare (even if the arguments and return value were of the same type). Now consider the array dtab[] in the implementation file. dtab's static storage class makes it private to the file, but it really belongs to the class, not to the file. If I chose to spread the Date member functions across multiple files, I would be forced to group those that needed access to dtab together in the same file.

    A better solution is to make dtab a static member of the Date class. Static members belong to the whole class, not to a single object. This means that only one copy of dtab exists, and is shared by all objects of the class. Making the function isleap static allows you to call it without an associated object, i.e., you just need to say

    isleap(y);
    instead of

    d.isleap(y);
    To make isleap available to any caller, place it in the public section of the class definition, and call it like this:

    Date::isleap(y);
    As a finishing touch, I will redefine the default constructor to initialize its object with the current date. The final class definition, implementation and sample program are in
    Listing 12 - Listing 14 respectively.

    Summary

    In these last two installments I've tried to illustrate how C++ supports data abstraction — the creation of user-defined types. Constructors cause objects to be initialized automatically when you declare them. You can protect class members from inadvertent access by making them private. Overloading common operators makes your objects look and feel like built-in types — a plus for readability and maintenance.

    Listing 1 Introduces a function to compare dates

    // date4.h
    
    class Date
    {
       int month;
       int day;
       int year;
    
    public:
       // Constructors
       Date()
         {month = day = year= 0;}
       Date(int m, int d, int y)
         {month = m; day = d; year = y;}
    
       // Accessor Functions
       int get_month() const
         {return month;}
       int get_day() const
         {return day;}
       int get_year() const
         {return year;}
    
       Date * interval(const Date&) const;
       int compare(const Date&) const;
    };
    
    // End of File
    

    Listing 2 Implements the interval and compare member functions

    // date4.cpp
    
    #include "date4.h"
    
    inline int isleap(int y)
      {return y%4 == 0 && y%100 != 0 || y%400 == 0;}
    
    static int dtab[2][13] =
    {
      {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
      {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
    };
    
    Date * Date::interval(const Date& d2) const
    {
       static Date result;
       int months, days, years, prev_month;
    
       // Compute the interval - assume d1 precedes d2
       years = d2.year - year;
       months = d2.month - month;
       days = d2.day - day;
    
       // Do obvious corrections (days before months!)
       //
       // This is a loop in case the previous month is
       // February, and days < -28.
       prev_month = d2.month - 1;
       while (days < 0)
       {
          // Borrow from the previous month
          if (prev_month == 0)
             prev_month = 12;
          -months;
          days += dtab[isleap(d2.year)][prev_month-];
       }
    
       if (months < 0)
       {
          // Borrow from the previous year
          -years;
          months += 12;
       }
    
       // Prepare output
       result.month = months;
       result.day = days;
       result.year = years;
       return &result;
    }
    
    int Date::compare(const Date& d2) const
    {
       int months, days, years, order;
    
       years = year - d2.year;
       months = month - d2.month;
       days = day - d2.day;
    
       // return <0, 0, or >0, like strcmp()
       if (years == 0 && months == 0 && days == 0)
          return 0;
       else if (years == 0 && months == 0)
          return days;
       else if (years == 0)
          return months;
       else
          return years;
    }
    // End of File
    

    Listing 3 Tests the compare member function

    // tdate4.cpp
    
    #include <stdio.h>
    #include "date4.h"
    
    void compare_dates(const Date& d1, const Date& d2)
    {
       int compval = d1.compare(d2);
       char *compstr - (compval < 0) ? "precedes" :
         ((compval > 0) ? "follows" : "equals"};
    
       printf("%d/%d/%d %s %d/%d/%d\n",
         d1.get_month(),d1.get_day(0),d1.get_year(),
         compstr,
         d2.get_month(),d2.get_day(),d2.get_year());
    }
    main()
    {
       Date d1(1,1,1970);
       compare dates(d1,Date(10,1,1951));
       compare_dates{d1,Date(1,1,1970));
       compare_dates(d1,Date(12,31,1992));
       return 0;
    }
    
    /* OUTPUT
    
    1/1/1970 follows 10/1/1951
    1/1/1970 equals 1/1/1970
    1/1/1970 precedes 12/31/1992
    */
    
    // End of File
    

    Listing 4 Defines relational operators for the Date class

    // date5.h
    
    class Date
    {
       int month;
       int day;
       int year;
    
    public:
       // Constructors
       Date()
         {month = day = year = 0;}
       Date(int m, int d, int y)
         {month = m; day = d; year = y;}
    
       // Accessor Functions
       int get_month() const
         {return month;}
       int get_day() const
         {return day;}
       int get_year() const
         {return year;}
    
       Date * interval(const Date&) const;
       int compare(const Date&) const;
    
       // Relational operators
       int operator<(const Date& d2) const
         {return compare(d2) < 0;}
       int operator<=(const Date& d2) const
         {return compare(d2) <= 0;}
       int operator>(const Date& d2) const
         {return compare(d2) > 0;}
       int operator>=(const Date& d2) const
         {return compare(d2) >= 0;}
       int operator!=(const Date& d2) const
         {return compare(d2) != 0;}
       int operator!=(const Date& d2) const
         {return compare(d2) !=0;}
    };
    
    // End of File
    

    Listing 5 Uses the Date relational operators

    // tdate5.cpp
    
    #include <stdio.h>
    #include <stdlib.h>
    #include "date5.h"
    
    void compare_dates(const Date& d1, const Date& d2)
    {
       char *compstr = (d1 < d2) ? "precedes" :
         ((d1 > d2) ? "follows" : "equals");
    
       printf("%d/%d/%d %s %d/%d/%d\n",
         d1.get_month(),d1.get_day(),d1.get_year(),
         compstr,
         d2.get_month(),d2.get_day(),d2.get_year());
    }
    
    main()
    {
       Date d1(1,1,1970);
       compare_dates(d1,Date(10,1,1951));
       compare_dates(d1,Date(1,1,1970));
       compare_dates(d1,Date(12,31,1992));
       return 0;
    }
    
    /* OUTPUT
    
    1/1/1970 follows 10/1/1951
    1/1/1970 equals 1/1/1970
    1/1/1970 precedes 12/31/1992
    */
    
    // End of File
    

    Listing 6 Adds binary and unary minus to the Date class

    // date6.h
    
    class Date
    {
       int month;
       int day;
       int year;
    
    public:
       // Constructors
       Date()
         {month = day = year = 0;}
       Date(int m, int d, int y)
         {month = m; day = d; year = y;}
    
       // Accessor Functions
       int get_month() const
         {return month;}
       int get_day() const
         {return day;}
       int get_year() const
         {return year;}
    
       Date operator-(const Date& d2) const;
       Date& operator-()
         {month = -month; day = -day; year = -year;
          return *this;}
    
       int compare(const Date&) const;
    
       // Relational operators
       int operator<(const Date& d2) const
         {return compare(d2) < 0;}
       int operator<=(const Date& d2) const
         {return compare(d2) <= 0;}
       int operator>(const Date& d2) const
         {return compare(d2) > 0;}
       int operator>=(const Date& d2) const
         {return compare(d2) >= 0;}
       int operator==(const Date& d2) const
         {return compare(d2) == 0;}
       int operator!=(const Date& d2) const
         {return compare(d2) != 0;}
    };
    
    // End of File
    

    Listing 7 Implements the binary minus operator

    // date6.cpp
    #include <assert.h>
    #include "date6.h"
    
    inline int isleap(int y)
      {return y%4 == 0 && y%100 != 0 || y%400 == 0;}
    
    static int dtab[2][13] =
    {
      {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
      {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
    };
    
    Date Date::operator-(const Date& d2) const
    {
       int months, days, years, prev_month, order;
       const Date * first, * last;
    
       // Must know which date is first
       if (compare(d2) <= 0)
       {
          // this <= d2
          order = -1;
          first = this;
          last = &d2;
       }
       else
       {
          order = 1;
          first = &d2;
          last = this;
       }
    
       // Compute the interval; first <= last
       years = last->year - first->year;
       months = last->month - first->month;
       days = last->day - first->day;
       assert(years >= 0 && months >= 0 && days >= 0);
    
       // Do obvious corrections (days before months!)
       // This is a loop in case the previous month is
       // February, and days < -28.
       prev_month = last->month - 1;
       while (days < 0)
       {
          // Borrow from the previous month
          if (prev_month == 0)
             prev_month = 12;
          --months;
          days += dtab[isleap(last->year)][prey_month--];
       }
    
       if {months < 0)
       {
          // Borrow from the previous year
          --years;
          months += 12;
       }
    
       // Return a date object with the interval
       if (order == -1)
          return Date(-months,-days,-years);
       else
          return Date(months, days, years);
    }
    
    int Date::compare(const Date& d2) const
    {
        // same as in Listing 2
    }
    // End of File
    

    Listing 8 Subtracts two dates

    // tdate6.cpp:
    
    #include <stdio.h>
    #include "date6.h"
    
    main()
    {
       Date d1(1,1,1970), d2(12,8,1992);
       Date result = d1 - d2;
       printf("years: %d, months: %d, days: %d\n",
          result.get_year(),
          result.get_month(),
          result.get_day());
       result = d2 - d1;
       printf("years: %d, months: %d, days: %d\n",
          result.get_year(),
          result.get_month(),
          result.get_day());
       int test = d1 - d2 == -(d2 - d1);
       printf("d1 - d2 == -(d2 - d1)? %s\n",
         test ? "yes" : "no");
       return 0;
    }
    
    /* OUTPUT
    
    years: -22, months: -11, days: -7
    years: 22, months: 11, days: 7
    d1 - d2 == -(d2 - d1)? yes
    */
    
    // End of File
    

    Listing 9 Adds stream I/O to the Date class

    // date7.h
    
    class ostream;
    
    class Date
    {
       int month;
       int day;
       int year;
    
    public:
       // Constructors
       Date()
         {month = day = year = 0;}
       Date(int m, int d, int y)
         {month = m; day = d; year = y;}
    
       // Accessor Functions
       int get_month() const
         {return month;}
       int get_day() const
         {return day;}
       int get_year() const
         {return year;}
    
       Date operator-(const Date& d2) const;
       Date& operator-()
         {month= -month; day = -day; year = -year;
          return *this;}
    
       int compare(const Date&) const;
    
       // Relational operators
       int operator<(const Date& d2) const
         {return compare(d2) < 0;}
       int operator<=(const Date& d2) const
         {return compare(d2) <= 0;)
       int operator>(const Date& d2) const
         {return compare(d2) > 0;}
       int operator>=(const Date& d2) const
         {return compare(d2) >= 0;}
       int operator==(const Date& d2) const
         {return compare(d2) == 0;}
       int operator!=(const Date& d2) const
         {return compare(d2) != 0)
    
       // I/O operators
       friend ostream& operator<<(ostream&, const Date&);
       friend istream& operator>>(istream&, Date&);
    };
    
    // End of File
    

    Listing 10 Implements Date stream I/O functions

    #include <iostream.h>
    #include "date7.h"
    
    ostream& operator<<(ostream& os, const Date& d)
    {
       os << d.month << '/' << d.day << '/' << d.year;
       return os;
    }
    
    istream& operator>>(istream& is, Date& d)
    {
       char slash;
       is >> d.month >> slash >> d.day >> slash >> d.year;
       return is;
    }
    
    // End of File
    

    Listing 11 Illustrates stream I/O of Date objects

    // tdate7.cpp:
    
    #include <iostream.h>
    #include "date7.h"
    
    main()
    {
       Date d1, d2;
       cout << "Enter a date: ";
       cin >> d1;
       cout << "Enter another date: ";
       cin >> d2;
       cout << "d1 - d2 = "<< d1 - d2 << endl;
       cout << "d2 - d1 = "<< d2 - d1 << endl;
       return 0;
    }
    
    /* OUTPUT
    
    Enter a date: 10/1/1951
    Enter another date: 5/1/1954
    d1 - d2 = -7/0/-2
    d2 - d1 = 7/0/2
    */
    
    // End of File
    

    Listing 12 Defines static members

    // date8.h
    
    // Forward declarations
    class istream;
    class ostream;
    
    class Date
    {
       int month;
       int day;
       int year;
    
       static int dtab[2][13];
    
    public:
       // Constructors
       Date();         // Get today's date (see .cpp file)
       Date(int m, int d, int y)
         {month = m; day = d; year = y;}
    
       // Accessor Functions
       int get_month() const
         {return month;}
       int get_day() const
         {return day;}
       int get_year() const
         {return year;}
    
       Date operator-(const Date& d2) const;
       Date& operator-()
         {month = -month; day = -day; year = -year;
          return *this;}
    
       int compare(const Date&) const;
    
       // Relational operators
       int operator<(const Date& d2) const
         {return compare{d2) < 0;}
       int operator<=(const Date& d2) const
         {return compare(d2) <= 0;}
       int operator>(const Date& d2) const
         {return compare(d2) > 0;}
       int operator>=(const Date& d2) const
         {return compare(d2) >= 0;}
       int operator==(const Date& d2) const
         {return compare(d2) == 0;}
       int operator!=(const Date& d2) const
         {return compare(d2) != 0;}
    
       // Stream I/O operators
       friend ostream& operator<<(ostream&, const Date&);
       friend istream& operator>>(istream&, Date&);
    
       static int isleap(int y)
         {return y%4 == 0 && y%100 != 0 || y%400 == 0;}
    };
    
    // End of File
    

    Listing 13 Final implementation of the Date class

    // date8.cpp
    
    #include <iostream.h>
    #include <time.h>
    #include <assert.h>
    #include "date8.h"
    
    // Must initialize statics outside the class definition
    int Date::dtab[2][13] =
    {
      {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
      {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
    };
    
    Date Date::operator-(const Date& d2) const
    {
       int months, days, years, prev_month, order;
       const Date * first, * last;
    
       // Must know which date is first
       if (compare(d2) <= 0)
       {
          // this <= d2
          order= -1;
          first = this;
          last = &d2;
       }
       else
       {
          order = 1;
          first = &d2;
          last = this;
       }
    
       // Compute the interval; first <= last
       years = last->year - first->year;
       months = last->month - first->month;
       days = last->day - first->day;
       assert(years >= 0 && months >= 0 && days >= 0);
    
       // Do obvious corrections (days before months!)
       //
       // This is a loop in case the previous month is
       // February, and days < -28.
       prev_month = last->month - 1;
       while (days < 0)
       {
          // Borrow from the previous month
          if (prev_month == 0)
             prev_month = 12;
          -months;
          days += dtab[isleap(last->year)][prev_month-];
       }
    
       if (months < 0)
       {
          // Borrow from the previous year
          -years;
          months += 12;
       }
    
       // Return a date object with the interval
       if (order == 1)
          return Date(-months,-days,-years);
       else
          return Date(months,days,years);
    }
    
    int Date::compare(const Date& d2) const
    {
       int months, days, years, order;
    
       years = year - d2.year;
       months = month - d2.month;
       days = day - d2.day;
    
       // return <0, 0, or >0, like strcmp()
       if (years == 0 && months == 0 && days == 0)
          return 0;
       else if (years == 0 && months == 0)
          return days;
       else if (years == 0)
          return months;
       else
          return years;
    }
    
    ostream& operator<<(ostream& os, const Date& d)
    {
       os << d.month << '/' << d.day << '/' << d.year;
       return os;
    }
    
    istream& operator>>(istream& is, Date& d)
    {
       char slash;
       is >> d.month >> slash >> d.day >> slash >> d.year;
       return is;
    }
    
    Date::Date()
    (
       // Get today's date
       time_t tval = time(0);
       struct tm *tmp= localtime(&tval);
    
       month = tmp->tm_mon+1;
       day = tmp->tm_mday;
       year = tmp->tm_year + 1900;
    }
    // End of File
    

    Listing 14 Gets today's date

    // tdate8.cpp:
    
    #include <iostream.h>
    #include "date8.h"
    
    main()
    {
       Date today, d2;
       cout << "Today's date is "<< today << endl;
       cout << "Enter another date: ";
       cin >> d2;
       cout << "today - d2 = "<< today - d2 << endl;
       cout << "d2 - today = "<< d2 - today << endl;
       return 0;
    }
    
    /* OUTPUT
    Today's date is 12/12/1992
    Enter another date: 1/1/1970
    today - d2 = 11/11/22
    d2 - today = -11/-11/-22
    */
    // End of File