7
\$\begingroup\$

I was at CppCon last week and watched a clever talk about using ranges and calendars. This is just me trying to play around with the new technology.

Step 1: dump out a calendar.

Usage:

cal <year>

Output:

         January
    Sun Mon Tue Wed Thu Fri Sat
                01  02  03  04
    05  06  07  08  09  10  11
    ...etc

#include <iostream>
#include <time.h>
#include <iomanip>

enum WeekDay {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

class Calander
{
    /*
     * Line 0:              <Month>
     * Line 1: Sun Mon Tue Wed Thu Fri Sat
     * Line 2: Week 1 (May need leading space)
     * Line 3: Week 2
     * Line 4: Week 3
     * Line 5: Week 5 or (blank)
     * Line 6: Week 6 or (blank)
     * Line 7: (blank)
     */
    class WeekLine
    {
        static int getDaysInMonth(int month, bool isLeap)
        {
            static constexpr int  normal[]    = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
            static constexpr int  leap[]      = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

            return isLeap ? leap[month] : normal[month];
        }
        static bool isLeapYear(int year)
        {
            return (year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0));
        }

        static int getOffsetOfFirstDay(int year, int month)
        {
            struct ::tm     timeS;
            timeS.tm_sec    = 0;
            timeS.tm_min    = 0;
            timeS.tm_hour   = 0;
            timeS.tm_mday   = 1;
            timeS.tm_mon    = month;
            timeS.tm_year   = year - 1900;
            timeS.tm_isdst  = 0;
            timeS.tm_zone   = nullptr;
            timeS.tm_gmtoff = 0;
            timegm(&timeS);

            // TODO:
            // Other week start days apart from Sunday.
            return timeS.tm_wday;    // day of week (Sunday = 0)
        }

        int         year;
        bool        isLeap;
        int         month;
        int         dayOfMonth;
        WeekDay     firstDayOfWeek;
        int         offset;
        int         linesPrinted;

        public:
            WeekLine()
                : year(-1)
                , month(0)
                , dayOfMonth(0)
            {}
            WeekLine(int year, WeekDay firstDayOfWeek)
                : year(year)
                , isLeap(isLeapYear(year))
                , month(0)
                , dayOfMonth(0)
                , firstDayOfWeek(firstDayOfWeek)
                , offset(getOffsetOfFirstDay(year, 0))
                , linesPrinted(0)
            {}


            bool operator!=(WeekLine const& rhs)   {return year * 10'000 + month * 100 + dayOfMonth != rhs.year * 10'000 + rhs.month * 100 + rhs.dayOfMonth;}
            WeekLine& operator*()      {return *this;}
            WeekLine& operator++()
            {
                ++linesPrinted;
                if (linesPrinted > 2)
                {
                    dayOfMonth  += 7 - offset;
                    offset      = 0;
                }
                if (linesPrinted == 9)
                {
                    // We have reached the end of the current
                    // month. So reset to next month.
                    linesPrinted= 0;
                    month       += 1;
                    dayOfMonth  = 0;
                    offset      = getOffsetOfFirstDay(year, month);
                }
                if (month >= 12)
                {
                    // We have reached the end.
                    // Make the same as the end iterator.
                    year        = -1;
                    month       = 0;
                    dayOfMonth  = 0;
                }
                return *this;
            }

            friend std::ostream& operator<<(std::ostream& str, WeekLine const& val)
            {
                static const std::string month[] = {"January", "February", "March",
                                                    "April",   "May",      "June",
                                                    "July",    "August",   "September",
                                                    "October", "November", "December"};
                if (val.linesPrinted == 0)
                {
                    std::string const& name = month[val.month];
                    return str << std::string((26 - name.size()) / 2, ' ') << name << "\n";
                }
                if (val.linesPrinted == 1) {
                    return str << "Sun Mon Tue Wed Thu Fri Sat \n";
                }
                for(int loop = 0;loop < val.offset;++loop) {
                    str << "    ";
                }
                int max = std::min(7, getDaysInMonth(val.month, val.isLeap) - val.dayOfMonth);
                for(int loop = 0;loop < (max - val.offset);++loop) {
                    str << std::setw(2) << std::setfill('0') << (val.dayOfMonth + loop + 1) << "  ";
                }
                return str << "\n";
            }
    };
    int         year;
    WeekDay     firstDayOfWeek;
    public:
        Calander(int year)
            : year(year)
            , firstDayOfWeek(Sun)
        {}
        WeekLine  begin()   const   {return WeekLine(year, firstDayOfWeek);}
        WeekLine  end()     const   {return WeekLine();}

};

int main(int argc, char* argv[])
{
    if (argc != 2) {
        return 1;
    }
    int year    = std::atoi(argv[1]);
    Calander    cal(year);
    for(auto week: cal) {
        std::cout << week;
    }
}
\$\endgroup\$
  • \$\begingroup\$ Ah, but can you also print it in 3 month column format? See also e.g. this attempt to avoid range-based code: pastebin.com/AyfLaBVU The key advantage of range-based code is not that you can't do things using clever use of nested loops (like the linked example), but that maintaining and debugging such nested loopy code is more burdensome. The range-based code lets you transform your input in a series of sequential steps to the desired output. Like Unix pipes and filters. \$\endgroup\$ – TemplateRex Sep 29 '15 at 19:54
  • \$\begingroup\$ @TemplateRex: That's going to be the next version. Be patient. \$\endgroup\$ – Martin York Sep 29 '15 at 20:09
4
\$\begingroup\$
  • One small change

    class WeekLine
        : public std::iterator<std::forward_iterator_tag, void, void, void, void>
    

    will allow it to be used like this

    std::copy(cal.begin(), cal.end(), 
        std::ostream_iterator<decltype(cal.begin())>(std::cout));
    

    Somewhat useless, but at least it'll be usable with the standard library.

    Speaking of which, is there a reason why your WeekLine is private? At least do something like this:

    using iterator = WeekLine;
    

    That way I can replace decltype(cal.begin()) with Calander::iterator.

  • You should use <ctime> instead of <time.h>.

  • std::atoi comes from <cstdlib>. Don't omit headers since it may fail to compile on another implementation.

  • Why not use an enum class for WeekDay? You never use it, for numerical values or anything else, so there's no point in injecting Sun, Mon, etc. into the global namespace. Make it private like the other classes.

  • In one case of declaring an array, you use constexpr, in another you use const. constexpr is implicitly const, so for consistency, choose one or the other. There also probably isn't any difference semantically in these situations.

  • I noticed you using C++14 digit separators. In this case, I recommend against it since 1) support may not be available on all compilers, 2) it's confusing to read even for people who know what they are. At least format your function:

    bool operator!=(WeekLine const& rhs)   
    {
       return year * 10'000 
         + month * 100 
         + dayOfMonth != (rhs.year * 10'000) 
         + (rhs.month * 100) 
         + rhs.dayOfMonth;
    }
    
  • Here's a more efficient way to check for a leap year:

    if year is not divisible by 4 then not leap year
    else if year is not divisible by 100 then leap year
    else if year is divisible by 400 then leap year
    else not leap year
    

    This "most-efficient" pseudo-code simply changes the order of tests so the division by 4 takes place first, followed by the less-frequently occurring tests. Because "year" does not divide by four 75-percent of the time, the algorithm ends after only one test in three out of four cases.

    And the actual code:

    if ((year & 3) == 0 && ((year % 25) != 0 || (year & 15) == 0))
    {
        /* leap year */
    }
    

    A comprehensive explanation/rationale can be found in the linked page.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.