Book Image

Advanced C++

By : Gazihan Alankus, Olena Lizina, Rakesh Mane, Vivek Nagarajan, Brian Price
5 (1)
Book Image

Advanced C++

5 (1)
By: Gazihan Alankus, Olena Lizina, Rakesh Mane, Vivek Nagarajan, Brian Price

Overview of this book

C++ is one of the most widely used programming languages and is applied in a variety of domains, right from gaming to graphical user interface (GUI) programming and even operating systems. If you're looking to expand your career opportunities, mastering the advanced features of C++ is key. The book begins with advanced C++ concepts by helping you decipher the sophisticated C++ type system and understand how various stages of compilation convert source code to object code. You'll then learn how to recognize the tools that need to be used in order to control the flow of execution, capture data, and pass data around. By creating small models, you'll even discover how to use advanced lambdas and captures and express common API design patterns in C++. As you cover later chapters, you'll explore ways to optimize your code by learning about memory alignment, cache access, and the time a program takes to run. The concluding chapter will help you to maximize performance by understanding modern CPU branch prediction and how to make your code cache-friendly. By the end of this book, you'll have developed programming skills that will set you apart from other C++ programmers.
Table of Contents (11 chapters)
7
6. Streams and I/O

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.