Files
gnucash/bindings/python/tests/test_numeric.py
T
Ryan Duguid faf7a9999d [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.
2026-10-30 09:14:23 +11:00

148 lines
5.6 KiB
Python

from unittest import TestCase, main
from gnucash import GncNumeric, GNC_DENOM_AUTO, GNC_HOW_DENOM_FIXED, \
GNC_HOW_RND_NEVER, GNC_HOW_RND_FLOOR, GNC_HOW_RND_CEIL
class TestGncNumeric(TestCase):
def test_defaut(self):
num = GncNumeric()
self.assertEqual(str(num), "0/1")
self.assertEqual(num.num(), 0)
self.assertEqual(num.denom(), 1)
def test_from_num_denom(self):
num = GncNumeric(1, 2)
self.assertEqual(str(num), "1/2")
self.assertEqual(num.num(), 1)
self.assertEqual(num.denom(), 2)
def test_from_int(self):
num = GncNumeric(3)
self.assertEqual(str(num), "3/1")
self.assertEqual(num.num(), 3)
self.assertEqual(num.denom(), 1)
with self.assertRaises(Exception) as context:
GncNumeric((2**64)+1)
#On Linux it raises an OverflowError while on MacOS it's a TypeError.
self.assertTrue(isinstance(context.exception, TypeError) or
isinstance(context.exception, OverflowError))
def test_from_float(self):
num = GncNumeric(3.1, 20, GNC_HOW_DENOM_FIXED | GNC_HOW_RND_NEVER)
self.assertEqual(str(num), "62/20")
self.assertEqual(num.num(), 62)
self.assertEqual(num.denom(), 20)
num = GncNumeric(1/3.0, 10000000000, GNC_HOW_RND_FLOOR)
self.assertEqual(str(num), "3333333333/10000000000")
self.assertEqual(num.num(), 3333333333)
self.assertEqual(num.denom(), 10000000000)
num = GncNumeric(1/3.0, 10000000000, GNC_HOW_RND_CEIL)
self.assertEqual(str(num), "3333333334/10000000000")
self.assertEqual(num.num(), 3333333334)
self.assertEqual(num.denom(), 10000000000)
def test_from_float_auto(self):
num = GncNumeric(3.1)
self.assertEqual(str(num), "31/10")
self.assertEqual(num.num(), 31)
self.assertEqual(num.denom(), 10)
def test_from_instance(self):
orig = GncNumeric(3)
num = GncNumeric(instance=orig.instance)
self.assertEqual(str(num), "3/1")
self.assertEqual(num.num(), 3)
self.assertEqual(num.denom(), 1)
def test_from_str(self):
num = GncNumeric("3.1")
self.assertEqual(str(num), "31/10")
self.assertEqual(num.num(), 31)
self.assertEqual(num.denom(), 10)
num = GncNumeric("1/3")
self.assertEqual(str(num), "1/3")
self.assertEqual(num.num(), 1)
self.assertEqual(num.denom(), 3)
def test_to_str(self):
num = GncNumeric("1000/3")
self.assertEqual(str(num), "1000/3")
num = GncNumeric(1, 0)
self.assertEqual(str(num), "1/0")
def test_to_double(self):
for test_num in [0.0, 1.1, -1.1, 1/3.0]:
self.assertEqual(GncNumeric(test_num).to_double(), test_num)
def test_to_fraction(self):
fraction = GncNumeric("1000/3").to_fraction()
self.assertEqual(fraction.numerator, 1000)
self.assertEqual(fraction.denominator, 3)
def test_reflected_operators(self):
# A plain number on the left must stay the left-hand operand.
self.assertEqual(10 - GncNumeric(3), GncNumeric(7))
self.assertEqual(1.5 - GncNumeric(1, 2), GncNumeric(1))
self.assertEqual(10 / GncNumeric(4), GncNumeric(5, 2))
self.assertEqual(1 / GncNumeric(3), GncNumeric(1, 3))
self.assertEqual(7 // GncNumeric(2), GncNumeric(3))
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)
with self.assertRaises(TypeError):
GncNumeric("1", 2)
with self.assertRaises(TypeError):
GncNumeric(1.1, "round")
with self.assertRaises(TypeError):
GncNumeric(complex(1, 1))
def test_num_denom_read_only(self):
# num/denom are read-only accessor methods, not writable fields.
# Assigning to them used to silently do nothing and corrupt data
# (Bug 799796); it must now raise instead.
num = GncNumeric(1, 2)
with self.assertRaises(AttributeError):
num.num = 5
with self.assertRaises(AttributeError):
num.denom = 5
# The value is unchanged and the accessors still work.
self.assertEqual(num.num(), 1)
self.assertEqual(num.denom(), 2)
if __name__ == '__main__':
main()