How can I add months to a chrono::system_clock::time_point value?
Thank you!
How can I add months to a chrono::system_clock::time_point value?
Thank you!
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.
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.
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/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.
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.
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.
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 you're good to go.
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.
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 static_assert( 1500y/2/last != 1500y/2/29 );
, will there be some kind of std::chrono::historical::year_month_day ? –
Kennith <chrono>
. I've written several examples as proof of concept. Here's one of them: howardhinnant.github.io/date/julian.html –
Dwaynedweck © 2022 - 2024 — McMap. All rights reserved.
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. – Mulvaneystd::chrono::hours(24*30)
be good enough? Months are hard to deal with because their length varies. – Simonnesimonpure