How can I add months to a chrono::system_clock::time_point value?
Thank you!
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.
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.
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:
Convert the system_clock::time_point
to the calendar.
Perform the months computation in the calendrical system.
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"
).
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:
For the chronological computation, std::chrono::months
is supplied by <chrono>
so you don't have to compute it yourself.
For the UTC calendrical computation, loose #include "date.h"
and use instead #include <chrono>
, and drop using namespace date
, and things will just work.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With