Stephen G Tuggy
Stephen G Tuggy

Reputation: 1141

C++ - How to format a file's last modified date and time in the user's preferred date/time locale format in a platform-independent, thread-safe manner

I want to retrieve a given file's last-modified date and time, and format it as a string. I want to do so in a way that is thread- and memory-safe; cross-platform compatible; and does not require the use of custom libraries. The date and time should be formatted according to the user's preferred locale and date/time format, as returned by the operating system.

Using C++11/14 features is fine. So is using Boost. Speed of execution does not particularly matter.

Here is what I have at the moment:

    std::string text;
    text += filename;
    text  = "Savegame: "+text+lf+"_________________"+lf;
    {
        text += "Saved on: ";
        const boost::filesystem::path file_name_path{filename};
        const boost::filesystem::path save_dir_path{getSaveDir()};
        const boost::filesystem::path& full_file_path = boost::filesystem::absolute(file_name_path, save_dir_path);
        std::time_t last_saved_time{};
        std::stringstream last_saved_string_stream{};
        last_saved_string_stream.exceptions(std::ios_base::failbit);
        try
        {
            std::locale users_preferred_locale{std::locale("")};
            boost::gregorian::date_facet output_facet{};
            last_saved_string_stream.imbue(users_preferred_locale);
            last_saved_string_stream.imbue(std::locale(last_saved_string_stream.getloc(), &output_facet));
            output_facet.format("%x");
            last_saved_time = boost::filesystem::last_write_time(full_file_path);
            auto last_saved_time_point{boost::chrono::system_clock::from_time_t(last_saved_time)};
            last_saved_string_stream << last_saved_time_point;
        }
        catch (boost::filesystem::filesystem_error& fse)
        {
            VS_LOG_AND_FLUSH(fatal, (boost::format("Filesystem Error determining savegame's last saved date/time: %1%") % fse.what()));
        }
        catch (std::exception& e)
        {
            VS_LOG_AND_FLUSH(fatal, (boost::format("General Error determining savegame's last saved date/time: %1%") % e.what()));
        }
        catch (...)
        {
            VS_LOG_AND_FLUSH(fatal, "Really bad error determining savegame's last saved date/time! What just happened??");
        }
        text += last_saved_string_stream.str() + lf;
    }

VS_LOG_AND_FLUSH is a macro (I know, I know) that calls BOOST_LOG_TRIVIAL with the specified log level and message, and then flushes all the boost log sinks, plus stdout and stderr.

The implementation of getSaveDir() is not important for purposes of this question. Let's just accept that it returns a std::string containing the directory in which this program's savegames are stored.

Even getting this far was way more difficult than it should have been. I found surprisingly few examples of people actually using these libraries -- especially using them together.

This code compiles, but crashes silently when run on Windows. Meanwhile, on Linux, it runs, but displays the file's timestamp as some huge number of nanoseconds since January 1st, 1970. This is not the correct format. The format should be something like "9/10/2021 5:30 PM" in the US, or "10/09/2021 17:30:00" in some European countries, for example.

What am I doing wrong? And what should I do instead?

I have tried a couple of different format strings on the output_facet.format(... line. I've tried "%c" and "%x", each with the same result.

Upvotes: 2

Views: 1141

Answers (2)

Stephen G Tuggy
Stephen G Tuggy

Reputation: 1141

First: As it turns out, getSaveDir() was not reliably returning the expected value, after all. That is now fixed.

Second: My catch clauses depended on more formatted string output, after some string output already failed. Not necessarily a good idea.

Third: My catch clauses did not actually exit the program. VS_LOG_AND_FLUSH does not include this functionality.

Fourth: The specific set of header files I included at the top of this .cpp file turned out to matter. Even the order might matter. Here is what I found worked:

#include <boost/filesystem.hpp>
#include <boost/chrono/time_point.hpp>
#include <boost/chrono/io/time_point_io.hpp>
#include <boost/chrono/chrono.hpp>

#include <iostream>
#include <sstream>
#include <chrono>
#include <locale>

Finally: The specific output function I was looking for was boost::chrono::time_fmt.

Here is the code I have now:

    string text;
    text += filename;
    text  = "Savegame: "+text+lf+"_________________"+lf;
    try
    {
        text += "Saved on: ";
        const boost::filesystem::path file_name_path{filename};
        const boost::filesystem::path save_dir_path{getSaveDir()};
        const boost::filesystem::path full_file_path{boost::filesystem::absolute(file_name_path, save_dir_path)};
        std::time_t last_saved_time{boost::filesystem::last_write_time(full_file_path)};
        boost::chrono::system_clock::time_point last_saved_time_point{boost::chrono::system_clock::from_time_t(last_saved_time)};
        std::ostringstream last_saved_string_stream{};
        last_saved_string_stream << boost::chrono::time_fmt(boost::chrono::timezone::local, "%c")
                                 << last_saved_time_point;
        text += last_saved_string_stream.str() + lf;
    }
    catch (boost::filesystem::filesystem_error& fse)
    {
        VS_LOG_AND_FLUSH(fatal, "boost::filesystem::filesystem_error encountered");
        VS_LOG_AND_FLUSH(fatal, fse.what());
        VSExit(-6);
    }
    catch (std::exception& e)
    {
        VS_LOG_AND_FLUSH(fatal, "std::exception encountered");
        VS_LOG_AND_FLUSH(fatal, e.what());
        VSExit(-6);
    }
    catch (...)
    {
        VS_LOG_AND_FLUSH(fatal, "unknown exception type encountered!");
        VSExit(-6);
    }

This seems to work reliably on both Windows and Linux. (Code not yet tested on macOS, but it does compile on Mac, at least.)

Update 2022-05-13

After following some of the suggestions given in other answers, here is the code I have now. Note that now it uses an ostringstream for pretty much all string concatenation:

    std::ostringstream ss{};
    ss << "Savegame: " << filename << lf;
    ss << "_________________" << lf;
    try {
        ss << "Saved on: ";
        const boost::filesystem::path file_name_path{filename};
        const boost::filesystem::path save_dir_path{getSaveDir()};
        const boost::filesystem::path full_file_path{boost::filesystem::absolute(file_name_path, save_dir_path)};
        std::time_t last_saved_time{boost::filesystem::last_write_time(full_file_path)};
        boost::chrono::system_clock::time_point
                last_saved_time_point{boost::chrono::system_clock::from_time_t(last_saved_time)};
        ss << boost::chrono::time_fmt(boost::chrono::timezone::local, "%c")
                << last_saved_time_point
                << lf;
    }
    catch (boost::filesystem::filesystem_error &fse) {
        VS_LOG_AND_FLUSH(fatal, "boost::filesystem::filesystem_error encountered:");
        VS_LOG_AND_FLUSH(fatal, fse.what());
        VSExit(-6);
    }
    catch (std::exception &e) {
        VS_LOG_AND_FLUSH(fatal, "std::exception encountered:");
        VS_LOG_AND_FLUSH(fatal, e.what());
        VSExit(-6);
    }
    catch (...) {
        VS_LOG_AND_FLUSH(fatal, "unknown exception type encountered!");
        VSExit(-6);
    }
// ...

Upvotes: 2

sehe
sehe

Reputation: 393084

You're close. Some notes:

  • facets are owned by their locales and you should not have taken the address of a local variable there:

     boost::gregorian::date_facet output_facet{};
     last_saved_string_stream.imbue(std::locale(last_saved_string_stream.getloc(), &output_facet));
     output_facet.format("%x");
    

    should be

     last_saved_string_stream.imbue(std::locale(last_saved_string_stream.getloc(),
         new boost::gregorian::date_facet("%x")));
    
  • In fact, the redundant imbue could probably be merged:

     std::stringstream last_saved_string_stream{};
     last_saved_string_stream.exceptions(std::ios_base::failbit);
    
     std::locale users_preferred_locale{std::locale("")};
     last_saved_string_stream.imbue(users_preferred_locale);
    
         last_saved_string_stream.imbue(std::locale(last_saved_string_stream.getloc(), new boost::gregorian::date_facet("%x")));
    

    Can become

     std::stringstream ss{};
     ss.exceptions(std::ios_base::failbit);
    
     ss.imbue(std::locale(std::locale(""), new boost::gregorian::date_facet("%x")));
    
  • Note that you can reduce the excess scope on the stringstream and time_t variables

  • The conversion to chrono time_point is not helping. The facets only apply to Boost DateTime library types, not Chrono. Instead, convert to ptime or local_date_time:

     ss << boost::posix_time::from_time_t(
         fs::last_write_time(full_file_path));
    
  • Why combine string-concatenation, std iostreams and boost::format all in one function?

  • You can probably go without the imbue, according to my test:

Live On Coliru

#include <boost/filesystem.hpp>
#include <boost/format.hpp>
#include <boost/chrono.hpp>
#include <boost/date_time.hpp>
namespace fs = boost::filesystem;
using namespace std::string_literals;

// mocks
enum severity { fatal };
template <typename Msg>
static void VS_LOG_AND_FLUSH(severity, Msg const& msg) { std::clog << msg << std::endl; }
fs::path getSaveDir() { return fs::current_path(); }
static constexpr char const* lf = "\n";

std::string DescribeSaveGame(std::string filename, bool do_imbue)
{
    std::stringstream ss;
    ss.exceptions(std::ios_base::failbit);
    if (do_imbue) {
        ss.imbue(std::locale(std::locale(""),
                    new boost::gregorian::date_facet("%x")));
    }

    ss << "Savegame: " << filename << lf << "_________________" << lf;
    try {
        ss << "Saved on: ";
        ss << boost::posix_time::from_time_t(
                  fs::last_write_time(fs::absolute(filename, getSaveDir())))
           << lf;

        return ss.str();
    } catch (std::exception const& e) {
        VS_LOG_AND_FLUSH(fatal,
            "General Error determining savegame's last saved date/time: "s +
                e.what());
    } catch (...) {
        VS_LOG_AND_FLUSH(fatal,
                         "Really bad error determining savegame's last saved "
                         "date/time! What just happened??");
    }
    return "error"; // TODO?
}

int main() {
    std::cout << DescribeSaveGame("test.bin", true) << std::endl;
    std::cout << DescribeSaveGame("test.bin", false) << std::endl;
}

Printing

Savegame: test.bin
_________________
Saved on: 2021-Sep-11 14:29:09

Savegame: test.bin
_________________
Saved on: 2021-Sep-11 14:29:09

Simplest

The above implies that you can probably simplify the whole thing down to a lexical_cast:

std::string DescribeSaveGame(std::string const& filename) {
    auto save_date = boost::posix_time::from_time_t(
        last_write_time(absolute(filename, getSaveDir())));

    return "Savegame: " + filename + lf + "_________________" + lf +
        "Saved on: " + boost::lexical_cast<std::string>(save_date) + lf;
}

Live On Coliru

Still prints

Savegame: test.bin
_________________
Saved on: 2021-Sep-11 14:37:10

Upvotes: 2

Related Questions