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.

tttapa
  • 1,397
  • 12
  • 26
  • Howard hinnant's [date library](https://github.com/HowardHinnant/date) should be able to print the time directly without conversion to `time_t` – Alan Birtles Jun 27 '19 at 10:31
  • @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? – tttapa Jun 27 '19 at 10:37
  • 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 it – Alan Birtles Jun 27 '19 at 10:52
  • @AlanBirtles: It tried the conversion using Hinnant's date library header file (https://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. – Semjon Mössinger Mar 22 '20 at 22:43

1 Answers1

15

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';
}
Gulrak
  • 726
  • 7
  • 10
  • why does it work? what if precisions of those clocks are different? – fen Oct 31 '19 at 20:56
  • @Gulrak 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. – Peter - Reinstate Monica Nov 04 '19 at 21:48
  • @fen 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.) – Peter - Reinstate Monica Nov 04 '19 at 21:49
  • @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. – Gulrak Nov 05 '19 at 05:43
  • Is there any improvement to the fact that calling two times now() would introduce a conversion error ? In my case this happens often ... :-( – Jesko Jan 08 '23 at 22:45
  • @Jesko besides the suggestions in the [linked question](https://stackoverflow.com/questions/35282308/convert-between-c11-clocks), 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. – Gulrak Jan 16 '23 at 10:14