c++datetime-formatfstreamc++-chronoc++23

How to write/read std::chrono::zoned_seconds to/from a stream using chrono::parse?


I am trying to write a chrono::zoned_seconds object as text to a file and then retrieve it and construct another chrono::zoned_seconds object later. How can this be done in a fairly efficient way?

I don't think the code snippet below shows the correct result:

#include <fstream>
#include <print>
#include <chrono>
#include <format>
#include <string>
#include <sstream>


int main()
{
    {
        std::ofstream file("data.txt");
        if (!file) {
            std::println( "Unable to open file.\n" );
            return 1;
        }

        auto now = std::chrono::system_clock::now();
        const std::chrono::zoned_seconds zs { "America/New_York", std::chrono::time_point_cast<std::chrono::seconds>( now ) };

        std::format_to(std::ostreambuf_iterator<char>(file), "{:%F %T %Z}", zs);
        std::println( "{:%F %T %Z}", zs ); // correct time
    }

    {
        std::ifstream file("data.txt");
        if (!file) {
            std::println( "Unable to open file.\n" );
            return 1;
        }

        std::string str;
        std::getline(file, str);
        std::istringstream iss { str };

        std::chrono::sys_seconds tp;
        std::string zone_name;
        iss >> std::chrono::parse("%F %T %Z", tp, zone_name);
        std::chrono::zoned_seconds zs { zone_name, tp };
        std::println( "{:%F %T %Z}", zs );  // incorrect time!!
    }
}

As can be seen, I used std::chrono::parse but the outputs don't match:

2024-03-01 13:35:20 EST
2024-03-01 08:35:20 EST

Solution

  • There are two bugs in your code. To show them I'm going to read and write from a stringstream to enable demos at https://wandbox.org (which does not allow file creation). However parsing and streaming to stringstream is identical to parsing and streaming to fstream except for construction/opening of the stream.

    Here is your code virtually unchanged, except using stringstream:

    #include <sstream>
    #include <print>
    #include <chrono>
    #include <format>
    #include <string>
    #include <sstream>
    
    
    int main()
    {
        std::stringstream file;
        auto now = std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now());
        const std::chrono::zoned_seconds zs { "America/New_York", now };
    
        file << std::format( "{:%F %T %Z}", zs);
        std::println( "{:%F %T %Z}", zs ); // correct time
    
        std::chrono::sys_seconds tp;
        std::string zone_name;
        file >> std::chrono::parse("%F %T %Z", tp, zone_name);
        std::chrono::zoned_seconds zs2 { zone_name, tp };
        std::println( "{:%F %T %Z}", zs2 );
    }
    

    And the output currently looks like:

    2024-03-01 14:04:28 EST
    2024-03-01 09:04:28 EST
    

    The reason for the large discrepancy in time is that when your format a zoned_time it displays the local time, not UTC. But when you read it in above, you are parsing as if the time parsed is UTC, not local:

        std::chrono::sys_seconds tp;
    

    By changing the above line to:

        std::chrono::local_seconds tp;
    

    you change the semantics of 2024-03-01 14:04:28 from UTC to local time.

    Output:

    2024-03-01 14:08:06 EST
    2024-03-01 14:08:06 EST
    

    Although this looks right, it is still subtly wrong.

    EST is a IANA time zone with no DST rules. It has a fixed UTC offset of -5h. America/New_York is the same as EST between sys_days{Sunday[1]/November/y} + 6h and sys_days{Sunday[2]/March/(y+1)} + 7h, and otherwise is on daylight saving with a UTC offset of -4h. So the program above will only be correct when daylight saving is not in effect for America/New_York.

    To see this more clearly, replace:

    auto now = std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now());
    

    with:

    auto now = sys_days{July/4/2024} + 12h + 0s;
    

    I.e. Let's see how this program behaves in the Summer time.

    This line:

    std::chrono::zoned_seconds zs2 { zone_name, tp };
    

    throws a std::runtime_error exception because it is trying to locate a time zone named "EDT" and is failing to find it. IANA has lots of time zones with an abbreviation of "EDT", but none that have that name.

    gcc provides a helpful message in the runtime_error::what() message:

    what(): tzdb: cannot locate zone: EDT


    Aside: See this article which addresses the difficulties associated with mapping a time zone abbreviation to a time zone name, and techniques for doing as much as possible.


    To correct this bug, "America/New_York" must be streamed to file instead of "EST" or "EDT". This can be fixed by changing one line:

    file << std::format( "{:%F %T %Z}", zs);
    

    to:

    file << std::format( "{:%F %T }", zs) << zs.get_time_zone()->name();
    

    I.e. this outputs the actual time zone name as opposed to the time zone abbreviation.

    Output:

    2024-03-01 14:13:30 EST
    2024-03-01 14:13:30 EST
    

    So now this line:

    std::chrono::zoned_seconds zs2 { zone_name, tp };
    

    is guaranteed to use the same time zone name as was used in this line:

    const std::chrono::zoned_seconds zs { "America/New_York", now };
    

    Here is the complete fixed example for reference:

    #include <sstream>
    #include <print>
    #include <chrono>
    #include <format>
    #include <string>
    #include <sstream>
    
    
    int main()
    {
        std::stringstream file;
        auto now = std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now());
        const std::chrono::zoned_seconds zs { "America/New_York", now };
    
        file << std::format( "{:%F %T }", zs) << zs.get_time_zone()->name();
        std::println( "{:%F %T %Z}", zs ); // correct time
    
        std::chrono::local_seconds tp;
        std::string zone_name;
        file >> std::chrono::parse("%F %T %Z", tp, zone_name);
        std::chrono::zoned_seconds zs2 { zone_name, tp };
        std::println( "{:%F %T %Z}", zs2 );
    }