user788171
user788171

Reputation: 17553

c++, getting milliseconds since midnight in local time

It's unbelievable how difficult the above is to accomplish in C++. I'm looking for a way to do this as efficiently as possible while still maintaining millisecond precision.

The solutions I have so far have either required a lot of code and function calls making the implementation slow, or they require me to change the code twice a year to account for daylight savings time.

The computer this will be running on is synced using ntp and should have direct access to the local time adjusted for DST. Can somebody with expertise on this share some solutions?

My platform is CentOS5, g++ 4.1.2, Boost 1.45, solution doesn't need to be portable, can be platform specific. It just needs to be quick and avoid twice a year code changing.

Upvotes: 5

Views: 7536

Answers (6)

Howard Hinnant
Howard Hinnant

Reputation: 219420

C++20 update to new answer for old question

3rd party libraries no longer required.

Assuming one wants to measure "physical seconds". I.e. if a daylight saving adjustment has been made since midnight, this computation takes that into account. This code is very similar to the code below, but shortened to just the version that throws an exception if there is no local midnight, or if there are two local midnights.

#include <chrono>
#include <iostream>

std::chrono::milliseconds
since_local_midnight(std::chrono::system_clock::time_point t = std::chrono::system_clock::now(),
                     std::chrono::time_zone const* zone = std::chrono::current_zone())
{
    using namespace std::chrono;
    zoned_time zt{zone, t};
    zt = floor<days>(zt.get_local_time());
    return floor<milliseconds>(t - zt.get_sys_time());
}

int
main()
{
    std::cout << since_local_midnight() << '\n';
}

Demo.

Explanation:

  • Combine the time_zone and the time_point into a zoned_time. A zoned_time is nothing more than a pairing of these two pieces of information. It makes it convenient to retrieve either the local time, or the UTC (sys) time.
  • Retrieve the local time and floor that to days precision to get the local midnight. Assign this new local time back into the zoned_time. This assignment will throw an exception if there is not a single midnight on this date in this time zone.
  • Retrieve the UTC equivalent of the local midnight and subtract that from the input time_point. Truncate the answer to milliseconds precision as requested in the OP.

See the more extended answer below for ways to deal with or work around the case where there are 0 or 2 local midnights.

New answer for old question.

Rationale for new answer: We have better tools now.

I'm assuming the desired result is "actual" milliseconds since the local midnight (getting the correct answer when there has been a UTC offset change since midnight).

A modern answer based on <chrono> and using this free, open-source library is very easy. This library has been ported to VS-2013, VS-2015, clang/libc++, macOS, and linux/gcc.

In order to make the code testable, I'm going to enable an API to get the time since midnight (in milliseconds) from any std::chrono::system_clock::time_point in any IANA time zone.

std::chrono::milliseconds
since_local_midnight(std::chrono::system_clock::time_point t,
                     const date::time_zone* zone);

And then to get the current time since midnight in the local time zone is easy to write on top of this testable primitive:

inline
std::chrono::milliseconds
since_local_midnight()
{
    return since_local_midnight(std::chrono::system_clock::now(), 
                                date::current_zone());
}

Writing the meat of the matter is relatively straight-forward:

std::chrono::milliseconds
since_local_midnight(std::chrono::system_clock::time_point t,
                     const date::time_zone* zone)
{
    using namespace date;
    using namespace std::chrono;
    auto zt = make_zoned(zone, t);
    zt = floor<days>(zt.get_local_time());
    return floor<milliseconds>(t - zt.get_sys_time());
}

The first thing to do is create a zoned_time which really does nothing at all but pair zone and t. This pairing is mainly just to make the syntax nicer. It actually doesn't do any computation.

The next step is to get the local time associated with t. That is what zt.get_local_time() does. This will have whatever precision t has, unless t is coarser than seconds, in which case the local time will have a precision of seconds.

The call to floor<days> truncates the local time to a precision of days. This effectively creates a local_time equal to the local midnight. By assigning this local_time back to zt, we don't change the time zone of zt at all, but we change the local_time of zt to midnight (and thus change its sys_time as well).

We can get the corresponding sys_time out of zt with zt.get_sys_time(). This is the UTC time which corresponds to the local midnight. It is then an easy process to subtract this from the input t and truncate the results to the desired precision.

If the local midnight is non-existent, or ambiguous (there are two of them), this code will throw an exception derived from std::exception with a very informative what().

The current time since the local midnight can be printed out with simply:

std::cout << since_local_midnight().count() << "ms\n";

To ensure that our function is working, it is worthwhile to output a few example dates. This is most easily done by specifying a time zone (I'll use "America/New_York"), and some local date/times where I know the right answer. To facilitate nice syntax in the test, another since_local_midnight helps:

inline
std::chrono::milliseconds
since_local_midnight(const date::zoned_seconds& zt)
{
    return since_local_midnight(zt.get_sys_time(), zt.get_time_zone());
}

This simply extracts the system_clock::time_point and time zone from a zoned_time (with seconds precision), and forwards it on to our implementation.

auto zt = make_zoned(locate_zone("America/New_York"), local_days{jan/15/2016} + 3h);
std::cout << zt << " is "
          << since_local_midnight(zt).count() << "ms after midnight\n";

This is 3am in the middle of the Winter which outputs:

2016-01-15 03:00:00 EST is 10800000ms after midnight

and is correct (10800000ms == 3h).

I can run the test again just by assigning a new local time to zt. The following is 3am just after the "spring forward" daylight saving transition (2nd Sunday in March):

zt = local_days{sun[2]/mar/2016} + 3h;
std::cout << zt << " is "
          << since_local_midnight(zt).count() << "ms after midnight\n";

This outputs:

2016-03-13 03:00:00 EDT is 7200000ms after midnight

Because the local time from 2am to 3am was skipped, this correctly outputs 2 hours since midnight.

An example from the middle of Summer gets us back to 3 hours after midnight:

zt = local_days{jul/15/2016} + 3h;
std::cout << zt << " is "
          << since_local_midnight(zt).count() << "ms after midnight\n";

2016-07-15 03:00:00 EDT is 10800000ms after midnight

And finally an example just after the Fall transition from daylight saving back to standard gives us 4 hours:

zt = local_days{sun[1]/nov/2016} + 3h;
std::cout << zt << " is "
          << since_local_midnight(zt).count() << "ms after midnight\n";

2016-11-06 03:00:00 EST is 14400000ms after midnight

If you want, you can avoid an exception in the case that midnight is non-existent or ambiguous. You have to decide before hand in the ambiguous case: Do you want to measure from the first midnight or the second?

Here is how you would measure from the first:

std::chrono::milliseconds
since_local_midnight(std::chrono::system_clock::time_point t,
                     const date::time_zone* zone)
{
    using namespace date;
    using namespace std::chrono;
    auto zt = make_zoned(zone, t);
    zt = make_zoned(zt.get_time_zone(), floor<days>(zt.get_local_time()),
                    choose::earliest);
    return floor<milliseconds>(t - zt.get_sys_time());
}

If you want to measure from the second midnight, use choose::latest instead. If midnight is non-existent, you can use either choose, and it will measure from the single UTC time point that borders the local time gap that midnight is in. This can all be very confusing, and that's why the default behavior is to just throw an exception with a very informative what():

zt = make_zoned(locate_zone("America/Asuncion"), local_days{sun[1]/oct/2016} + 3h);
std::cout << zt << " is "
          << since_local_midnight(zt).count() << "ms after midnight\n";

what():
2016-10-02 00:00:00.000000 is in a gap between
2016-10-02 00:00:00 PYT and
2016-10-02 01:00:00 PYST which are both equivalent to
2016-10-02 04:00:00 UTC

If you use the choose::earliest/latest formula, instead of an exception with the above what(), you get:

2016-10-02 03:00:00 PYST is 7200000ms after midnight

If you want to do something really tricky like use choose for non-existent midnights, but throw an exception for ambiguous midnights, that too is possible:

auto zt = make_zoned(zone, t);
try
{
    zt = floor<days>(zt.get_local_time());
}
catch (const date::nonexistent_local_time&)
{
    zt = make_zoned(zt.get_time_zone(), floor<days>(zt.get_local_time()),
                    choose::latest);
}
return floor<milliseconds>(t - zt.get_sys_time());

Because hitting such a condition is truly rare (exceptional), the use of try/catch is justified. However if you want to do it without throwing at all, there exists a low-level API within this library to achieve that.

Finally note that this long winded answer is really about 3 lines of code, and everything else is about testing, and taking care of rare exceptional cases.

Upvotes: 7

etoricky
etoricky

Reputation: 691

I have referred to the post [here] and made a change so that the below function can return the milliseconds since midnight in GMT time.

int GetMsSinceMidnightGmt(std::chrono::system_clock::time_point tpNow) {
    time_t tnow = std::chrono::system_clock::to_time_t(tpNow);
    tm * tmDate = std::localtime(&tnow);
    int gmtoff = tmDate->tm_gmtoff;
    std::chrono::duration<int> durTimezone(gmtoff); // 28800 for HKT
    // because mktime assumes local timezone, we shift the time now to GMT, then fid mid
    time_t tmid = std::chrono::system_clock::to_time_t(tpNow-durTimezone);
    tm * tmMid = std::localtime(&tmid);
    tmMid->tm_hour = 0;
    tmMid->tm_min = 0;
    tmMid->tm_sec = 0;
    auto tpMid = std::chrono::system_clock::from_time_t(std::mktime(tmMid));
    auto durSince = tpNow - durTimezone - tpMid;
    auto durMs = std::chrono::duration_cast<std::chrono::milliseconds>(durSince);
    return durMs.count();
}

If you want to have local time, it is much more easier.

Upvotes: 0

jxh
jxh

Reputation: 70502

You can run localtime_r, and mktime after adjusting the result of localtime_r to compute the value of "midnight" relative to the Epoch.

Edit: Pass now into the routine to avoid an unnecessary call to time.

time_t global_midnight;
bool checked_2am;

void update_global_midnight (time_t now, bool dst_check) {
    struct tm tmv;
    localtime_r(&now, &tmv);
    tmv.tm_sec = tmv.tm_min = tmv.tm_hour = 0;
    global_midnight = mktime(&tmv);
    checked_2am = dst_check || (now >= (global_midnight + 2*3600));
}

Assume global_midnight is initially 0. Then, you would adjust it's value at 2am, and the next day, so that it stays in sync with DST. When you call clock_gettime, you can compute the difference against global_midnight.

Edit: Since the OP wants to benchmark the routine, tweaking code for compilers that assume true to be the fast path, and round to nearest msec.

unsigned msecs_since_midnight () {
    struct timespec tsv;
    clock_gettime(CLOCK_REALTIME, &tsv);
    bool within_a_day = (tsv.tv_sec < (global_midnight + 24*3600));
    if (within_a_day)
        if (checked_2am || (tsv.tv_sec < (global_midnight + 2*3600))
            return ((tsv.tv_sec - global_midnight)*1000
                    + (tsv.tv_nsec + 500000)/1000000);
    update_global_midnight(tsv.tv_sec, within_a_day);
    return ((tsv.tv_sec - global_midnight)*1000
            + (tsv.tv_nsec + 500000)/1000000);
}

Upvotes: 0

user788171
user788171

Reputation: 17553

None of the answers provided really does what I need it to do. I've come up with something standalone that I think should work. If anybody spots any errors or can think of a faster method, please let me know. Present code takes 15 microseconds to run. I challenge SO to make something quicker (and I really hope SO succeeds =P)

inline int ms_since_midnight()
{
  //get high precision time
  timespec now;
  clock_gettime(CLOCK_REALTIME,&now);

  //get low precision local time
  time_t now_local = time(NULL);
  struct tm* lt = localtime(&now_local);

  //compute time shift utc->est
  int sec_local = lt->tm_hour*3600+lt->tm_min*60+lt->tm_sec;
  int sec_utc = static_cast<long>(now.tv_sec) % 86400;
  int diff_sec; //account for fact utc might be 1 day ahead
  if(sec_local<sec_utc) diff_sec = sec_utc-sec_local;
  else diff_sec = sec_utc+86400-sec_local;
  int diff_hour = (int)((double)diff_sec/3600.0+0.5); //round to nearest hour

  //adjust utc to est, round ns to ms, add
  return (sec_utc-(diff_hour*3600))*1000+(int)((static_cast<double>(now.tv_nsec)/1000000.0)+0.5);
}

Upvotes: 0

musiphil
musiphil

Reputation: 3847

It really depends on why you need "milliseconds since midnight" and what you plan to use it for.

Having said that, you need to take into account the fact that 3am doesn't really mean 3 hours since midnight, when DST is involved. If you really need "milliseconds since midnight" for some reason, you can get one Epoch time at midnight, another at 3am, and subtract the two.

But again, the notion of "midnight" may not be that stable in some cases; if a region's rule is to fall back from 1am to midnight when DST ends, you have two midnights within a day.

So I'm really doubtful of your dependence on "midnight". Typically, those broken-down times are for display and human understanding only, and all internal timekeeping is done with Epoch times.

Upvotes: 1

musiphil
musiphil

Reputation: 3847

If you're on Linux, gettimeofday gives the number of seconds/microseconds since the Epoch, which may help. But this really doesn't have anything to do with DST, since DST matters only with broken-down times (i.e. year, month, day, hour, minute, second).

To get the broken-down time, use gmtime or localtime with the "seconds" part of the result of gettimeofday:

struct timeval tv;
gettimeofday(&tv, 0);
struct tm *t = localtime(&tv.tv_sec);  // t points to a statically allocated struct

localtime gives the broken-down time in your local timezone, but it may be susceptible to DST. gmtime gives the broken-down time in UTC, which is immune to DST.

Upvotes: 0

Related Questions