[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.
This commit is contained in:
Ryan Duguid committed 2026-10-30 09:14:23 +11:00
1 parent 65949b43f0
commit faf7a9999d
2 files changed
+27 -2

No files matched your search

+5 -2
View File
@@ -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):
+22
View File
@@ -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)