Files
gnucash/src/libqof/qof/gnc-rational.cpp
John Ralls fd6234f58f Better manage truncation in GncRational::round_to_numeric.
Mike Alexander brought this up with a test case that failed to round down
sufficiently; he found that reducing the rounding denominator by 2 sufficed
to make his test case pass.

In fact the sizing of the replacement denominator by shifting the larger of
the numerator or denominator by an arbitrary 62 bits was not correct most
of the time, so instead we begin with a shift of the full lower leg worth,
try to do the conversion, and if the conversion is still “big” shift the
larger value one more and try the operation again, repeating until the
result will fit in a GncNumeric.
2017-04-07 12:38:55 -07:00

317 lines
8.5 KiB
C++

/********************************************************************
* gnc-rational.hpp - A rational number library *
* Copyright 2014 John Ralls <jralls@ceridwen.us> *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
* *
*******************************************************************/
#include <sstream>
#include "gnc-rational.hpp"
#include "gnc-numeric.hpp"
GncRational::GncRational(GncNumeric n) noexcept :
m_num(n.num()), m_den(n.denom())
{
if (m_den.isNeg())
{
m_num *= -m_den;
m_den = 1;
}
}
GncRational::GncRational (gnc_numeric n) noexcept :
m_num (n.num), m_den (n.denom)
{
if (m_den.isNeg())
{
m_num *= -m_den;
m_den = 1;
}
}
bool
GncRational::valid() const noexcept
{
if (m_num.valid() && m_den.valid())
return true;
return false;
}
bool
GncRational::is_big() const noexcept
{
if (m_num.isBig() || m_den.isBig())
return true;
return false;
}
GncRational::operator gnc_numeric () const noexcept
{
if (!valid())
return gnc_numeric_error(GNC_ERROR_OVERFLOW);
try
{
return {static_cast<int64_t>(m_num), static_cast<int64_t>(m_den)};
}
catch (std::overflow_error)
{
return gnc_numeric_error (GNC_ERROR_OVERFLOW);
}
}
GncRational
GncRational::operator-() const noexcept
{
return GncRational(-m_num, m_den);
}
GncRational
GncRational::inv () const noexcept
{
if (m_num == 0)
return *this;
if (m_num < 0)
return GncRational(-m_den, -m_num);
return GncRational(m_den, m_num);
}
GncRational
GncRational::abs() const noexcept
{
if (m_num < 0)
return -*this;
return *this;
}
void
GncRational::operator+=(GncRational b)
{
GncRational new_val = *this + b;
*this = std::move(new_val);
}
void
GncRational::operator-=(GncRational b)
{
GncRational new_val = *this - b;
*this = std::move(new_val);
}
void
GncRational::operator*=(GncRational b)
{
GncRational new_val = *this * b;
*this = std::move(new_val);
}
void
GncRational::operator/=(GncRational b)
{
GncRational new_val = *this / b;
*this = std::move(new_val);
}
int
GncRational::cmp(GncRational b)
{
if (m_den == b.denom())
{
auto b_num = b.num();
return m_num < b_num ? -1 : b_num < m_num ? 1 : 0;
}
auto gcd = m_den.gcd(b.denom());
GncInt128 a_num(m_num * b.denom() / gcd), b_num(b.num() * m_den / gcd);
return a_num < b_num ? -1 : b_num < a_num ? 1 : 0;
}
GncRational::round_param
GncRational::prepare_conversion (GncInt128 new_denom) const
{
if (new_denom == m_den || new_denom == GNC_DENOM_AUTO)
return {m_num, m_den, 0};
GncRational conversion(new_denom, m_den);
auto red_conv = conversion.reduce();
GncInt128 old_num(m_num);
auto new_num = old_num * red_conv.num();
if (new_num.isOverflow())
throw std::overflow_error("Conversion overflow");
auto rem = new_num % red_conv.denom();
new_num /= red_conv.denom();
return {new_num, red_conv.denom(), rem};
}
GncInt128
GncRational::sigfigs_denom(unsigned figs) const noexcept
{
auto num_abs = m_num.abs();
bool not_frac = num_abs > m_den;
int64_t val{ not_frac ? num_abs / m_den : m_den / num_abs };
unsigned digits{};
while (val >= 10)
{
++digits;
val /= 10;
}
return not_frac ? powten(figs - digits - 1) : powten(figs + digits);
}
GncRational
GncRational::reduce() const
{
auto gcd = m_den.gcd(m_num);
if (gcd.isNan() || gcd.isOverflow())
throw std::overflow_error("Reduce failed, calculation of gcd overflowed.");
return GncRational(m_num / gcd, m_den / gcd);
}
GncRational
GncRational::round_to_numeric() const
{
unsigned int ll_bits = GncInt128::legbits;
if (m_num.isZero())
return GncRational(); //Default constructor makes 0/1
if (!(m_num.isBig() || m_den.isBig()))
return *this;
if (m_num.abs() > m_den)
{
auto quot(m_num / m_den);
if (quot.isBig())
{
std::ostringstream msg;
msg << " Cannot be represented as a "
<< "GncNumeric. Its integer value is too large.\n";
throw std::overflow_error(msg.str());
}
GncRational new_v;
while (new_v.num().isZero())
{
try
{
new_v = convert<RoundType::half_down>(m_den / (m_num.abs() >> ll_bits));
if (new_v.is_big())
{
--ll_bits;
new_v = GncRational();
}
}
catch(const std::overflow_error& err)
{
--ll_bits;
}
}
return new_v;
}
auto quot(m_den / m_num);
if (quot.isBig())
return GncRational(); //Smaller than can be represented as a GncNumeric
GncRational new_v;
while (new_v.num().isZero())
{
auto divisor = m_den >> ll_bits;
if (m_num.isBig())
{
GncInt128 oldnum(m_num), num, rem;
oldnum.div(divisor, num, rem);
auto den = m_den / divisor;
num += rem * 2 >= den ? 1 : 0;
if (num.isBig() || den.isBig())
{
--ll_bits;
continue;
}
GncRational new_rational(num, den);
return new_rational;
}
new_v = convert<RoundType::half_down>(m_den / divisor);
if (new_v.is_big())
{
--ll_bits;
new_v = GncRational();
}
}
return new_v;
}
GncRational
operator+(GncRational a, GncRational b)
{
if (!(a.valid() && b.valid()))
throw std::range_error("Operator+ called with out-of-range operand.");
GncInt128 lcm = a.denom().lcm(b.denom());
GncInt128 num(a.num() * lcm / a.denom() + b.num() * lcm / b.denom());
if (!(lcm.valid() && num.valid()))
throw std::overflow_error("Operator+ overflowed.");
GncRational retval(num, lcm);
return retval;
}
GncRational
operator-(GncRational a, GncRational b)
{
GncRational retval = a + (-b);
return retval;
}
GncRational
operator*(GncRational a, GncRational b)
{
if (!(a.valid() && b.valid()))
throw std::range_error("Operator* called with out-of-range operand.");
GncInt128 num (a.num() * b.num()), den(a.denom() * b.denom());
if (!(num.valid() && den.valid()))
throw std::overflow_error("Operator* overflowed.");
GncRational retval(num, den);
return retval;
}
GncRational
operator/(GncRational a, GncRational b)
{
if (!(a.valid() && b.valid()))
throw std::range_error("Operator/ called with out-of-range operand.");
auto a_num = a.num(), b_num = b.num(), a_den = a.denom(), b_den = b.denom();
if (b_num == 0)
throw std::underflow_error("Divide by 0.");
if (b_num.isNeg())
{
a_num = -a_num;
b_num = -b_num;
}
/* q = (a_num * b_den)/(b_num * a_den). If a_den == b_den they cancel out
* and it's just a_num/b_num.
*/
if (a_den == b_den)
return GncRational(a_num, b_num);
/* Protect against possibly preventable overflow: */
if (a_num.isBig() || a_den.isBig() ||
b_num.isBig() || b_den.isBig())
{
GncInt128 gcd = b_den.gcd(a_den);
b_den /= gcd;
a_den /= gcd;
}
GncInt128 num(a_num * b_den), den(a_den * b_num);
if (!(num.valid() && den.valid()))
throw std::overflow_error("Operator/ overflowed.");
return GncRational(num, den);
}