Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Advanced C++

You're reading from   Advanced C++ Master the technique of confidently writing robust C++ code

Arrow left icon
Product type Paperback
Published in Oct 2019
Publisher
ISBN-13 9781838821135
Length 762 pages
Edition 1st Edition
Languages
Arrow right icon
Authors (5):
Arrow left icon
Olena Lizina Olena Lizina
Author Profile Icon Olena Lizina
Olena Lizina
Rakesh Mane Rakesh Mane
Author Profile Icon Rakesh Mane
Rakesh Mane
Gazihan Alankus Gazihan Alankus
Author Profile Icon Gazihan Alankus
Gazihan Alankus
Brian Price Brian Price
Author Profile Icon Brian Price
Brian Price
Vivek Nagarajan Vivek Nagarajan
Author Profile Icon Vivek Nagarajan
Vivek Nagarajan
+1 more Show less
Arrow right icon
View More author details
Toc

Table of Contents (11) Chapters Close

About the Book 1. Anatomy of Portable C++ Software 2A. No Ducks Allowed – Types and Deduction FREE CHAPTER 2B. No Ducks Allowed – Templates and Deduction 3. No Leaks Allowed - Exceptions and Resources 4. Separation of Concerns - Software Architecture, Functions, and Variadic Templates 5. The Philosophers' Dinner – Threads and Concurrency 6. Streams and I/O 7. Everybody Falls, It's How You Get Back Up – Testing and Debugging 8. Need for Speed – Performance and Optimization 1. Appendix

Activity 2: Implementing classes for Date Calculations

In this activity, we will implement two classes, Date and Days that will make it very easy for us to work with dates and the time differences between them. Let's get started:

  1. Load the prepared project from the Lesson3/Activity02 folder and configure the Current Builder for the project to be CMake Build (Portable). Build and configure the launcher and run the unit tests. We recommend that the name that's used for the tests runner is L3A2datetests. The project has dummy files and one failing test.
  2. Open the date.hpp file in the editor and add the following lines inside the basic Date class to allow access to the stored values:

    int Day()   const {return m_day;}

    int Month() const {return m_month;}

    int Year()  const {return m_year;}

  3. Open the dateTests.cpp file and add the following code to the DateTest class:

    void VerifyDate(const Date& dt, int yearExp, int monthExp, int dayExp) const

    {

        ASSERT_EQ(dayExp, dt.Day());

        ASSERT_EQ(monthExp, dt.Month());

        ASSERT_EQ(yearExp, dt.Year());

    }

    Normally, you would refactor this test as the tests develop, but we will pull it out up front.

  4. Replace ASSERT_FALSE() in the existing test with the following test:

    Date dt;

    VerifyDate(dt, 1970, 1, 1);

  5. Rebuild and run the tests – they should now all pass.
  6. Add the following test:

    TEST_F(DateTest, Constructor1970Jan2)

    {

        Date dt(2, 1, 1970);

        VerifyDate(dt, 1970, 1, 2);

    }

  7. To make this test we need to add the following two constructors to the Date class:

    Date() = default;

    Date(int day, int month, int year) :

            m_year{year}, m_month{month}, m_day{day}

    {

    }

  8. We now need to introduce the functions to convert to/from the date_t type. Add the following alias to the date.hpp file inside our namespace:

    using date_t=int64_t;

  9. To the Date class, add the declaration of the following method:

    date_t ToDateT() const;

  10. Then, add the following test:

    TEST_F(DateTest, ToDateTDefaultIsZero)

    {

        Date dt;

        ASSERT_EQ(0, dt.ToDateT());

    }

  11. As we are doing (TDD), we add the minimal implementation of the method to pass the test.

    date_t Date::ToDateT() const

    {

        return 0;

    }

  12. Now, we add the next test:

    TEST_F(DateTest, ToDateT1970Jan2Is1)

    {

        Date dt(2, 1, 1970);

        ASSERT_EQ(1, dt.ToDateT());

    }

  13. We continue to add one more test and then another, all the time refining the algorithm in ToDateT() firstly to deal with dates in 1970, then 1-Jan-1971, and then a date in 1973, which means we span one leap year, and so on. The full set of tests that are used to develop the ToDateT() method are as follows:

    TEST_F(DateTest, ToDateT1970Dec31Is364)

    {

        Date dt(31, 12, 1970);

        ASSERT_EQ(364, dt.ToDateT());

    }

    TEST_F(DateTest, ToDateT1971Jan1Is365)

    {

        Date dt(1, 1, 1971);

        ASSERT_EQ(365, dt.ToDateT());

    }

    TEST_F(DateTest, ToDateT1973Jan1Is1096)

    {

        Date dt(1, 1, 1973);

        ASSERT_EQ(365*3+1, dt.ToDateT());

    }

    TEST_F(DateTest, ToDateT2019Aug28Is18136)

    {

        Date dt(28, 8, 2019);

        ASSERT_EQ(18136, dt.ToDateT());

    }

  14. To pass all of these tests, we add the following items to the declaration of the Date class:

    public:

        static constexpr int EpochYear = 1970;

        static constexpr int DaysPerCommonYear = 365;

        static constexpr int YearsBetweenLeapYears = 4;

    private:

        int GetDayOfYear(int day, int month, int year) const;

        bool IsLeapYear(int year) const;

        int CalcNumberLeapYearsFromEpoch(int year) const;

  15. The implementation of ToDateT() and the supporting methods in date.cpp is as follows:

    namespace {

    int daysBeforeMonth[2][12] =

    {

        { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 204, 334}, // Common Year

        { 0, 31, 50, 91, 121, 152, 182, 213, 244, 274, 205, 335}  // Leap Year

    };

    }

    namespace acpp::date

    {

    int Date::CalcNumberLeapYearsFromEpoch(int year) const

    {

        return (year-1)/YearsBetweenLeapYears

                                       - (EpochYear-1)/YearsBetweenLeapYears;

    }

    int Date::GetDayOfYear(int day, int month, int year) const

    {

        return daysBeforeMonth[IsLeapYear(year)][month-1] + day;

    }

    bool Date::IsLeapYear(int year) const

    {

        return (year%4)==0;   // Not full story, but good enough to 2100

    }

    date_t Date::ToDateT() const

    {

        date_t value = GetDayOfYear(m_day, m_month, m_year) - 1;

        value += (m_year-EpochYear) * DaysPerCommonYear;

        date_t numberLeapYears = CalcNumberLeapYearsFromEpoch(m_year);

        value += numberLeapYears;

        return value;

    }

    }

  16. Now that ToDateT() is working, we turn to its inverse, that is, FromDateT(). Again, we build up the tests one at a time to develop the algorithm over a range of dates. The following tests were used:

    TEST_F(DateTest, FromDateT0Is1Jan1970)

    {

        Date dt;

        dt.FromDateT(0);

        ASSERT_EQ(0, dt.ToDateT());

        VerifyDate(dt, 1970, 1, 1);

    }

    TEST_F(DateTest, FromDateT1Is2Jan1970)

    {

        Date dt;

        dt.FromDateT(1);

        ASSERT_EQ(1, dt.ToDateT());

        VerifyDate(dt, 1970, 1, 2);

    }

    TEST_F(DateTest, FromDateT364Is31Dec1970)

    {

        Date dt;

        dt.FromDateT(364);

        ASSERT_EQ(364, dt.ToDateT());

        VerifyDate(dt, 1970, 12, 31);

    }

    TEST_F(DateTest, FromDateT365Is1Jan1971)

    {

        Date dt;

        dt.FromDateT(365);

        ASSERT_EQ(365, dt.ToDateT());

        VerifyDate(dt, 1971, 1, 1);

    }

    TEST_F(DateTest, FromDateT1096Is1Jan1973)

    {

        Date dt;

        dt.FromDateT(1096);

        ASSERT_EQ(1096, dt.ToDateT());

        VerifyDate(dt, 1973, 1, 1);

    }

    TEST_F(DateTest, FromDateT18136Is28Aug2019)

    {

        Date dt;

        dt.FromDateT(18136);

        ASSERT_EQ(18136, dt.ToDateT());

        VerifyDate(dt, 2019, 8, 28);

    }

  17. Add the following declarations to the header file:

    public:

        void FromDateT(date_t date);

    private:

        int CalcMonthDayOfYearIsIn(int dayOfYear, bool IsLeapYear) const;

  18. Use the following implementation since the preceding tests are added one at a time:

    void Date::FromDateT(date_t date)

    {

        int number_years = date / DaysPerCommonYear;

        date = date - number_years * DaysPerCommonYear;

        m_year = EpochYear + number_years;

        date_t numberLeapYears = CalcNumberLeapYearsFromEpoch(m_year);

        date -= numberLeapYears;

        m_month = CalcMonthDayOfYearIsIn(date, IsLeapYear(m_year));

        date -= daysBeforeMonth[IsLeapYear(m_year)][m_month-1];

        m_day = date + 1;

    }

    int Date::CalcMonthDayOfYearIsIn(int dayOfYear, bool isLeapYear) const

    {

        for(int i = 1 ; i < 12; i++)

        {

        if ( daysBeforeMonth[isLeapYear][i] > dayOfYear)

                return i;

        }

        return 12;

    }

  19. Now that we have the supporting routines ready, we can implement the real feature of the Date class difference between two dates and determine the new date by adding a number of days. Both of these operations need a new type (class) Days.
  20. Add the following implementation of Days to the header (above Date):

    class Days

    {

    public:

        Days() = default;

        Days(int days) : m_days{days}     {    }

        operator int() const

        {

            return m_days;

        }

    private:

        int m_days{0};

    };

  21. The first operator will be an addition of Days to Date. Add the following method declaration (inside the public section of the Date class) :

    Date& operator+=(const Days& day);

  22. Then, add the inline implementation (outside the Date class) to the header file:

    inline Date operator+(const Date& lhs, const Days& rhs )

    {

        Date tmp(lhs);

        tmp += rhs;

        return tmp;

    }

  23. Write the following tests to verify the sum operation:

    TEST_F(DateTest, AddZeroDays)

    {

        Date dt(28, 8, 2019);

        Days days;

        dt += days;

        VerifyDate(dt, 2019, 8, 28);

    }

    TEST_F(DateTest, AddFourDays)

    {

        Date dt(28, 8, 2019);

        Days days(4);

        dt += days;

        VerifyDate(dt, 2019, 9, 1);

    }

  24. The actual implementation of the sum operation is simply based on the two support methods

    Date& Date::operator+=(const Days& day)

    {

        FromDateT(ToDateT()+day);

        return *this;

    }

  25. Add the following test:

    TEST_F(DateTest, AddFourDaysAsInt)

    {

        Date dt(28, 8, 2019);

        dt += 4;

        VerifyDate(dt, 2019, 9, 1);

    }

  26. When we run the tests, they all build and this test passes. But this is not the desired outcome. We do not want them to be able to add naked integers to our dates. (A future version may add months and years, so what does adding an integer mean?). To make this fail by causing the build to fail, we change the Days constructor to be explicit:

    explicit Days(int days) : m_days{days}     {    }

  27. Now the build fails, so we need to fix the test by changing the addition line to cast to Days as follows:

    dt += static_cast<Days>(4);

    All tests should pass again.

  28. The final functionality we want is the difference between two dates. Here are the tests that were used to verify the implementation:

    TEST_F(DateTest, DateDifferences27days)

    {

        Date dt1(28, 8, 2019);

        Date dt2(1, 8, 2019);

        Days days = dt1 - dt2;

        ASSERT_EQ(27, (int)days);

    }

    TEST_F(DateTest, DateDifferences365days)

    {

        Date dt1(28, 8, 2019);

        Date dt2(28, 8, 2018);

        Days days = dt1 - dt2;

        ASSERT_EQ(365, (int)days);

    }

  29. Add the following declaration of the function to the public section of the Date class in the header file:

    Days operator-(const Date& rhs) const;

  30. Add the following code after the Date class in the header file:

    inline Days Date::operator-(const Date& rhs) const

    {

        return Days(ToDateT() - rhs.ToDateT());

    }

    Because we made the Days constructor explicit, we must call it in the return statement. With all these changes in place, all the tests should pass.

  31. Configure L3A2date as a datetools binary and open main.cpp in the editor. Remove the comment from the definition of ACTIVITY2:

    #define ACTIVITY2

  32. Build and then run the sample application. This will produce the following output:
Figure 3.53: Output of successful Date sample application
Figure 3.53: Output of successful Date sample application

We have implemented all of the requirements of the Date and Days classes and delivered them all with unit tests. The unit tests allowed us to implement incremental functionality to build up the two complicated algorithms, ToDateT and FromDateT which form the underlying support for the functionality that we wanted to deliver.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime