Date/Time Manipulations

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.

References

  • std::tm data structure definition. Note how range of seconds changed from [0, 61] to [0, 60]. There should be 61 seconds total to account for leap seconds, and [0, 60] is what you want with normal seconds falling in range [0, 59].
  • mktime().