How to convert `std::filesystem::file_time_type` to a string using GCC 9
Asked Answered
S

1

14

I'm trying to format the modification time of a file as a string (UTC). The following code compiles with GCC 8, but not GCC 9.

#include <chrono>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <sstream>

namespace fs = std::filesystem;

int main() {
    fs::file_time_type file_time = fs::last_write_time(__FILE__);
    std::time_t tt = decltype(file_time)::clock::to_time_t(file_time);
    std::tm *gmt = std::gmtime(&tt);
    std::stringstream buffer;
    buffer << std::put_time(gmt, "%A, %d %B %Y %H:%M");
    std::string formattedFileTime = buffer.str();
    std::cout << formattedFileTime << '\n';
}

I tried both decltype(file_time)::clock and std::chrono::file_clock, using both C++17 and C++ 20, but neither of them worked.

$ g++-9 -std=c++2a -Wall -Wextra -lstdc++fs file_time_type.cpp

file_time_type.cpp: In function ‘int main()’:
file_time_type.cpp:12:50: error: ‘to_time_t’ is not a member of ‘std::chrono::time_point<std::filesystem::__file_clock>::clock’ {aka ‘std::filesystem::__file_clock’}
   12 |     std::time_t tt = decltype(file_time)::clock::to_time_t(file_time);
      |                                                  ^~~~~~~~~

The example on https://en.cppreference.com/w/cpp/filesystem/file_time_type mentions that it doesn't work on GCC 9, because C++20 will allow portable output, but I have no idea how to get it working. Shouldn't it work with GCC 9 if I just not use C++20?

I would prefer a C++17 solution with GCC 9, if possible.

Sheya answered 27/6, 2019 at 10:9 Comment(4)
Howard hinnant's date library should be able to print the time directly without conversion to time_tCyr
@AlanBirtles, thank you for the suggestion. I don't have any third party dependencies yet, so I'm a bit hesitant to add this library just to format a single date. Is there any alternative using the standard library?Sheya
You can use the c++20 date library (which is derived from Howards), its a header only library so using it is just a case of downloading the header from github and including itCyr
@AlanBirtles: It tried the conversion using Hinnant's date library header file (github.com/HowardHinnant/date/blob/master/include/date/date.h). However, I failed. Simple string conversion is only possible for system_clock but filesystem uses _File_time_clock. Finally, I think it's not an "As can easily be seen" thing. If you know the solution you could maybe post it. This question still has no accepted answer ;-). The current answer is obviously a workaround.Mope
P
18

As system_clock has to_time_t, the easiest way is to convert to it. This is not perfect (due to precision issues), but most of the time good enough and what I'm using on MSVC as well:

template <typename TP>
std::time_t to_time_t(TP tp)
{
    using namespace std::chrono;
    auto sctp = time_point_cast<system_clock::duration>(tp - TP::clock::now()
              + system_clock::now());
    return system_clock::to_time_t(sctp);
}

Update: As I'm not yet able to comment, some clarification on why this works and what can be a problem: It works because the difference between two time points of the same clock is easy, and for the second part, there is an extra template operator+ for duration and time point of different sources (2) and ratio differences will be taken care of in std::common_type.

The remaining issue is, that the two calls to now() are not at the same time, there is a slight risk of introducing a small conversion error due to the time difference between that calls. There is another question about C++11 clock conversions that goes into much more detail on error probabilities and tricks to reduce the error, but if you don't need a round-trip conversion with comparing results, but just want to format a time stamp, this smaller solution should be good enough.

So to complete the answer to the original question:

#include <chrono>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <sstream>

namespace fs = std::filesystem;

template <typename TP>
std::time_t to_time_t(TP tp)
{
    using namespace std::chrono;
    auto sctp = time_point_cast<system_clock::duration>(tp - TP::clock::now()
              + system_clock::now());
    return system_clock::to_time_t(sctp);
}

int main() {
    fs::file_time_type file_time = fs::last_write_time(__FILE__);
    std::time_t tt = to_time_t(file_time);
    std::tm *gmt = std::gmtime(&tt);
    std::stringstream buffer;
    buffer << std::put_time(gmt, "%A, %d %B %Y %H:%M");
    std::string formattedFileTime = buffer.str();
    std::cout << formattedFileTime << '\n';
}
Pallet answered 4/10, 2019 at 13:36 Comment(6)
why does it work? what if precisions of those clocks are different?Sheikh
@Pallet it's good practice to incorporate clarifications in response to to comments in the actual answer and not in an additional comment. If you could comment you could write a one-liner to create a ping in the commenter's message box; I'll do that.Numerous
@Sheikh Since Gulrak cannot write comments yet, apparently not even under his own posts: He updated his answer with a response regarding clocks with different ratios. (I assume that was what you asked about.)Numerous
@Peter-ReinstateMonica Sorry, I actually didn't see, that I could already comment my own posts, should have better read the SO introduction. Thanks for pointing me to it and for the ping.Pallet
Is there any improvement to the fact that calling two times now() would introduce a conversion error ? In my case this happens often ... :-(Cortez
@Cortez besides the suggestions in the linked question, that only reduces the error at the expense of more runtime, there is sadly not much you can do in C++17. Only in C++20 the clock type of std::filesystem::file_time_type is well defined as std::chrono::file_clock and brings the needed formatting/printing features.Pallet

© 2022 - 2024 — McMap. All rights reserved.