c++c++20c++-chrono

`std::chrono::parse()`ing std::string in Eastern time


I'm trying to convert std::strings such as "2024-11-19 09:30:00.037000" **that I know to always be in Eastern time) to std::chrono objects and vice versa, but I can't get anything to work well.

I mostly work with std::chrono::time_point by calling std::chrono::system_clock::now() a lot, but whenever I write to disk, I'd like to write everything in Eastern time with microsecond precision.

I know this question has been asked a lot. I've spent a few days trying to adapt answers on this site such as this one and this one, but to no avail.

For example, this won't compile:

timepoint_t parse_eastern_time(const std::string& dtstr) {
    std::istringstream iss{dtstr};
    zoned_time_t tp{};
    iss >> std::chrono::parse("%Y-%m-%d %H:%M:%S.%f",tp);
    return tp.get_sys_time();
}

and this runs but chops off everything subseconds

timepoint_t parse_eastern_time(const std::string& dtstr) {
    // Format of data stored in eastern time: "2024-11-19 09:30:00.037000"
    std::tm tm = {};
    std::stringstream ss(dtstr);
    ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S.%f");
    timepoint_t tp = clock_type::from_time_t(std::mktime(&tm));
    return tp;
}

Here's all my testing code in case you want to run it on Godbolt:

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

namespace timestuff {

// type alias for clock for entire app
using clock_type = std::chrono::high_resolution_clock;

using duration_t = clock_type::duration;
using timepoint_t = std::chrono::time_point<clock_type, duration_t>;
using zoned_time_t = std::chrono::zoned_time<duration_t>;

std::string to_eastern_string(zoned_time_t tp)
{
    std::ostringstream oss;
    //oss << std::format("{0:%Y-%m-%d %T%Z}", tp);
    //oss << std::format("{0:%Y-%m-%d %T}", tp);
    oss << std::format("{0:%Y-%m-%d %T}", tp);
    return oss.str();
}

timepoint_t parse_eastern_time(const std::string& dtstr) {
    

    ///std::istringstream iss{dtstr};
    ///zoned_time_t tp{};
    ///iss >> std::chrono::parse("%Y-%m-%d %H:%M:%S.%f",tp);
    ///return tp.get_sys_time();

    // Format of data stored in eastern time: "2024-11-19 09:30:00.037000"
    std::tm tm = {};
    std::stringstream ss(dtstr);
    ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S.%f");
    timepoint_t tp = clock_type::from_time_t(std::mktime(&tm));
    return tp;
}

}

int main() {

        using namespace timestuff;

        auto ny_time_zone = std::chrono::locate_zone("America/New_York");
        timestuff::zoned_time_t ny_time(ny_time_zone, clock_type::now());
        std::string now_string = to_eastern_string(ny_time);
        std::cout << now_string <<"\n";

        // yay
        std::string dtstr = "2024-11-19 09:30:00.037000";
        std::cout << to_eastern_string(parse_eastern_time(dtstr)) << "\n";

        std::string dtstr2 = "2024-11-21 15:59:59.823";
        std::cout << to_eastern_string(parse_eastern_time(dtstr2)) << "\n";


    return 0;
}

Solution

  • zoned_time is a higher-level API and often more convenient to work with. However in this case, I suspect zoned_time might be abstracting so much that it is actually confusing the situation. So I'm going to drop down a level in abstraction and not use it. Instead I'll show a solution with just local_time and time_zone.

    local_time is a std::chrono::time_point that refers to a local time with respect to some not-yet-specified time_zone. And one can associate a time_zone with a local_time by using the time_zone to translate back and forth between local_time and sys_time. sys_time is just a time_point based on system_clock and represents UTC.

    Starting at the top of your code:

    Don't use high_resolution_clock. It has no portable relationship with any calendar. In practice it is always a type alias for system_clock or steady_clock, and which is dependent on platform. Just choose system_clock or steady_clock. For parsing and formatting date/times in the civil calendar, your choice drops to just system_clock:

    using clock_type = std::chrono::system_clock;
    

    Since you want to maintain precision at microseconds, let's do that uniformly throughout your program:

    using duration_t = std::chrono::microseconds;
    using timepoint_t = std::chrono::sys_time<duration_t>;
    

    sys_time is just a type alias for time_point<system_clock, Duration>.

    Because of this expression in main: to_eastern_string(parse_eastern_time(dtstr2)), I'm seeing that the return type of parse_eastern_time must be the same (or implicitly convertible to) the argument for to_eastern_string. Since parse_eastern_time returns timepoint_t, I believe to_eastern_string should have as its argument type time_point_t.

    std::string to_eastern_string(timepoint_t tp)
    {
        std::ostringstream oss;
        auto tz = std::chrono::locate_zone("America/New_York");
        oss << std::format("{:%F %T}", tz->to_local(tp));
        return oss.str();
    }
    

    Above I use the time_zone "America/New_York" to translate tp to a local_time. The format string {:%F %T} will output the full precision of tp. %F is simply a short cut for %Y-%m-%d.

    timepoint_t parse_eastern_time(std::string dtstr)
    {
        std::istringstream iss{std::move(dtstr)};
        std::chrono::local_time<duration_t> tp;
        iss >> std::chrono::parse("%F %T",tp);
        return std::chrono::locate_zone("America/New_York")->to_sys(tp);
    }
    

    Above I parse into a local_time and then associate that local_time with "America/New_York" by using that time_zone to translate the local_time to sys_time. This all using the desired precision duration_t.

    Now main:

    int main() {
    
            using namespace timestuff;
    
            timepoint_t utc_time = floor<duration_t>(clock_type::now());
            std::string now_string = to_eastern_string(utc_time);
            std::cout << now_string <<"\n";
    
            // yay
            std::string dtstr = "2024-11-19 09:30:00.037000";
            std::cout << to_eastern_string(parse_eastern_time(dtstr)) << "\n";
    
            std::string dtstr2 = "2024-11-21 15:59:59.823";
            std::cout << to_eastern_string(parse_eastern_time(dtstr2)) << "\n";
    
    
        return 0;
    }
    

    As soon as system_clock::now() is called, that is floored into the desired precision. After this one step, the entire program traffics in microseconds precision.

    Example output:

    2024-11-24 20:36:50.071210
    2024-11-19 09:30:00.037000
    2024-11-21 15:59:59.823000
    

    Demo.