//
// If FLT_RADIX == 2 as expected, we think we can do floating point to rational
// conversions exactly from the exponent and mantissa, otherwise we'll fall back
// on continued fractions.
//
#if FLT_RADIX != 2 && !defined(RATIONAL_USE_CONTINUED_FRACTIONS)
#define RATIONAL_USE_CONTINUED_FRACTIONS
#endif
#ifdef RATIONAL_USE_CONTINUED_FRACTIONS
#ifndef RATIONAL_FLOATING_POINT_ACCURACY
#define RATIONAL_FLOATING_POINT_ACCURACY 0.00001
#endif
#endif
The preprocessor that C++ inherits from C is fairly crude, so I usually give macros
long ugly names precisely because I want ‘em to stick out like sore thumbs.
The idea is that, if we happen to be compiling for something like an IBM mainframe (where, IIRC, FLT_RADIX is 16), then we’ll stick with the good old continued fractions algorithm rather than trying to get my tricky code to work.  In this case, we’ll need to exit a loop when the value is “close enough”; and the …ACCURACY macro provides a default of ±0.001% of the passed value. (I had no reason to pick that number; I just pulled it out from my rear end.)
Users can define either or both of those macros for themselves if they want to force the continued fractions algorithm or provide a different accuracy default.
Excerpts from the rational class:
class rational final
{
bigint num, den;
#ifdef RATIONAL_USE_CONTINUED_FRACTIONS
void init(long double, double);
#else
void init(long double);
#endif
// ...
public:
Explicit construction uses SFINAE to make sure that these functions apply only to floating point arguments.
#ifdef RATIONAL_USE_CONTINUED_FRACTIONS
template<typename F>
explicit rational(F val, double acc = RATIONAL_FLOATING_POINT_ACCURACY,
typename std::enable_if<std::is_floating_point<F>::value,int>::type = 0)
: num(), den()
{
init(long double(val), acc);
}
#else
template<typename F>
explicit rational(F val,
typename std::enable_if<std::is_floating_point<F>::value,int>::type = 0)
: num(), den()
{
init(long double(val));
}
#endif
Same idea for explicit assignment:
#ifdef RATIONAL_USE_CONTINUED_FRACTIONS
template<typename F>
rational& assign(F val, double acc = RATIONAL_FLOATING_POINT_ACCURACY,
typename std::enable_if<std::is_floating_point<F>::value,int>::type = 0)
{
init(long double(val), acc);
return *this;
}
#else
template<typename F>
rational& assign(F val,
typename std::enable_if<std::is_floating_point<F>::value,int>::type = 0)
{
init(long double(val));
return *this;
}
#endif
// ...
};
And in rational.cpp:
#ifdef RATIONAL_USE_CONTINUED_FRACTIONS
void rational::init(long double val, double accuracy)
{
if (std::isnan(val) || std::isinf(val))
{
throw std::invalid_argument(
"Converting non-finite floating point value to rational");
}
long double max_error = std::fabs(val * accuracy);
bool neg = val < 0.0;
if (neg)
{
val = -val;
}
//
// continued fractions (Knuth, 4.5.3):
//
long double val0 = val; // current fraction term
bigint int0 = bigint(val0);
bigint num0 = int0, // current approximation
den0 = 1U; // first guess: int(val) / 1
bigint num1 = 1U, num2; // previous and 2nd-previous numerator
bigint den1 = 0U, den2; // previous and 2nd-previous denominator
long double err0 = std::fabs(val0 - long double(int0));
while (err0 > max_error)
{
val0 = 1.0 / (val0 - long double(int0));
int0 = bigint(val0);
num2 = num1;
num1 = num0;
num0 = int0 * num1 + num2;
den2 = den1;
den1 = den0;
den0 = int0 * den1 + den2;
long double err1 = err0;
err0 = std::fabs(val - long double(num0) / long double(den0));
if (err0 >= err1) // oops...no longer converging
{
throw std::logic_error(
"Overflow converting floating point to rational");
}
}
num = std::move(num0);
den = std::move(den0);
if (neg)
{
num.negate();
}
}
#else
void rational::init(long double val)
{
if (std::isnan(val) || std::isinf(val))
{
throw std::invalid_argument(
"Converting non-finite floating point value to rational");
}
if (val == 0.0)
{
num.clear();
den.set_to_one();
return;
}
bool neg = val < 0.0;
if (neg)
{
val = -val;
}
union
{
long double ldval;
unsigned char ucval[sizeof(long double)];
} mant;
int exp;
mant.ldval = std::frexp(val, &exp);
//
// Find the mantissa bits:
//
static constexpr unsigned mant_msb = // index for mant.ucval
sizeof(long double) - LDBL_MANT_DIG / CHAR_BIT;
static constexpr unsigned phantom_bit = LDBL_MANT_DIG % CHAR_BIT;
//
// Put the most significant 1 back in:
//
mant.ucval[mant_msb] |= 1 << phantom_bit;
//
// And mask off the higher bits just in case:
//
unsigned char mask = 1;
for (unsigned bit = phantom_bit; bit > 0; --bit)
{
mask |= 1 << bit;
}
mant.ucval[mant_msb] &= mask;
//
// Make a fraction equal to just the raw mantissa bits:
//
num.clear();
for (unsigned idx = mant_msb; idx < sizeof(long double); ++idx)
{
num <<= CHAR_BIT;
num += mant.ucval[idx];
}
den.set_to_one();
if (exp < 0)
{
den <<= -exp;
}
else if (exp > 0)
{
num <<= exp;
}
lowest_terms();
if (neg)
{
num.negate();
}
}
#endif // not RATIONAL_USE_CONTINUED_FRACTIONS
And just in case you’re wondering what the lowest_terms() member function is about:
rational& rational::lowest_terms()
{
bigint gcdval(std::move(dbacc::gcd(num, den)));
if (gcdval > 1)
{
num /= gcdval;
den /= gcdval;
}
return *this;
}