From faf7a9999d7816c43ebc55a3d4ecc945b23fd0b0 Mon Sep 17 00:00:00 2001 From: Ryan Duguid Date: Fri, 30 Oct 2026 08:56:39 +1100 Subject: [PATCH] [bindings/python] Floor GncNumeric // and make int() exact GncNumeric // truncated towards zero, so GncNumeric(-7, 2) // 1 gave -3 where Python's // floors to -4. int() converted through a float and lost precision above 2**53: int(GncNumeric(2**53 + 1)) returned 9007199254740992. Round // with GNC_HOW_RND_FLOOR and convert int() through the exact fraction. int() now goes through to_fraction(), which read a negative denominator as a divisor; GnuCash treats it as a multiplier, so GncNumeric(3, -100) is 300. to_fraction() now does the same. --- bindings/python/gnucash_core.py | 7 +++++-- bindings/python/tests/test_numeric.py | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py index 5cefcfba53..16fcb39921 100644 --- a/bindings/python/gnucash_core.py +++ b/bindings/python/gnucash_core.py @@ -648,7 +648,7 @@ class GncNumeric(GnuCashCoreClass): return a.div(b, GNC_DENOM_AUTO, GNC_HOW_RND_ROUND) def _floordiv(a, b): - return a.div(b, 1, GNC_HOW_RND_TRUNC) + return a.div(b, 1, GNC_HOW_RND_FLOOR) __add__, __radd__ = _operator_fallbacks(_add, operator.add) __iadd__ = __add__ @@ -722,7 +722,7 @@ class GncNumeric(GnuCashCoreClass): return self.to_double() def __int__(self): - return int(self.to_double()) + return int(self.to_fraction()) def __pos__(a): """+a""" @@ -738,6 +738,9 @@ class GncNumeric(GnuCashCoreClass): def to_fraction(self): from fractions import Fraction + # A negative denominator multiplies: 3/-100 is 300. + if self.denom() < 0: + return Fraction(self.num() * -self.denom()) return Fraction(self.num(), self.denom()) def __str__(self): diff --git a/bindings/python/tests/test_numeric.py b/bindings/python/tests/test_numeric.py index 01293fe039..c91a9f5506 100644 --- a/bindings/python/tests/test_numeric.py +++ b/bindings/python/tests/test_numeric.py @@ -95,6 +95,28 @@ class TestGncNumeric(TestCase): self.assertEqual(10 + GncNumeric(3), GncNumeric(13)) self.assertEqual(10 * GncNumeric(3), GncNumeric(30)) + def test_to_int(self): + # int() truncates towards zero and must not round through a float. + self.assertEqual(int(GncNumeric(7, 2)), 3) + self.assertEqual(int(GncNumeric(-7, 2)), -3) + self.assertEqual(int(GncNumeric(2**53 + 1)), 2**53 + 1) + self.assertEqual(int(GncNumeric(-(2**62 + 1), 1)), -(2**62 + 1)) + self.assertEqual(int(GncNumeric(3, -100)), 300) + self.assertEqual(int(GncNumeric(-3, -100)), -300) + + def test_floordiv(self): + # // rounds towards negative infinity, as it does for int and Fraction. + self.assertEqual(GncNumeric(7, 2) // 1, GncNumeric(3)) + self.assertEqual(GncNumeric(-7, 2) // 1, GncNumeric(-4)) + self.assertEqual(GncNumeric(-1, 2) // 1, GncNumeric(-1)) + self.assertEqual(GncNumeric(-7) // GncNumeric(2), GncNumeric(-4)) + self.assertEqual(-7 // GncNumeric(2), GncNumeric(-4)) + self.assertEqual(GncNumeric(-6) // 3, GncNumeric(-2)) + self.assertEqual(GncNumeric(7) // -2, GncNumeric(-4)) + self.assertEqual(GncNumeric(-7) // GncNumeric(-2), GncNumeric(3)) + self.assertEqual(GncNumeric(7) // GncNumeric(3, 2), GncNumeric(4)) + self.assertEqual(GncNumeric(-7) // GncNumeric(3, 2), GncNumeric(-5)) + def test_incorect_args(self): with self.assertRaises(TypeError): GncNumeric(1, 2, 3)