3
\$\begingroup\$

Problem

I need to convert a double to a std::string which contains no scientific notation and no unnecessary fillers.

Using std::fixed with iostream yielded ugly results - extra fillers (0's). The closest working code I could get was to using sprintf("%g",...) and then dealing with exponentials, should they appear (beyond 6 places).

I am looking for improvements in robustness or clarity/simplicity in the implementation.

Details/Elaboration (addition)

I realize that the conversion between double and std::string and back can be expected to be lossy. So I am not looking for an implementation that works for all cases. For example, I don't expect to be able to convert "0.2000" to double and then get the same back (getting "0.2" back would be fine). Similarly with "+0.2000": getting "0.2" back is ok.

I have attempted solutions involving sprintf("%g",...), but the result is automatic conversion to scientific notation when we go beyond six significant digits. There appears to be no way to configure that setting. Alternatively, sprintf("%f",...) requires that we manually set or calculate the exact precision that we want - which I don't know how to do if that information is lost inside a double. Note that what I have found here seems to equally apply to most simple solutions involving std::iostream and std::fixed.

#include <iostream>
#include <cstring>
#include <string>
#include <cassert>

static std::string
to_string( double d )
{
  char s[64];
  sprintf( s, "%g", d );

  char* e = std::strchr( s, 'e' );
  if ( e==nullptr )
    return s;
  char* p = std::strchr( s, '.' );

  // eg 2.4e-07
  int exp   = atoi( e+1 );   // -7
  int extra = 0;

  // snip period eg 24
  if ( p!=nullptr ) {
    extra = e-p-1;
    memmove( p, p + 1, strlen(s) - (p-s));
  }

  int coeff = atoi( s );
  // std::cerr << "DEBUG  = " << s << " coeff = " << coeff << " exp = " << exp << "\n";
  if ( exp<0 ) {
    sprintf( s, "0.%0*d", -exp+extra, coeff);
    return s;
  }

  // eg 7.3e+09
  sprintf( s, "%d%0*d", coeff, exp-extra, 0 );
  return s;
}

static void
test_string_conversion( std::string const& s )
{
    assert( to_string( atof( s.c_str() ))==s );
}

int main()
{
   test_string_conversion( "1.5203" );
   test_string_conversion( "-1.025" );
   test_string_conversion( "3.1415" );
   test_string_conversion( "-38271" );
   test_string_conversion( "0.8382" );
   test_string_conversion( "-0.8382" );
   test_string_conversion( "7" );
   test_string_conversion( "-3" );
   test_string_conversion( "0.0000024" );
   test_string_conversion( "7300000000" );
   return 0;
}
\$\endgroup\$
  • \$\begingroup\$ improvements in robustness or clarity/simplicity: Yes all this can be done by simply using the standard library. The problem I have is that I don't understand your requirements. In most you are only printing to 4 decimal places 1.5203 while others you are printing to seven 0.0000024 which is it that you want? \$\endgroup\$ – Martin York May 13 '15 at 6:06
  • \$\begingroup\$ Your problem comes down to representation. If you have a fractional number where the last digit is not 5 you will not be able to represent it exactly in a double and thus you can print out many many decimal places. \$\endgroup\$ – Martin York May 13 '15 at 6:07
  • \$\begingroup\$ @LokiAstari From a non-technical person's perspective, as much as possible, I want an inverse function to atof(). I realize that there are many hairy details in precision, binary representation, lossy conversions, etc... ...and yet, sprintf( "%g" ) seems to be get it mostly right most of the time (aside from extraneous zeros which I try to deal with). \$\endgroup\$ – kfmfe04 May 13 '15 at 6:19
  • \$\begingroup\$ Also note: operator<< standard behavior when passed a double is to use the equivelant of %g formatting. See n3797 Section: 22.4.2.2.2 [facet.num.put.virtuals] \$\endgroup\$ – Martin York May 13 '15 at 6:19
  • \$\begingroup\$ There is no inverse to atof() as you point out the conversion is not exact. What you are asking for is std::cout << aDoubleValue. Unless you want to specify what you want more exactly the rest is meaningless. \$\endgroup\$ – Martin York May 13 '15 at 6:21
4
\$\begingroup\$

As mentioned in the comments of the other answer, your decimal floating point numbers are stored as binary and rarely have finite representations.

If you simply want code that passes your test I would mention that it is much easier to remove padding from std::fixed than it is to expand the scientific notation.

#include <iostream>
#include <string>
#include <sstream>
#include <limits>
#include <iomanip>
#include <cstdlib>
#include <cassert>
#include <cstddef>

static std::string to_string(double d)
{
    std::ostringstream oss;
    oss.precision(std::numeric_limits<double>::digits10);
    oss << std::fixed << d;
    std::string str = oss.str();

    // Remove padding
    // This must be done in two steps because of numbers like 700.00
    std::size_t pos1 = str.find_last_not_of("0");
    if(pos1 != std::string::npos)
        str.erase(pos1+1);

    std::size_t pos2 = str.find_last_not_of(".");
    if(pos2 != std::string::npos)
        str.erase(pos2+1);

    return str;
}


static void test_string_conversion(const std::string& str)
{
    assert( to_string( atof(str.c_str()) ) == str );
}


int main(void)
{
    test_string_conversion( "1.5203" );
    test_string_conversion( "-1.025" );
    test_string_conversion( "3.1415" );
    test_string_conversion( "-38271" );
    test_string_conversion( "0.8382" );
    test_string_conversion( "-0.8382" );
    test_string_conversion( "7" );
    test_string_conversion( "-3" );
    test_string_conversion( "0.0000024" );
    test_string_conversion( "7300000000" );
}
\$\endgroup\$
  • \$\begingroup\$ nice approach - I will throw some more reasonable tests at your implementation later and comment/accept after confirmation... \$\endgroup\$ – kfmfe04 May 12 '15 at 21:43
  • \$\begingroup\$ Good idea. The "remove padding" code should probably be in its own function or method. \$\endgroup\$ – Snowbody May 13 '15 at 14:38
0
\$\begingroup\$

std::sprintf() isn't a bad choice (it's amazingly fast), but if you don't want exponentials, use std::sprintf("%f",...) instead.

Other choices include:

  • Boost::format
  • std::ostringstream or one of the other streams. You said you knew about std::fixed but there are other format modifiers such as setprecision and setf that may be necessary to get the format right.
\$\endgroup\$
  • 1
    \$\begingroup\$ Part of the headache for the %f route is having to deal with setprecision and setf - without setting them explicitly in %f or in std::ostringstream, I will not get the original string back (with the test cases I have in the code). Perhaps I can improve the clarity of the code by calculating the correct '%x.yf' string somehow from double and just sprintf'ing it? \$\endgroup\$ – kfmfe04 May 12 '15 at 19:57
  • \$\begingroup\$ Unfortunately, there's not an easy way to calculate the "right" number of decimal places to use. The standard for floating-point numbers stores them as binary, and very few numbers have an exact binary representation. In most cases you can't round-trip. So the last significant digit is almost certainly going to be off. \$\endgroup\$ – Snowbody May 12 '15 at 20:55
  • \$\begingroup\$ Excellent point. I agree that there will be edge cases, due to floating point precision, which are bound to fail (the conversions are lossy). However, it would be nice to get correct answers to cases one step from the edge. In fact, in my true use case, I am dealing with std::decimal::decimal64 to avoid exactly those floating point issues, but even in that code, I still need to do this same conversion because there is no function to get the coefficient and exponent back from std::decimal::decimal64! \$\endgroup\$ – kfmfe04 May 12 '15 at 21:32
0
\$\begingroup\$

What you are looking for is std::fixed

#include <iostream>
#include <iomanip>

int main()
{
   std::cout << std::fixed << 1.5203 << "\n";
   std::cout << std::fixed << -1.025 << "\n";
   std::cout << std::fixed << 3.1415 << "\n";
   std::cout << std::fixed << -38271 << "\n";
   std::cout << std::fixed << 0.8382 << "\n";
   std::cout << std::fixed << -0.8382 << "\n";
   std::cout << std::fixed << 7 << "\n";
   std::cout << std::fixed << -3 << "\n";
   std::cout << std::fixed << 0.0000024 << "\n";
   std::cout << std::fixed << 7300000000 << "\n";
}

1.520300
-1.025000
3.141500
-38271
0.838200
-0.838200
7
-3
0.000002
7300000000
\$\endgroup\$
  • \$\begingroup\$ Thanks for the attempt - I have added more details to the OP. Also, look at the outputs from this solution and compare them with the original string versions. They differ (more so) than the implementation I have in the OP. \$\endgroup\$ – kfmfe04 May 13 '15 at 6:38
  • \$\begingroup\$ The trouble is your tests are tailored. What you are trying to do is pointless. You are saying I have this arbitrarily defined thing that I want that I can't actually specify exactly but I want to write code for it. This code sort of does what I want most of the time. The standard techniques have their flaws but they have an exact well defined behavior. If you really want to print a double out without loss of precision you should be printing the hex version (its loss-less for the internal representation to and from string). The decimal version is really exact (unless it ends in a 5). \$\endgroup\$ – Martin York May 13 '15 at 17:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.