Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use std::chrono to extract number of seconds passed since midnight in local time

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?

like image 327
glades Avatar asked Oct 30 '25 00:10

glades


2 Answers

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:

  1. Get the UTC time now.
  2. Get the UTC time of the local midnight.
  3. Subtract the two UTC times.

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:

  1. 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.

  2. 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:

  • Extracts the local time, and truncates it to days precision.
  • Forms a new 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.
  • Uses 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).
  • Assign this new 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.

like image 106
Howard Hinnant Avatar answered Nov 01 '25 15:11

Howard Hinnant


You are looking for std::chrono::time_zone.

setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0", 1);
tzset();
std::chrono::time_zone const* time_zone = std::chrono::current_zone();
if (!time_zone)
  return -1;

once you have a valid time_zone this is easy

const auto local = tz.to_local(sys);
const auto local_daystart = std::chrono::floor<std::chrono::days>(local);
const auto sys_daystart = tz.to_sys(local_daystart);

time_zone was added in c++20. You can directly search for a specific time zone in the system database, or use the current time zone of your process (as above).

You have to accept failure, because sometimes a system doesn't have a time zone.

like image 41
Yakk - Adam Nevraumont Avatar answered Nov 01 '25 15:11

Yakk - Adam Nevraumont



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!