patterncppMinor
Converting std::chrono::time_point to/from std::string
Viewed 0 times
stdtime_pointconvertingfromchronostring
Problem
Consider these functions that allow to convert
with usage:
```
std::string str;
auto now = std::chrono::system_clock::now();
toString(now,str); std::cerr >;
using TP = std::chrono::time_point;
toString(TP(DD(0)),str); std::cout
std::chrono::time_point to/from std::string with a predefined date-time format.constexpr size_t log10(size_t xx) { return xx == 1 ? 0 : 1 + log10(xx/10); }
template ::digits10, typename TimePoint >
requires (TimePoint::period::den % 10 != 0) && std::is_floating_point_v && Precision ::digits10
inline bool toString(const TimePoint& timePoint, std::string& str)
{
Double seconds = timePoint.time_since_epoch().count();
(seconds *= TimePoint::period::num) /= TimePoint::period::den;
auto zeconds = std::modf(seconds,&seconds);
time_t tt = seconds;
std::ostringstream oss;
oss requires (TimePoint::period::den % 10 == 0)
inline bool toString(const TimePoint& timePoint, std::string& str)
{
uint64_t feconds = timePoint.time_since_epoch().count() * TimePoint::period::num;
time_t tt = feconds / TimePoint::period::den;
std::ostringstream oss;
oss
bool fromString(TimePoint& timePoint, const std::string& str)
{
std::istringstream iss(str);
std::tm tm{};
if (!(iss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S")))
return false;
timePoint = {};
timePoint += std::chrono::seconds(std::mktime(&tm));
if (iss.eof())
return true;
if (iss.get() != '.')
return false;
std::string zz;
if (!(iss >> zz))
return false;
static_assert(std::chrono::high_resolution_clock::period::num == 1 && std::chrono::high_resolution_clock::period::den % 10 == 0);
zz.resize(log10(std::chrono::high_resolution_clock::period::den),'0');
size_t zeconds = 0;
try { zeconds = std::stoul(zz); } catch (const std::exception&) { return false; }
timePoint += std::chrono::high_resolution_clock::duration(zeconds);
return true;
}with usage:
```
std::string str;
auto now = std::chrono::system_clock::now();
toString(now,str); std::cerr >;
using TP = std::chrono::time_point;
toString(TP(DD(0)),str); std::cout
Solution
Headers and typenames
We're missing
And we consistently misspell the types
A name collision
On systems that define a global
Interface consistency
The two alternative forms for
Incidentally, did you measure a performance difference between the two implementations that proved a benefit? If so, it's worth quantifying that in a comment so that future readers understand that it's worthwhile having both versions.
Document the assumption
This code only works when converting to or from
Prefer to return values over status codes
Rather than returning a status code, I'd prefer to throw an exception when conversion fails; then the return value can be used directly. Failure in
If you really dislike exceptions, it's still easier on the caller to return a
Be careful with negative times
If
Peek the '.' instead of reading it
If we leave '.' in the input stream, we can read that as part of a floating-point value:
Test cases seem inconsistent with the code
The test cases use month names, which require a
We could do with a few more test cases.
Consider a streamable wrapper
If your main use-case is to stream dates in and out, it may be better to bypass returning a string, and instead create an object that knows how to stream directly to a
Simplified code
We're missing
#include
#include
#include
#include
#include
#include
#include
#include And we consistently misspell the types
std::size_t, std::time_t, std::uint64_t.A name collision
On systems that define a global
log10 as well as std::log10, there's ambiguity on the calls to our log10. I suggest using a name that conveys intent, such as digits() (even that's a bit risky - it would be better in a private namespace). It costs nothing to make it terminate if passed 0 as argument, too:static constexpr std::size_t feconds_width(std::size_t x)
{
return x <= 1 ? 0 : 1 + feconds_width(x/10);
}Interface consistency
The two alternative forms for
toString() need to be instantiated differently, depending on the timepoint's denominator value. If we make Double default to double, then both can be called without explicit template arguments.Incidentally, did you measure a performance difference between the two implementations that proved a benefit? If so, it's worth quantifying that in a comment so that future readers understand that it's worthwhile having both versions.
Document the assumption
This code only works when converting to or from
TimePoint classes which have the same epoch as std::time_t.Prefer to return values over status codes
Rather than returning a status code, I'd prefer to throw an exception when conversion fails; then the return value can be used directly. Failure in
toString() seems especially unlikely - I think the only possible cause is running out of room in the output string and being unable to allocate more.if (!oss) throw std::runtime_error("timepoint-to-string");
return oss.str();If you really dislike exceptions, it's still easier on the caller to return a
std::optional rather than return a boolean and modify a reference argument.Be careful with negative times
If
tt<0, then tt%60 can be negative. That's not what we want. However, since we create a std::tm, we can read seconds from that:auto tm = std::localtime(&tt);
if (!tm) throw std::runtime_error(std::strerror(errno));
oss tm_sec+zeconds;Peek the '.' instead of reading it
If we leave '.' in the input stream, we can read that as part of a floating-point value:
double zz;
if (iss.peek() != '.' || !(iss >> zz))
throw std::invalid_argument("decimal");
using hr_clock = std::chrono::high_resolution_clock;
std::size_t zeconds = zz * hr_clock::period::den / hr_clock::period::num;
timePoint += hr_clock::duration(zeconds);Test cases seem inconsistent with the code
The test cases use month names, which require a
%b conversion, not %m.We could do with a few more test cases.
Consider a streamable wrapper
If your main use-case is to stream dates in and out, it may be better to bypass returning a string, and instead create an object that knows how to stream directly to a
std::ostream. As I don't know if that's your intended use, I won't demonstrate that.Simplified code
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
template::digits10,
typename TimePoint>
requires std::is_floating_point_v
&& Precision ::digits10
inline std::string toString(const TimePoint& timePoint)
{
auto seconds = Double(timePoint.time_since_epoch().count())
* TimePoint::period::num / TimePoint::period::den;
auto const zeconds = std::modf(seconds,&seconds);
std::time_t tt(seconds);
std::ostringstream oss;
auto const tm = std::localtime(&tt);
if (!tm) throw std::runtime_error(std::strerror(errno));
oss tm_sec+zeconds;
if (!oss) throw std::runtime_error("timepoint-to-string");
return oss.str();
}
template
TimePoint fromString(const std::string& str)
{
std::istringstream iss{str};
std::tm tm{};
if (!(iss >> std::get_time(&tm, "%Y-%b-%d %H:%M:%S")))
throw std::invalid_argument("get_time");
TimePoint timePoint{std::chrono::seconds(std::mktime(&tm))};
if (iss.eof())
return timePoint;
double zz;
if (iss.peek() != '.' || !(iss >> zz))
throw std::invalid_argument("decimal");
using hr_clock = std::chrono::high_resolution_clock;
std::size_t zeconds = zz * hr_clock::period::den / hr_clock::period::num;
return timePoint += hr_clock::duration(zeconds);
}
int main()
{
using std::chrono::system_clock;
auto now = system_clock::now();
std::clog >;
using TP = std::chrono::time_point;
for (int i = 0; i (s))
<< std::endl;
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
}
}Code Snippets
#include <chrono>
#include <cmath>
#include <cstdint>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>static constexpr std::size_t feconds_width(std::size_t x)
{
return x <= 1 ? 0 : 1 + feconds_width(x/10);
}if (!oss) throw std::runtime_error("timepoint-to-string");
return oss.str();auto tm = std::localtime(&tt);
if (!tm) throw std::runtime_error(std::strerror(errno));
oss << std::put_time(tm, "%Y-%b-%d %H:%M:")
<< std::setw(Precision+3) << std::setfill('0')
<< std::fixed << std::setprecision(Precision)
<< tm->tm_sec+zeconds;double zz;
if (iss.peek() != '.' || !(iss >> zz))
throw std::invalid_argument("decimal");
using hr_clock = std::chrono::high_resolution_clock;
std::size_t zeconds = zz * hr_clock::period::den / hr_clock::period::num;
timePoint += hr_clock::duration(zeconds);Context
StackExchange Code Review Q#156695, answer score: 8
Revisions (0)
No revisions yet.