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
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.
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.
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 );
}