1

I want to stringify a fraction of unsigned integers in C++ with variable precision. So 1/3 would be printed as 0.33 using a precision of 2. I know that float and std::ostream::precision could be used for a quick and dirty solution:

std::string stringifyFraction(unsigned numerator,
                              unsigned denominator,
                              unsigned precision)
{
    std::stringstream output;
    output.precision(precision);
    output << static_cast<float>(numerator) / denominator;
    return output.str();
}

However, this is not good enough because float has limited precision and can't actually represent decimal numbers accurately. What other options do I have? Even a double would fail if I wanted 100 digits or so, or in case of a recurring fraction.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • It doesn't seem like your problem is with converting to string, but that you want a way to represent rational numbers without loss of precision. The title of the question maybe your final goal, but it isn't your actual question. – François Andrieux Aug 20 '20 at 19:06
  • @FrançoisAndrieux I am not sure how you came to that conclusion. I already have a lossless representation: the numerator and denominator. The reason why I posted this Q&A is because I frequently run into this problem, like for example printing `uint8_t` RGB values into files in a `[0, 1]` decimal range. Or printing file sizes in bytes as `x.yyy MB`. – Jan Schultke Aug 20 '20 at 19:10

1 Answers1

4

It's always possible to just perform long division to stringify digit-by-digit. Note that the result consists of an integral part and a fractional part. We can get the integral part by simply dividing using the / operator and calling std::to_string. For the rest, we need the following function:

#include <string>

std::string stringifyFraction(const unsigned num,
                              const unsigned den,
                              const unsigned precision)
{
    constexpr unsigned base = 10;

    // prevent division by zero if necessary
    if (den == 0) {
        return "inf";
    }

    // integral part can be computed using regular division
    std::string result = std::to_string(num / den);
    
    // perform first step of long division
    // also cancel early if there is no fractional part
    unsigned tmp = num % den;
    if (tmp == 0 || precision == 0) {
        return result;
    }

    // reserve characters to avoid unnecessary re-allocation
    result.reserve(result.size() + precision + 1);

    // fractional part can be computed using long divison
    result += '.';
    for (size_t i = 0; i < precision; ++i) {
        tmp *= base;
        char nextDigit = '0' + static_cast<char>(tmp / den);
        result.push_back(nextDigit);
        tmp %= den;
    }

    return result;
}

You could easily extend this to work with other bases as well, by just making base a template parameter, but then you couldn't just use std::to_string anymore.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • 2
    OP might want to also display repeating decimals. like `9/11 = 0.(81)`. That would be a simple addition to this already excellent answer – Jeffrey Aug 20 '20 at 19:08