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

1

6

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

Thank you!

Chiastolite answered 24/3, 2017 at 22:47 Comment(2)
time_point is a mostly unitless instant in time. You must first convert it to a DateTime of some sort, which represents a calendar representation of a time. Then simply add one month.Mulvaney
Would std::chrono::hours(24*30) be good enough? Months are hard to deal with because their length varies.Simonnesimonpure
D
22

Overview

This is a very interesting question with a surprising number of 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/400 days:

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};

This is the simplest, and most efficient way to add months to a system_clock::time_point.

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/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;
}

If you don't explicitly check !ymd.ok() that is ok too. The only thing that can cause !ymd.ok() is for the day field to overflow. For example if you add a month to Oct 31, you'll get Nov 31. When you convert Nov 31 back to sys_days it will overflow to Dec 1, just like mktime. Or one could also declare an error on !ymd.ok() with an assert or exception. The choice of behavior is completely up to the client.

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

You might want to perform your calendrical computation in a specific timezone. The previous computation was with respect to 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. And C++20 standardizes this existing practice.

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 in the calendrical system, then convert back to local time, and finally back to system_clock::time_point (UTC):

#include "date/tz.h"

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

    // Get the current local time
    zoned_time lt{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.

The conversion from local time back to UTC is not guaranteed to be unique:

// Convert back to local time
lt = local_days{ymd} + time_of_day;

For example if the resultant local time falls within a daylight saving transition where the UTC offset is decreasing, then there exist two mappings from this local time to UTC. The default behavior is to throw an exception if this happens. However one can also preemptively choose the first chronological mapping or the second in the event there are two mappings by replacing this:

lt = local_days{ymd} + time_of_day;

with:

lt = zoned_time{lt.get_time_zone(),
                local_days{ymd} + time_of_day, choose::earliest};

(or choose::latest).

If the resultant local time falls within a daylight saving transition where the UTC offset is increasing, then the result is in a gap where there are zero mappings to UTC. In this case both choose::earliest and choose::latest map to the same UTC time which borders the local time gap.

An Alternative Time Zone Computation

Above I used current_zone() to pick up my current location, but I could have also used a specific time zone (e.g. "Asia/Tokyo"). If a different time zone has different daylight saving rules, and if the computation crosses a daylight saving boundary, then this could impact the result you get.

An Alternative Calendrical Computation

Instead of adding months to 2017-03-25, one might prefer to add months to the 4th Saturday of March 2017, resulting in the 4th Saturday of November 2017. The process is quite similar, one just converts to and from a different "calendar":

Instead of this:

    year_month_day ymd{ld};
    ymd += months{8};

one does this:

    year_month_weekday ymd{ld};
    ymd += months{8};

or even more concisely:

    auto ymd = year_month_weekday{ld} + months{8};

One can choose to do this computation in the sys_time system (UTC) or in a specific time zone, just like with the year_month_day calendar.

And one can choose to check for !ymd.ok() in the case that you start on the 5th Saturday, but the resulting month doesn't have 5 Saturdays. If you don't check, then the conversion back to sys_days or local_days will roll over to the first Saturday (for example) of the next month. Or you can snap back to the last Saturday of the month:

if (!ymd.ok())
    ymd = year_month_weekday{ymd.year()/ymd.month()/ymd.weekday()[last]};

Or you could assert or throw an exception on !ymd.ok(). And like above, one could choose what happens if the resultant local time does not have a unique mapping back to UTC.

There are lots of design choices to make. And they can each impact the result you get. And in hindsight, just doing the simple chronological computation may not be unreasonable. It all depends on your needs.

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 you're good to go.

For Simplicity

If all you want to do is calendrical computations with months and/or years, system_clock::time_point is probably the wrong data structure to start with. You can just work with year_month_day, never converting to or from sys_days or local_days. You can use parse and format directly with year_month_day.

There is even a year_month data structure that can perform years and months arithmetic so you don't even have to worry about the day field.

auto constexpr ymd = year{2024}/January/15 + months{15};
static_assert(ymd == year{2025}/April/15);
auto constexpr ym = year{2024}/January + months{15};
static_assert(ym == year{2025}/April);
static_assert(ym/15 == year{2025}/April/15);

std::istringstream stream{"2024-01-15"};
year_month_day ymd2;
stream >> parse("%F", ymd2);
string s = format("%F", ymd2 + months{15});  // "2025-04-15"

Gone are the differences between chronological and calendrical arithmetic, the danger of day-field overflow, differences between time zones, and worries about converting from local time to UTC not being a unique mapping.

Dwaynedweck answered 25/3, 2017 at 15:19 Comment(4)
In this line years is 365.2425 days, or 146097/400 days I assume that 365.2425 days is the exact length of the year. I found this unusual to me because I got used to 365 days, with 1 leap year every 4 years =). Though I'm still unsure about 146097/400 days. Why these numbers? Is that because this is just one way of representing 365.2425 days without using a type with floating point? BTW, this is an excellent explanation! Thank you!Hiccup
The old Roman calendar had 365 days. The Julian calendar modified that to 365 days with 1 leap year every 4 years (365.25 days). The Gregorian calendar modified the Julian calendar in 1582 by skipping leap years for years evenly divided by 100, but not 400. I.e. 1700, 1800 and 1900 were not leap years, but 1600 and 2000 were. It is the Gregorian calendar that is being modeled here: Every 400 years there are exactly 146097 days: 400*365 + 400/4 - 3.Dwaynedweck
Due to static_assert( 1500y/2/last != 1500y/2/29 );, will there be some kind of std::chrono::historical::year_month_day ?Kennith
Nope. C++20 has a proleptic civil calendar: en.wikipedia.org/wiki/Proleptic_Gregorian_calendar This is consistent with ISO 8601. This policy sidesteps the issue that countries switched from the Julian calendar to the Gregorian on different dates. However, users can write their own calendar which interoperates with C++20 <chrono>. I've written several examples as proof of concept. Here's one of them: howardhinnant.github.io/date/julian.htmlDwaynedweck

© 2022 - 2024 — McMap. All rights reserved.