From 92c083490a8bb94378391846143350829efb65ec Mon Sep 17 00:00:00 2001
From: Bruce Toll <4109762+tollb@users.noreply.github.com>
Date: Thu, 30 Dec 2021 15:44:06 -0500
Subject: [PATCH 2/3] Improve floating point round-trip for Postgres

Improve the round-trip precision of floating point values with the
Postgres backend for Wt Dbo.

Restore the use of stod/stof, with boost::spirit as a fallback for
subnormals. This addresses a potential loss of precision when using
the boost::spirit based convert<float> and convert<double> for
normal values.

When the C++ implementation has __cpp_lib_to_chars support, we can do
better by using the to_chars and from_chars functions that are
intended to provide round-tripping (within an implementation). However,
in some implementations, these functions do not handle subnormals,
so fallback to boost::spirit on std::out_of_range values.
---
 src/Wt/Dbo/backend/Postgres.C | 83 ++++++++++++++++++++++++++++++++++-
 1 file changed, 81 insertions(+), 2 deletions(-)

diff --git a/src/Wt/Dbo/backend/Postgres.C b/src/Wt/Dbo/backend/Postgres.C
index 24ede512..f813629a 100644
--- a/src/Wt/Dbo/backend/Postgres.C
+++ b/src/Wt/Dbo/backend/Postgres.C
@@ -29,6 +29,21 @@
 #include <cstring>
 #include <ctime>
 
+/*
+ * include charconv (s.b. in c++ 17, but sometimes available with c++ 14, as an extension)
+ * NOTE: Some implementations of charconv are incomplete, e.g. lack floating point.
+ *       Therefore, a separate feature test, e.g. #ifdef __cpp_lib_to_chars, is still required
+ */
+
+#if defined __has_include
+#  if __has_include (<charconv>)
+#    include <charconv>
+#    ifdef __cpp_lib_to_chars
+#      define WT_CPP_LIB_TO_CHARS
+#    endif
+#  endif
+#endif
+
 #include <boost/config/warning_disable.hpp>
 #include <boost/spirit/include/karma.hpp>
 #include <boost/spirit/include/qi_parse.hpp>
@@ -106,6 +121,16 @@ namespace {
 
   static inline std::string double_to_s(const double d)
   {
+#ifdef WT_CPP_LIB_TO_CHARS
+    std::array<char, 30> buf;
+
+    auto rv = std::to_chars(buf.data(), buf.data() + buf.size(), d);
+    if (rv.ec != std::errc())
+      throw std::invalid_argument(
+          "double_to_s: to_chars failed for double (as to_string() '" +
+          std::to_string(d)  + "'), err: " + std::error_condition(rv.ec).message());
+    return std::string(buf.data(), rv.ptr - buf.data());
+#else
     char buf[30];
     char *p = buf;
     if (d != 0) {
@@ -115,10 +140,21 @@ namespace {
     }
     *p = '\0';
     return std::string(buf, p);
+#endif
   }
 
   static inline std::string float_to_s(const float f)
   {
+#ifdef WT_CPP_LIB_TO_CHARS
+    std::array<char, 30> buf;
+
+    auto rv = std::to_chars(buf.data(), buf.data() + buf.size(), f);
+    if (rv.ec != std::errc())
+      throw std::invalid_argument(
+          "float_to_s: to_chars failed for float (as to_string() '" +
+          std::to_string(f) + "'), err: " + std::error_condition(rv.ec).message());
+    return std::string(buf.data(), rv.ptr - buf.data());
+#else
     char buf[30];
     char *p = buf;
     if (f != 0) {
@@ -128,6 +164,7 @@ namespace {
     }
     *p = '\0';
     return std::string(buf, p);
+#endif
   }
 }
 
@@ -553,7 +590,28 @@ public:
     if (PQgetisnull(result_, row_, column))
       return false;
 
-    *value = convert<float>("stof", boost::spirit::float_, PQgetvalue(result_, row_, column));
+#ifdef WT_CPP_LIB_TO_CHARS
+    std::string result_s = PQgetvalue(result_, row_, column);
+
+    // try to convert with from_chars which has good round-trip properties
+    auto rv = std::from_chars(result_s.data(), result_s.data() + result_s.size(), *value);
+
+    // fall-back to boost::spirit for "out of range", e.g. subnormals in some implementations
+    if (rv.ec == std::errc::result_out_of_range) {
+      *value = convert<float>("stof", boost::spirit::float_, PQgetvalue(result_, row_, column));
+    } else if (rv.ec != std::errc()) {
+      throw PostgresException(std::string("getResult: from_chars (float) of '") +
+          result_s + "' failed, err: " + std::error_condition(rv.ec).message());
+    }
+#else
+    try {
+      // try to convert with std::stof for backwards-compatibility and better round-trip/precision
+      *value = std::stof(PQgetvalue(result_, row_, column));
+    } catch (std::out_of_range) {
+      // fall-back to boost::spirit for "out of range", e.g. subnormals in some implementations
+      *value = convert<float>("stof", boost::spirit::float_, PQgetvalue(result_, row_, column));
+    }
+#endif
 
     LOG_DEBUG(this << " result float " << column << " " << *value);
 
@@ -565,7 +623,28 @@ public:
     if (PQgetisnull(result_, row_, column))
       return false;
 
-    *value = convert<double>("stod", boost::spirit::double_, PQgetvalue(result_, row_, column));
+#ifdef WT_CPP_LIB_TO_CHARS
+    std::string result_s = PQgetvalue(result_, row_, column);
+
+    // try to convert with from_chars which has good round-trip properties
+    auto rv = std::from_chars(result_s.data(), result_s.data() + result_s.size(), *value);
+
+    // fall-back on boost::spirit for "out of range", e.g. subnormals in some implementations
+    if (rv.ec == std::errc::result_out_of_range) {
+      *value = convert<double>("stod", boost::spirit::double_, PQgetvalue(result_, row_, column));
+    } else if (rv.ec != std::errc()) {
+      throw PostgresException(std::string("getResult: from_chars (double) of '") +
+          result_s + "' failed, err: " + std::error_condition(rv.ec).message());
+    }
+#else
+    try {
+      // try to convert with std::stod for backwards-compatibility and better round-trip/precision
+      *value = std::stod(PQgetvalue(result_, row_, column));
+    } catch (std::out_of_range) {
+      // fall-back to boost::spirit for "out of range", e.g. subnormals in some implementations
+      *value = convert<double>("stod", boost::spirit::double_, PQgetvalue(result_, row_, column));
+    }
+#endif
 
     LOG_DEBUG(this << " result double " << column << " " << *value);
 
-- 
2.33.1

