std::format %z specifier not working for local_time
Asked Answered
O

2

5

I want to get a timestamp in the format "YYYY-MM-DD hh::mm::ss.fff +zz".

I thought I could do this with the %z specifier for std::format but I'm getting this error on MSVC:

error C7595: 'std::basic_format_string<char,const std::chrono::time_point<
std::chrono::local_t,std::chrono::duration<std::chrono::system_clock::rep,std::chrono::system_clock::period>> &>::basic_format
_string': call to immediate function is not a constant expression
#include <chrono>
#include <format>
#include <iostream>

std::string get_current_time_and_date() {
  auto const time =
      std::chrono::current_zone()->to_local(std::chrono::system_clock::now());
  return std::format("{:%F %T %z}", time);
}

int main() {
  std::cout << get_current_time_and_date() << "\n";
  return 0;
}

If I use "{:%F %T}" as the format string, it works fine (but obviously does not include the time zone). This tells me that the error message is nonsensical, but is there some way around this? Is this a bug in the compiler?


For posterity, here's a workaround using good old strftime:

std::string get_current_time_and_date() {
  auto time = std::chrono::system_clock::now();
  auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
                time.time_since_epoch()) %
            1000;
  auto localT = std::chrono::system_clock::to_time_t(time);
  std::tm timeStruct;
  localtime_s(&timeStruct, &localT);
  std::ostringstream oss;
  const size_t TIME_STR_LEN = 32;

  char buf[TIME_STR_LEN];
  std::strftime(buf, TIME_STR_LEN, "%F %T", &timeStruct);

  char timezonebuf[16];
  std::strftime(timezonebuf, sizeof(timezonebuf), " %z", &timeStruct);

  oss << buf << '.' << std::setfill('0') << std::setw(3) << ms.count() << timezonebuf;
  return oss.str();
}
Over answered 23/6, 2023 at 18:22 Comment(0)
A
8

std::chrono::time_zone::to_local returns an std::chrono::local_time, which is an std::chrono::time_point that uses an std::chrono::local_t as its first template argument. However, a quote from std::chrono::local_t on cppreference (emphasis mine):

The class local_t is a pseudo-clock /…/ to indicate that the time point represents local time with respect of a not-yet-specified time zone.

In other words, such a timepoint (that the std::chrono::time_zone::to_local returns) is timezone-agnostic, it does not contain any information about the timezone used.

If you try and change

return std::format("{:%F %T %z}", time);

to

return std::vformat("{:%F %T %z}", std::make_format_args(time));

you'll see it throws an std::format_error with the description "format argument does not contain the information required by the chrono-specs".

There is a remark (#13) in the draft standard:

If %Z, %z, or a modified version of %z is used, an exception of type format_error is thrown.

In other words, %z is not supported for local_time (because the time zone info is not contained in the local_time anyway).


As suggested by Barry, perhaps std::choro::zoned_time is what you had in mind instead. It does allow the %z specifier. Here is a modified example:

#include <chrono>
#include <format>
#include <iostream>
#include <string>

std::string get_current_time_and_date()
{
    const auto zt{ std::chrono::zoned_time{
        std::chrono::current_zone(),
        std::chrono::system_clock::now() } };
    return std::format("{:%F %T %z}", zt);
}

int main()
{
    std::cout << get_current_time_and_date() << '\n';
}
Antediluvian answered 23/6, 2023 at 21:22 Comment(2)
Can add that what OP probably wanted wasn't current_zone()->to_local(now) but rather zoned_time(current_zone(), now), which does work with that specifier.Adapt
@Adapt Thanks for the suggestion, I've added an example.Antediluvian
G
2

At heap underrun explains, you can't use %z with std::chrono::local_times. You can however work around that by extracting the offset from the time zone's std::chrono::local_info.

Example:

std::string get_current_time_and_date() {
    auto const time = std::chrono::current_zone()->to_local(std::chrono::system_clock::now());

    auto zi = std::chrono::current_zone()->get_info(time);
    bool neg = false;
    auto offset = zi.first.offset; // see note below
    if (offset < std::chrono::seconds{}) {
        offset = -offset;
        neg = true;
    }
    return std::format("{:%F %T} {}{:%H%M}", time, neg ? '-' : '+', offset);
}

Note: Read the documentation for std:.chrono::local_info. You may need to check zi.result too.

Glynda answered 23/6, 2023 at 19:57 Comment(2)
Plus one from me, I think your answer does have useful info. (I don't know who voted this down; not me.)Antediluvian
@heapunderrun Thanks! It's clearly not as useful as your answer, but still, it may have piece in it that may be useful to someone :-)Glynda

© 2022 - 2024 — McMap. All rights reserved.