4

I'm trying to format numbers to a specific number of significant digits using C/C++ and preferably STL. I've seen examples of doing this in Javascript (toPrecision()) and .Net, but I can't find anything on doing this in C/C++. I want to create a function something like this:

std::string toPrecision(double value, int significantDigits) {
    std::string formattedString;
    // magic happens here
    return formattedString;
}

So that it produces results like this:

toPrecision(123.4567, 2) --> "120"
toPrecision(123.4567, 4) --> "123.4"
toPrecision(123.4567, 5) --> "123.45"

Does anyone know a good way to do this? I'm considering dumping the whole number into a string and then just scanning through it to find the non-zero digits and count them off in some intelligent way, but that seems cumbersome.

I could also download the source code to one of the browsers and just see what their toPrecision function looks like, but I think it would take me all day to work through the unfamiliar code. Hope someone can help!

mu is too short
  • 426,620
  • 70
  • 833
  • 800
John Stephen
  • 7,625
  • 2
  • 31
  • 45
  • Your examples truncate instead of rounding. Just curious, is that what you want? – Darryl Sep 23 '11 at 22:07
  • I may actually prefer rounding. I was going to mention that in the question but I thought it might confuse the issue and removed that part. If it's rounding then the results for example 2 and 3 would be "123.5" and "123.46". – John Stephen Sep 23 '11 at 22:17

4 Answers4

4

Stolen from another question:

#include <string>
#include <sstream>
#include <cmath>
#include <iostream>

std::string toPrecision(double num, int n) {
    https://stackoverflow.com/questions/202302/rounding-to-an-arbitrary-number-of-significant-digits

    if(num == 0) {
      return "0";
    }

    double d = std::ceil(std::log10(num < 0 ? -num : num));
    int power = n - (int)d;
    double magnitude = std::pow(10., power);
    long shifted = ::round(num*magnitude);

    std::ostringstream oss;
    oss << shifted/magnitude;
    return oss.str();
}

int main() {
  std::cout << toPrecision(123.4567, 2) << "\n";
  std::cout << toPrecision(123.4567, 4) << "\n";
  std::cout << toPrecision(123.4567, 5) << "\n";
}
Community
  • 1
  • 1
Robᵩ
  • 163,533
  • 20
  • 239
  • 308
  • 1
    I forgot to mention that the std::pow, std::ceil and std::log10 should just be ::pow, ::ceil and ::log10 since they're not actually in the std namespace. Worked great though, thanks again! – John Stephen Sep 23 '11 at 22:41
  • 1
    According to C++03, §17.4.1.2, ¶4, everything from C90 (except macros) from `` are "within namespace scope of the namespace std." `::round` isn't in C90, so it isn't in `std::`, but `pow`, `log10` and `ceil` should all be present in `std::`. And, the above program compiles as-is with g++. And, you're welcome. – Robᵩ Sep 23 '11 at 23:03
  • I'm using instead of so that's probably why I had namespace problems. I'll switch to using cmath since that seems more appropriate. – John Stephen Sep 24 '11 at 20:35
1

Check out setprecision() in iomanip. That should do what you are looking for on the double, then just convert to string

boba-feh
  • 24
  • 2
  • That looks promising - I'll give it a try and let you know how it works out – John Stephen Sep 23 '11 at 22:23
  • 1
    setprecision() sort of works, but unfortunately it converts numbers to scientific notation if they're large. For example, 12345.6 converted to 3 digits of precision outputs as "123e+02". Thanks for the point to iomanip though - I've never used that collection before and there's some great stuff in there! – John Stephen Sep 23 '11 at 22:38
  • @JohnStephen also `<< std::fixed`? – Troyseph Mar 22 '21 at 21:54
  • 1
    @Troyseph << std::fixed doesn't do precision in the sense that I need -- I need significant digits, not just some number of digits after the decimal point. The accepted answer does exactly that. – John Stephen Mar 23 '21 at 22:03
0

The method above with a ceil(log10(x)) is perfectly legit to determine the number of digit of the integer part of a number. But I feel it's a bit heavy on CPU to call for maths functions just to set a number of digit.

Isn't that simpler to convert the floating value into a string with too many digits, then to work on the string itself?

Here is what I'd try (with Qt instead of the STL):

QString toPrecision(double num, int n) {
    QString baseString = QString::number(num, 'f', n);
    int pointPosition=baseString.indexOf(QStringLiteral("."));
    // If there is a decimal point that will appear in the final result
    if (pointPosition != -1 && pointPosition < n) 
        ++n ; // then the string ends up being n+1 in length                                    
    if (baseString.count() > n) {
        if (pointPosition < n) {
            baseString.truncate(n);
        } else {
            baseString.truncate(pointPosition);
            for (int i = n ; i < baseString.count() ; ++i)
                baseString[i]='0';
        }
    } else if (baseString.count() < n) {
        if (pointPosition != -1) {
            for (int i = n ; i < baseString.count() ; ++i)
                baseString.append('0');
        } else {
            baseString.append(' ');
        }
    }
    return baseString ;
}

But the question was about the STL.. So, let's rewrite it that way:

std::string toPrecision(double num, size_t n) {
    std::ostringstream ss;
    ss << num ;
    std::string baseString(ss.str());
    size_t pointPosition=baseString.find('.');
    if (pointPosition != std::string::npos && pointPosition < n)
        ++n ;
    if (baseString.length() > n) {
        if (pointPosition < n) {
            baseString.resize(n);
        } else {
            baseString.resize(pointPosition);
            for (size_t i = n ; i < baseString.length() ; ++i)
                baseString[i]='0';
        }
    } else if (baseString.length() < n) {
        if (pointPosition != std::string::npos) {
                baseString.append(n-baseString.length(),'0');
        } else {
            baseString.append(n-baseString.length(),' ');
        }
    }
    return baseString ;
}
  • The FPU can handle the log10() call and other math functions in a single instruction, whereas this solution requires a lot of processing on the string instead. This solution also needs additional code to handle locales where the decimal separator isn't a period. – John Stephen Jul 19 '21 at 01:34
0

Print it to an ostringstream, setting the floating-point formatting parameters as appropriate.

  • 1
    Those don't produce the results I'm looking for. They're pretty much the same as using printf's %f options in that they do n digits before the decimal and m digits after. Not significant digits. – John Stephen Sep 23 '11 at 22:16