Date/Time Manipulations
C++ provides std::chrono::system_clock::time_point
that represents time, that works well with other classes such as durations (std::chrono::milliseconds
, etc.). However, it isn't always sufficient for date time manipulations because of daylight savings and leap years.
Since time_point
represents time interval since the clock's epoch, adding 24h
to a time_point
with daylight saving taking place within that 24-hour period means the result time will be off by an hour.
The solution is to work with std::tm
data structure.
Converting To std::tm
If you have a time string, this answer from Stack Overflow demonstrates how you can convert to std::tm
from that string:
std::tm tm;
memset(&tm, sizeof(tm), 0); // tm = { } doesn't work with all compilers.
strptime("Thu Jan 9 2014 12:35:34", "%a %b %d %Y %H:%M:%S", &tm);
auto t = std::chrono::system_clock::from_time_t(std::mktime(&tm));
If you have a std::chrono::system_clock::time_point
, the process is more involved but there is an answer on Stack Overflow.
Converting Back from std::tm
Converting std::tm
back to std::chrono::system_clock::time_point
is pretty easy by using time_t
:
auto t = std::chrono::system_clock::from_time_t(std::mktime(&tm));
Using std::tm
The reason why we want to do all manipulations in std::tm
is because std::mktime()
will normalize the results. You'll want to do some simple verifications and make sure the compiler you are using do things correctly.
Future Time
To derive a new time point, simply add to the fields within std::tm
and it is okay to have the value go out-of-range. For example, January 1st, 2017 can be represented this way:
std::tm tm;
tm.tm_mday = 1; // tm_mday has range [1, 31]
tm.tm_mon = 0; // tm_mon has range [0, 11]
tm.tm_year = 2017 - 1900; // tm_year represent years since 1900
To derive 50 days from our date:
tm.tm_mday += 50;
And after calling std::mktime()
you'll find tm_mon
updated to 2
(March) and tm_mday
to 19
(the 19th). This will work even if we are working with leap years. Go ahead and change year to 120
(year 2020 is a leap year) and verify the result day of month is 18
, since there is an extra day in Feburary.
Last Day of Month
Unlike all other fields, tm_mday
doesn't have range starting from 0
. It is special because (at least with my compiler) setting this field to 0
will give me last day of the month I specify, again taking into consideration if we are dealing with a leap year. While it isn't too difficult to figure out if a year is a leap year or not (year must be evenly divisible by 4 but not 100, or divisible by 4, 100, and 400), simply setting a field to 0
in a code path you already have is a logical and simple way to get more maintainable code.
Days of the Week
It isn't so easy to figure out what day it is given a date, but std::mktime()
will do it for you. It'll ignore tm_wday
field when reading in std::tm
but will update that field when the call returns.