Search code examples
c++boost

Performing date time arithmetic in custom date time class


I have a very naive struct representing date time which I would like to perform arithmetic on:

struct MyDateTime
{
    MyDateTime(int year, int month, int day, uint64_t nanos);

    int year;
    int month;
    int day;
    uint64_t nanosSinceMidnight;
};

I'd like to be able to add/subtract MyDateTime from another MyDateTime.

My idea was to make my struct a wrapper and use Boost internally.

I looked at Boost Posix Time:

https://www.boost.org/doc/libs/1_55_0/doc/html/date_time/examples.html#date_time.examples.time_math

But this seems to only be doing time math (not accounting for the date component).

I looked at Boost Gregorian Date but I couldn't see any time argument in the constructors.

What is the simplest way to use Boost, so I can perform datetime arithmetic?


Solution

  • As you may have realized by now, dates cannot be added.

    Dates and timestamps are mathematically akin to tensors, in that their difference type is in a different domain.

    When you commented that time_duration doesn't include a date, you still had a point though.

    Because the time_duration might be the time-domain difference type (the difference type ptime) but we need an analog for the date-part of ptime, which is boost::gregorian::date.

    Boost Gregorian dates are basically blessed tuples of (yyyy,mm,dd).So a natural difference type would just be a signed integral number of days. And that's exactly* what boost::gregorian::date_duration is:

    boost::gregorian::date_duration  x = date{} - date{};
    boost::posix_time::time_duration y = ptime{} - ptime{};
    

    Because that type is implemented in the Gregorian module you will get correct differences, even with special cases like leap days and other anomalies: https://www.calendar.com/blog/gregorian-calendar-facts/

    So, you could in fact use that type as a difference type, just for the ymd part.

    Simplify

    The good news is, you don't have to bother: boost::posix_time::ptime encapsulates a full boost::gregorian::date, hence when you get a boost::posix_time::time_duration from subtracting ptimes, you will already get the number of days ciphered in:

    #include <boost/date_time.hpp>
    
    int main() {
        auto now = boost::posix_time::microsec_clock::local_time();
    
        auto later    = now + boost::posix_time::hours(3);
        auto tomorrow = later + boost::gregorian::days(1);
        auto ereweek  = later - boost::gregorian::weeks(1);
    
        std::cout << later << " is " << (later - now) << " later than " << now
                  << std::endl;
        std::cout << tomorrow << " is " << (tomorrow - later) << " later than " << later
                  << std::endl;
        std::cout << ereweek << " is " << (ereweek - now) << " later than " << now
                  << std::endl;
    }
    

    Starting from the current time we add 3 hours, 1 day and then subtract a week. It prints: Live On Coliru:

    2021-Mar-28 01:50:45.095670 is 03:00:00 later than 2021-Mar-27 22:50:45.095670
    2021-Mar-29 01:50:45.095670 is 24:00:00 later than 2021-Mar-28 01:50:45.095670
    2021-Mar-21 01:50:45.095670 is -165:00:00 later than 2021-Mar-27 22:50:45.095670
    

    Note that 24h is 1 day, and -165h is (7*24 - 3) hours ago.

    There's loads of smarts in the Gregorian calendar module:

    std::cout << date{2021, 2, 1} - date{2020, 2, 1} << std::endl; // 366
    std::cout << date{2020, 2, 1} - date{2019, 2, 1} << std::endl; // 365
    

    Taking into account leap days. But also knowing the varying lengths of a calendar month in context:

    auto term = boost::gregorian::months(1);
    
    for (date origin : {date{2021, 2, 17}, date{2021, 3, 17}}) {
        std::cout << ((origin + term) - origin) << std::endl;
    };
    

    Prints 28 and 31 respectively.

    Applying It To Your Type

    I'd suggest keeping with the library difference type, as clearly you had not previously given it any thought that you needed one. By simply creating some interconversions you can have your cake and eat it too:

    struct MyDateTime {
        MyDateTime(int year = 1970, int month = 1, int day = 1, uint64_t nanos = 0)
            : year(year),
              month(month),
              day(day),
              nanosSinceMidnight(nanos) {}
    
        operator ptime() const {
            return {date(year, month, day),
                    microseconds(nanosSinceMidnight / 1'000)};
        }
    
        explicit MyDateTime(ptime const& from)
            : year(from.date().year()),
              month(from.date().month()),
              day(from.date().day()),
              nanosSinceMidnight(from.time_of_day().total_milliseconds() * 1'000) {}
    
      private:
        int      year;
        int      month;
        int      day;
        uint64_t nanosSinceMidnight;
    };
    

    Now, I would question the usefulness of keeping your MyDateTime type, but I realize legacy code exists, and sometimes you require a longer time period while moving away from it.

    Nanoseconds

    Nanosecond precision is not enabled by default. You need to [opt in to use that](https://www.boost.org/doc/libs/1_58_0/doc/html/date_time/details.html#boost-common-heading-doc-spacer:~:text=To%20use%20the%20alternate%20resolution%20(96,the%20variable%20BOOST_DATE_TIME_POSIX_TIME_STD_CONFIG%20must%20be%20defined). In the sample below I do.

    Be careful that al the translation units in your project use the define, or you will cause ODR violations.

    Live Demo

    Adding some convenience operator<< as well:

    Live On Coliru

    #define BOOST_DATE_TIME_POSIX_TIME_STD_CONFIG
    #include <boost/date_time.hpp>
    #include <vector>
    
    using boost::posix_time::ptime;
    using boost::gregorian::date;
    using boost::posix_time::nanoseconds;
    
    struct MyDateTime {
        MyDateTime(MyDateTime const&) = default;
        MyDateTime& operator=(MyDateTime const&) = default;
    
        MyDateTime(int year = 1970, int month = 1, int day = 1, uint64_t nanos = 0)
            : year(year),
              month(month),
              day(day),
              nanosSinceMidnight(nanos) {}
    
        operator ptime() const {
            return {date(year, month, day), nanoseconds(nanosSinceMidnight)};
        }
    
        /*explicit*/ MyDateTime(ptime const& from)
            : year(from.date().year()),
              month(from.date().month()),
              day(from.date().day()),
              nanosSinceMidnight(from.time_of_day().total_nanoseconds()) {}
    
      private:
        friend std::ostream& operator<<(std::ostream& os, MyDateTime const& dt) {
            auto save = os.rdstate();
            os << std::dec << std::setfill('0') << std::setw(4) << dt.year << "/"
               << std::setw(2) << dt.month << "/" << std::setw(2) << dt.day << " +"
               << dt.nanosSinceMidnight;
            os.setstate(save);
            return os;
        }
    
        int      year;
        int      month;
        int      day;
        uint64_t nanosSinceMidnight;
    };
    
    int main() {
        namespace g = boost::gregorian;
        namespace p = boost::posix_time;
        using p::time_duration;
    
        std::vector<time_duration> terms{p::seconds(30), p::hours(-168),
                                         p::minutes(-15),
                                         p::nanoseconds(60'000'000'000 * 60 * 24)};
    
        for (auto mydt : {MyDateTime{2021, 2, 17}, MyDateTime{2021, 3, 17}}) {
            std::cout << "---- Origin: " << mydt << "\n";
            for (time_duration term : terms) {
                mydt = ptime(mydt) + term;
                std::cout << "Result: " << mydt << "\n";
            }
        };
    }
    

    Prints

    ---- Origin: 2021/02/17 +0
    Result: 2021/02/17 +30000000000
    Result: 2021/02/10 +30000000000
    Result: 2021/02/09 +85530000000000
    Result: 2021/02/10 +85530000000000
    ---- Origin: 2021/03/17 +0
    Result: 2021/03/17 +30000000000
    Result: 2021/03/10 +30000000000
    Result: 2021/03/09 +85530000000000
    Result: 2021/03/10 +85530000000000