Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Add months to chrono::system_clock::time_point

How can I add months to a chrono::system_clock::time_point value?

Thank you!

like image 424
Antonio Petricca Avatar asked Mar 24 '17 22:03

Antonio Petricca


1 Answers

Overview

This is a very interesting question with several answers. The "correct" answer is something you must decide for your specific application.

With months, you can choose to do either chronological computations or calendrical computations. A chronological computation deals with regular units of time points and time durations, such as hours, minutes and seconds. A calendrical computation deals with irregular calendars that mainly serve to give days memorable names.

The Chronological Computation

If the question is about some physical process months in the future, physics doesn't care that different months have different lengths, and so a chronological computation is sufficient:

  • The baby is due in 9 months.

  • What will the weather be like here 6 months from now?

In order to model these things, it may be sufficient to work in terms of the average month. One can create a std::chrono::duration that has precisely the length of an average Gregorian (civil) month. It is easiest to do this by defining a series of durations starting with days:

days is 24 hours:

using days = std::chrono::duration
    <int, std::ratio_multiply<std::ratio<24>, std::chrono::hours::period>>;

years is 365.2425 days, or 146097/400days:

using years = std::chrono::duration
    <int, std::ratio_multiply<std::ratio<146097, 400>, days::period>>;

And finally months is 1/12 of years:

using months = std::chrono::duration
    <int, std::ratio_divide<years::period, std::ratio<12>>>;

Now you can easily compute 8 months from now:

auto t = system_clock::now() + months{8};

Important note: This computation does not preserve the time of day, or even the day of the month.

The Calendrical Computation

It is also possible to add months while preserving time of day and day of month. Such computations are calendrical computations as opposed to chronological computations.

After choosing a calendar (such as the Gregorian (civil) calendar, the Julian calendar, or perhaps the Islamic, Coptic or Ethiopic calendars — they all have months, but they are not all the same months), the process is:

  1. Convert the system_clock::time_point to the calendar.

  2. Perform the months computation in the calendrical system.

  3. Convert the new calendar time back into system_clock::time_point.

You can use Howard Hinnant's free, open-source date/time library to do this for a few calendars. Here is what it looks like for the civil calendar:

#include "date.h"

int
main()
{
    using namespace date;
    using namespace std::chrono;

    // Get the current time
    auto now = system_clock::now();
    // Get a days-precision chrono::time_point
    auto sd = floor<days>(now);
    // Record the time of day
    auto time_of_day = now - sd;
    // Convert to a y/m/d calendar data structure
    year_month_day ymd = sd;
    // Add the months
    ymd += months{8};
    // Add some policy for overflowing the day-of-month if desired
    if (!ymd.ok())
        ymd = ymd.year()/ymd.month()/last;
    // Convert back to system_clock::time_point
    system_clock::time_point later = sys_days{ymd} + time_of_day;
}

For grins I just ran this, and compared it with now + months{8} and got:

now   is           2017-03-25 15:17:14.467080
later is           2017-11-25 15:17:14.467080  // calendrical computation
now + months{8} is 2017-11-24 03:10:02.467080  // chronological computation

This gives a rough "feel" for how the calendrical computation differs from the chronological computation. The latter is perfectly accurate on average; it just has a deviation from the calendrical on the order of a few days. And sometimes the simpler (latter) solution is close enough, and sometimes it is not. Only you can answer that question.

The Calendrical Computation — Now with timezones

Finally, you might want to perform your calendrical computation in a specific timezone. The previous computation was UTC.

Side note: system_clock is not specified to be UTC, but the de facto standard is that it is Unix Time which is a very close approximation to UTC.

You can use Howard Hinnant's free, open-source timezone library to do this computation. This is an extension of the previously mentioned datetime library.

The code is very similar, you just need to convert to local time from UTC, then to a local calendar, do the computation then back to local time, and finally back to system_clock::time_point (UTC):

#include "tz.h"

int
main()
{
    using namespace date;
    using namespace std::chrono;

    // Get the current local time
    auto lt = make_zoned(current_zone(), system_clock::now());
    // Get a days-precision chrono::time_point
    auto ld = floor<days>(lt.get_local_time());
    // Record the local time of day
    auto time_of_day = lt.get_local_time() - ld;
    // Convert to a y/m/d calendar data structure
    year_month_day ymd{ld};
    // Add the months
    ymd += months{8};
    // Add some policy for overflowing the day-of-month if desired
    if (!ymd.ok())
        ymd = ymd.year()/ymd.month()/last;
    // Convert back to local time
    lt = local_days{ymd} + time_of_day;
    // Convert back to system_clock::time_point
    auto later = lt.get_sys_time();
}

Updating our results I get:

now   is           2017-03-25 15:17:14.467080
later is           2017-11-25 15:17:14.467080  // calendrical: UTC
later is           2017-11-25 16:17:14.467080  // calendrical: America/New_York
now + months{8} is 2017-11-24 03:10:02.467080  // chronological computation

The time is an hour later (UTC) because I preserved the local time (11:17am) but the computation started in daylight saving time, and ended in standard time, and so the UTC equivalent is later by 1 hour.

I used current_zone() to pick up my current location, but I could have also used a specific time zone (e.g. "Asia/Tokyo").

C++20 Update

As I write this update, technical work has ceased on C++20, and it looks like we will have a new C++ standard later this year (just administrative work left to do to complete C++20).

The advice in this answer translates well to C++20:

  1. For the chronological computation, std::chrono::months is supplied by <chrono> so you don't have to compute it yourself.

  2. For the UTC calendrical computation, loose #include "date.h" and use instead #include <chrono>, and drop using namespace date, and things will just work.

  3. For the time zone sensitive calendrical computation, loose #include "tz.h" and use instead #include <chrono>, drop using namespace date, and replace make_zoned with zoned_time, and you're good to go.

like image 189
Howard Hinnant Avatar answered Sep 29 '22 09:09

Howard Hinnant