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:
For readability purposes, I'm using local
using directivesto cut down on the verbosity.Next get the local time that corresponds to
utc_now:current_zone()looks up the currently set time zone. Andzoned_timeis simply a handy "pair" which holds atime_zone const*and asys_timetogether. One can use this structure to extract the local time. Or you can just format the local time out of it:For me this just output my local time and date:
Now to get the local midnight, the truncation to
daysmust happen in local time, not system time:This extracts the local time, truncates it to
daysand then assigns that local time back into thezoned_time. This will change the underlying UTC time point (sys_time) but not the time zone.Now you can format
localagain:Example output:
Finally, extract the UTC time from the local midnight (stored in
local) and subtract it from the starting UTC time:Example output:
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:
to:
This:
daysprecision.zoned_timewith the same time_zone aslocal. You do not want to callcurrent_zone()a second time in case you are on a moving mobile device.choose::earliestto 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_timeback intolocal.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::earliesttochoose::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.
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.