4

I'm using cpp_dec_float for arbitrary precision, and it's great, but I'm having trouble figuring out how to print all significant digits.

For example, with this code to setup

using boost::multiprecision::cpp_dec_float;
typedef boost::multiprecision::number<cpp_dec_float<100>> mp_type;

mp_type test_num("7.0710678118654752440084436210484903928483593768847403658833986900e-01");

and if I simply print with

std::cout << std::scientific << test_num << std::endl;

the result is 7.071068e-01, so that's out.

If I go for broke with

std::cout << std::setprecision(std::numeric_limits<mp_type>::digits) << std::scientific << test_num << std::endl;

I get 7.0710678118654752440084436210484903928483593768847403658833986900000000000000000000000000000000000000e-01. I'm happy not to lose the precision, but it's not very space conservative.

Is there a way to remove the trailing zeros without losing any precision with existing tools? If not, how can the trailing zeros be removed from the resultant string?

If existing tools can be used to satisfy my intent, how can cpp_dec_float be output in scientific notation without lost precision and trailing zeros removed to a string? I can only find the stream examples.

Closer

Thanks to mockinterface, I'm much closer.

I've changed the code to this:

using boost::multiprecision::cpp_dec_float;
typedef boost::multiprecision::number<cpp_dec_float<0>> mp_type;
mp_type test_num("7.0710678118654752440084436210484903928483593768847403658833986900e-01");
std::cout << test_num.str(0, std::ios_base::scientific) << std::endl;

To have potentially unlimited length; however, this is printed:

7.0710678118654752440084436210484903928480e-01

Which is close but seems strange. In the source mockinterface so graciously pointed out to me, I found these lines

if(number_of_digits == 0)
    number_of_digits = cpp_dec_float_total_digits10;

which suggests to me that it should take into account all significant digits, basically outputting what was input because of the unlimited length.

I checked the source for cpp_dec_float_total_digits10, and I am unable to determine exactly what it is; although, I did find this code section that seems to define it.

private:
   static const boost::int32_t cpp_dec_float_elem_digits10 = 8L;
   static const boost::int32_t cpp_dec_float_elem_mask     = 100000000L;

   BOOST_STATIC_ASSERT(0 == cpp_dec_float_max_exp10 % cpp_dec_float_elem_digits10);

   // There are three guard limbs.
   // 1) The first limb has 'play' from 1...8 decimal digits.
   // 2) The last limb also has 'play' from 1...8 decimal digits.
   // 3) One limb can get lost when justifying after multiply,
   //    as only half of the triangle is multiplied and a carry
   //    from below is missing.
   static const boost::int32_t cpp_dec_float_elem_number_request = static_cast<boost::int32_t>((cpp_dec_float_digits10 / cpp_dec_float_elem_digits10) + (((cpp_dec_float_digits10 % cpp_dec_float_elem_digits10) != 0) ? 1 : 0));

   // The number of elements needed (with a minimum of two) plus three added guard limbs.
   static const boost::int32_t cpp_dec_float_elem_number = static_cast<boost::int32_t>(((cpp_dec_float_elem_number_request < 2L) ? 2L : cpp_dec_float_elem_number_request) + 3L);

public:
   static const boost::int32_t cpp_dec_float_total_digits10 = static_cast<boost::int32_t>(cpp_dec_float_elem_number * cpp_dec_float_elem_digits10);

Can the number of significant digits be determined and used as the first argument for boost::multiprecision::cpp_dec_float::str()?

2 Answers2

2

This turned out to be a tough one.

The short story is: there is no such functionality in cpp_dec_float. What's worse, cpp_dec_float doesn't track the number of significant digits that have been set, so there's no "cheap" way to find the length needed to print the fraction.

Ideas:

  • For some border cases (e.g. 123.000000000000001) one could take the log10 of the reciprocal of the the fractional part + log10 of the integer part. This completely fails to be generically applicable.

  • If you want to use implementation details you might find the 'last inhabited' element in the backend array, and do the maths. However, this is pretty involved (requires modifying cpp_dec_float.hpp and a lot of testing).

  • Finally, I observed that the current implementation for .str() clearly makes zero effort to be efficient. At all.

So all in all I have the following suggestions. Either

  1. switch to the gmp backend (if you can afford it). Note

    • this is not a decimal float representation AFAICT
    • this requires an additional library (libgmp) to be linked
    • gmp_float does have arbitrary precision though, and
    • it's str() implementation does take into account the significance of zeroes in the mantissa

    See it Live On Coliru

    #include <boost/multiprecision/number.hpp>
    #include <boost/multiprecision/gmp.hpp>
    #include <iostream>
    
    namespace mp = boost::multiprecision;
    
    int main()
    {
        typedef mp::number<mp::gmp_float<100>> mp_type;
        mp_type test_num("7.071067811865475244008443621048490392848359376884740365883398690000000000000000000e-01");
    
        std::cout << test_num.str(0, std::ios_base::scientific) << '\n';
    }
    

    Prints 7.071067811865475244008443621048490392848359376884740365883398690e-01 without further actions required.

  2. If that's not an option, I'd just post-process the output, removing the trailing zeroes:

    template <typename T>
    std::string to_pretty_string(T const& v)
    {
        std::string s = v.str(0, std::ios_base::scientific);
        assert(s.length()>3); // min: 0.e
        switch (s[0])
        { // normalized scientific always has #.####### form of mantissa
            case '-':
            case '+': assert(s[2] == '.'); break;
            default:  assert(s[1] == '.'); break;
        }
    
        auto exp = s.find('e');
        if (std::string::npos != exp && exp > 0)
        {
            for(size_t pos = exp-1; pos; --pos)
            {
                if (s[pos] != '0')
                {
                    // remove run of 0s if applicable
                    s.erase(pos+1, exp-pos-1); 
                    break;
                }
            }
        }
        return std::move(s);
    }
    

See it Live On Coliru again

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Perfect, thank you so much sehe! I will use both to take off that last `0`. ;)) I didn't know the string constructor works with `gmp`, so again, thank you so much! I'm going to see if it works with MPFR since it has "correct rounding". http://www.mpfr.org/ I'm so happy I get to use the faster libraries! I wish I could upvote you more! Thank you very much! –  Apr 13 '14 at 00:03
  • @sehe is `std::move` on RVO significant? – mockinterface Apr 13 '14 at 01:49
  • Sorry to come back on this, but in the random chance that only `0`s follow the decimal, how can the decimal also be removed? http://coliru.stacked-crooked.com/a/c84d868316b6ae8d Again, can't thank you enough for this great code! I'm so new to c++, I can't even be sure what you've done to remove the `0`s, lol. –  Apr 14 '14 at 22:02
  • 1
    @Gracchus do you mean [this](http://coliru.stacked-crooked.com/a/3f8eed55ff4c902e)? Warning though: you'll have to watch out for locale-dependent decimal separators (it might be `,` or something else entirely) – sehe Apr 14 '14 at 22:40
  • @mockinterface Not in general, but I think MSVC requires it if you want to return by move (e.g. for move-only types). – sehe Apr 14 '14 at 22:41
  • @sehe I do indeed! Thank you so much! I hate to keep pestering, but now that you ask, do you know how to restrict the locale so that it's always a decimal separator? This data will never be visible. It's only used for more compact storage and potentially ever decreasing results. Again, thank you so much! You've been incredible! –  Apr 14 '14 at 23:52
  • @Gracchus I'd suggest to use binary serialization if the data is never visible. – sehe Apr 15 '14 at 00:00
1

You can explicitly state the number of digits you need to be output with the cpp_dec_float::str() method:

std::cout << std::scientific << test_num.str(75) << std::endl;
// output: 0.707106781186547524400844362104849039284835937688474036588339869
mockinterface
  • 14,452
  • 5
  • 28
  • 49
  • Thank you! Hmm, I may not know how many digits I should have. Also, how can the result in scientific notation be stored in a string? Thank you so much in advance! –  Apr 12 '14 at 05:22
  • 1
    For scientific notation try `std::string s = test_num.str(7, std::ios_base::scientific);`, if you don't know how many digits you'll have you are stuck in a hard place, you'll have to decide in advance on the number of digits you'll always show, as `cpp_dec_float` does its own formatting. You can always put the result in a `double` of course, and then format arbitrarily. – mockinterface Apr 12 '14 at 05:33
  • Thank you for showing me how to get the result into a `string`! Is there a performant way to simply remove the `0`s before the `e`? I could do it in a noob way, but `string` parsing always makes me nervous because I've always managed to kill performance, lol. Thank you so much in advance! –  Apr 12 '14 at 14:07
  • Afraid there isn't a simple way, however a simple loop that walks back from `e` skipping zeroes until it meets a non-zero shouldn't be complicated to implement. Further, once you see what dubious egestion `cpp_dec_float` does in its string formatting (see http://code.woboq.org/boost/boost/boost/multiprecision/cpp_dec_float.hpp.html) you will learn to stop worrying about performance and start love the lexical_cast. – mockinterface Apr 12 '14 at 14:39
  • @Gracchus you realize that, since you are using Boost Multiprecision, a header-only library, you by definition had this source included in your source file? – sehe Apr 12 '14 at 21:39