I want to calculate the number of seconds since daystart. The problem is that std::chrono::floor
gives me the daystart in UTC as opposed to that of my local timezone. Compare this:
Demo:
#include <time.h>
#include <cstdio>
#include <iostream>
#include <cstdint>
#include <chrono>
#include <ctime>
int main() {
setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0", 1);
tzset();
const auto tp_daystart = std::chrono::floor<std::chrono::days>(std::chrono::system_clock::now());
const auto tp_now = std::chrono::system_clock::now();
const auto daysec = std::chrono::duration_cast<std::chrono::seconds>(tp_now - tp_daystart);
std::time_t ttp = std::chrono::system_clock::to_time_t(tp_now);
std::time_t ttp_s = std::chrono::system_clock::to_time_t(tp_daystart);
std::cout << "time start: " << std::ctime(&ttp_s) << " / time now: " << std::ctime(&ttp) << "seconds since start of day = " << daysec.count() << "\n";
}
This yields:
time start: Mon May 1 02:00:00 2023
/ time now: Mon May 1 16:26:04 2023
seconds since start of day = 51964
The seconds from start of day is of course wrong, as it calculates from 2am to 4pm when it should actually start at 00:00:00
. , as I do want to calculate the seconds since the start of this day in this timezone! How do I accomplish that?
This question actually has a few tricky details where decisions must be made on exactly what you want to happen when weird things like daylight savings complicates things. But whatever you want to happen, it can be done in chrono.
I'm going to assume that you want physical seconds since the start of the day. That means that if there was a UTC offset change between midnight and now (e.g. an hour got subtracted) that this subtraction will be taken into account.
This implies that we need to:
As you're interested in seconds precision, we'll truncate to that from the start, and then not worry about it further:
#include <chrono>
#include <format>
#include <iostream>
int
main()
{
using namespace std;
using namespace chrono;
auto utc_now = floor<seconds>(system_clock::now());
For readability purposes, I'm using local using directives
to cut down on the verbosity.
Next get the local time that corresponds to utc_now
:
auto local = zoned_time{current_zone(), utc_now};
current_zone()
looks up the currently set time zone. And zoned_time
is simply a handy "pair" which holds a time_zone const*
and a sys_time
together. One can use this structure to extract the local time. Or you can just format the local time out of it:
cout << "time now : " << format("{:%a %b %e %T %Y}", local) << '\n';
For me this just output my local time and date:
time now : Mon May 1 10:56:07 2023
Now to get the local midnight, the truncation to days
must happen in local time, not system time:
local = floor<days>(local.get_local_time());
This extracts the local time, truncates it to days
and then assigns that local time back into the zoned_time
. This will change the underlying UTC time point (sys_time) but not the time zone.
Now you can format local
again:
cout << "time start: " << format("{:%a %b %e %T %Y}", local) << '\n';
Example output:
time start: Mon May 1 00:00:00 2023
Finally, extract the UTC time from the local midnight (stored in local
) and subtract it from the starting UTC time:
auto delta = utc_now - local.get_sys_time();
cout << "seconds since start of day = " << delta << '\n';
Example output:
seconds since start of day = 39367s
There is one more complication: What if there existed a UTC offset change that caused local time to completely skip over the local midnight? Or what if there were two local midnights?
How do you want to handle that?
There are several possibilities:
You could ignore this possibility because you are sure your local time zone never does this. In that case the code above is fine as is. If it does happen, an exception will be thrown on the line of code that assigns the local midnight back into local
.
If there are two midnights, you want to choose the first one, and if there are zero midnights you want to start counting at whatever the first local time is that is after midnight. For example if local time skips from 23:30 to 00:30, one starts counting at 00:30 local time.
In this case change:
local = floor<days>(local.get_local_time());
to:
local = zoned_time{local.get_time_zone(),
floor<days>(local.get_local_time()),
choose::earliest};
This:
days
precision.zoned_time
with the same time_zone as local
. You do not want to call current_zone()
a second time in case you are on a moving mobile device.choose::earliest
to select the first local time in case there are two mappings from local to UTC. This will also map to the UTC time point associated with a gap in local time (zero midnights).zoned_time
back into local
.Now an exception will never be thrown, and you will always count seconds from the very first instant of today, even if there are also bits of yesterday counted too.
If you want the second midnight, so that there are never bits of yesterday counted, then change choose::earliest
to choose::latest
.
If you don't want to actually count physical seconds, but rather "calendrical seconds", then do the arithmetic in local time, instead of UTC. This will paradoxically not involve the complexity of changing UTC offsets during the day, and so be simpler. In this computation, 4am will always be 4 hours after midnight, even if there was a UTC offset change at 2am that set the local time back to 1am.
auto utc_now = floor<seconds>(system_clock::now());
auto local_now = zoned_time{current_zone(), utc_now}.get_local_time();
auto local_midnight = floor<days>(local_now);
auto delta = local_now - local_midnight;
cout << "time now : " << format("{:%a %b %e %T %Y}", local_now) << '\n';
cout << "time start: " << format("{:%a %b %e %T %Y}", local_midnight) << '\n';
cout << "seconds since start of day = " << delta << '\n';
In this version, local time is never mapped back to UTC, so there is no opportunity to discover that said mapping is not unique and subsequently throw an exception.
And to be clear, all three different versions of this code almost always give the same result. It is only when a UTC offset change is made between "now" and the previous local midnight, that these different versions give different results.