summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMax Nikulin <manikulin@gmail.com>2024-07-08 17:17:30 +0700
committerJohn Wiegley <johnw@newartisans.com>2024-08-05 08:35:14 -0700
commite6dae78c033ea970a459b1a0ccc2f1310d1bff96 (patch)
tree510dc67c111f786f3a2ecdb861b4602c73a04e64 /src
parent064012a0d8c18aac253de79154175400fe7ad9cc (diff)
downloadledger-e6dae78c033ea970a459b1a0ccc2f1310d1bff96.tar.gz
ledger-e6dae78c033ea970a459b1a0ccc2f1310d1bff96.tar.bz2
ledger-e6dae78c033ea970a459b1a0ccc2f1310d1bff96.zip
Fix denominator of roundto result
Multiprecision rational created from a double value may have large power of 2 denominator since fractional decimal numbers can not be represented as binary floating point numbers. It leads to failed assertion when result is compared to a value converted directly from strings. Use integer multiprecision arithmetics to round numbers to ensure proper denominator. Inspired by python gmpy2 package <https://github.com/aleaxit/gmpy/blob/3e4564ae9d/src/gmpy2_mpq_misc.c#L315> The change makes `roundto` symmetric for positive/negative arguments. Halves are rounded to nearest even. Rounded away from zero are discussed in #1663 and it may be achieved with minimal modification. - See #2329 - Closes #1983
Diffstat (limited to 'src')
-rw-r--r--src/amount.cc41
1 files changed, 39 insertions, 2 deletions
diff --git a/src/amount.cc b/src/amount.cc
index 484c3099..604519df 100644
--- a/src/amount.cc
+++ b/src/amount.cc
@@ -684,8 +684,45 @@ void amount_t::in_place_roundto(int places)
{
if (! quantity)
throw_(amount_error, _("Cannot round an uninitialized amount"));
- double x=ceil(mpq_get_d(MP(quantity))*pow(10, places) - 0.49999999) / pow(10, places);
- mpq_set_d(MP(quantity), x);
+
+ // <https://github.com/ledger/ledger/issues/2362>
+ // _dup() call (see in_place_ceiling and in_place_floor)
+ // leads to 2 failures in the balance/testRound test,
+ // however in its absence this method affects copies of amount_t instances.
+ // Remove expected_failures from amount/testRound
+ // and update balance/testRound
+ // when uncommenting the following line.
+ // _dup();
+
+ mpz_t& scale(temp);
+ if (places)
+ mpz_ui_pow_ui(scale, 10, labs(places));
+
+ if (places > 0) {
+ mpz_mul(mpq_numref(MP(quantity)), mpq_numref(MP(quantity)), scale);
+ } else if (places < 0) {
+ mpz_mul(mpq_denref(MP(quantity)), mpq_denref(MP(quantity)), scale);
+ }
+
+ auto whole(mpq_numref(tempq));
+ auto reminder(mpq_denref(tempq));
+ mpz_fdiv_qr(whole, reminder, mpq_numref(MP(quantity)), mpq_denref(MP(quantity)));
+ mpz_mul_2exp(reminder, reminder, 1);
+ const int rem_denom_cmp = mpz_cmp(reminder, mpq_denref(MP(quantity)));
+ if (rem_denom_cmp > 0
+ || (rem_denom_cmp == 0 && mpz_odd_p(whole)))
+ mpz_add_ui(whole, whole, 1);
+
+ if (places > 0) {
+ mpq_set_num(MP(quantity), whole);
+ mpq_set_den(MP(quantity), scale);
+ mpq_canonicalize(MP(quantity));
+ } else if (places == 0)
+ mpq_set_z(MP(quantity), whole);
+ else {
+ mpq_set_ui(MP(quantity), 0, 1);
+ mpz_mul(mpq_numref(MP(quantity)), whole, scale);
+ }
}
void amount_t::in_place_unround()