I am creating unit tests for a function that rounds "rational" numbers stored as strings. The current rounding implementation casts the strings to a floating point type:
#include <boost/lexical_cast.hpp>
#include <iomanip>
#include <limits>
#include <sstream>
template<typename T = double,
size_t PRECISION = std::numeric_limits<T>::digits10>
std::string Round(const std::string& number)
{
std::stringstream ss{};
ss << std::fixed << std::setprecision(PRECISION);
ss << boost::lexical_cast<T>(number);
return ss.str();
}
In one of my tests, I input the number 3.55, which is represented as 3.5499999... on my machine. It all goes well when rounding from 2 decimals to 10. However, when I round to the first decimal, I unsurprisingly get 3.5 instead of 3.6.
What would be a simple method to avoid this error?
Currently, the best solution I was able to find was to use a multiple precision type:
#include <boost/multiprecision/cpp_dec_float.hpp>
#include <iomanip>
#include <sstream>
template<size_t PRECISION = 10>
std::string Round(const std::string& number)
{
using FixedPrecision =
boost::multiprecision::number<
boost::multiprecision::cpp_dec_float<PRECISION>>;
std::stringstream ss{};
ss << std::fixed << std::setprecision(PRECISION);
ss << FixedPrecision{number};
return ss.str();
}
While this solution addresses the problem in a straightforward way (vs manually parsing strings or creating a Rational number class), I find it overkill for such a simple problem.
To find ways to address this problem, I peeked at some calculators' implementations. I looked at gnome-calculator's source code and found that it uses GNU MPFR. I then looked at SpeedCrunch's implementation and found it re-uses the same code as bc, which employs a rational type (numerator, denominator).
Am I overlooking something?
If you are trying to round strings for a given number of decimal places (
n
decimal), you can do this directly on the string "the human way" : First check that the string has a decimal point. if it has one, check if it it has ann+1
digit after the decimal point. If it does, but it is less than five, you can substring the head of the string up to then
decimal. If it is greater than five, you have to transform your string, basically backtrack until you find a non '9' digit 'd', replace it with 'd+1' and set all the nines you found to 0. If ALL the digits before the n+1 decimal are nines (say -999.99879) append a 1 at the top(after the sign if there is one), and set all the nines you found to zero (-1000.00879). A bit tedious and somewhat inefficient, but straightforward and follows grammar school intuition.