From 1ef379a704e070a174eebb894487a230008d9977 Mon Sep 17 00:00:00 2001 From: Guy Taylor Date: Wed, 17 May 2017 11:37:20 +0100 Subject: [PATCH] Fix Python GncNumeric for non (int, int) pairs At current the Python GncNumeric has issues with type conversion eg. * GncNumeric(1.3) = 1.00 * GncNumeric("1.3") is OK but any future methods error This behaviour was relied on for the Account tests to pass as it used GncNumeric(0.5) == GncNumeric(1.0) but this is not what many users would expect. This fix alows GncNumeric to be constructed from a (int, int) numerator/denominator pair or int/float/str where double_to_gnc_numeric and string_to_gnc_numeric from C is used. --- bindings/python/gnucash_core.py | 66 +++++++++---- bindings/python/tests/runTests.py.in | 3 +- bindings/python/tests/test_account.py | 4 +- .../python-bindings/tests/test_numeric.py | 96 +++++++++++++++++++ 4 files changed, 149 insertions(+), 20 deletions(-) create mode 100644 src/optional/python-bindings/tests/test_numeric.py diff --git a/bindings/python/gnucash_core.py b/bindings/python/gnucash_core.py index b72ecb122b..76d838e004 100644 --- a/bindings/python/gnucash_core.py +++ b/bindings/python/gnucash_core.py @@ -42,7 +42,9 @@ from gnucash_core_c import gncInvoiceLookup, gncInvoiceGetInvoiceFromTxn, \ gncTaxTableLookup, gncTaxTableLookupByName, gnc_search_invoice_on_id, \ gnc_search_customer_on_id, gnc_search_bill_on_id , \ gnc_search_vendor_on_id, gncInvoiceNextID, gncCustomerNextID, \ - gncVendorNextID, gncTaxTableGetTables + gncVendorNextID, gncTaxTableGetTables, gnc_numeric_zero, \ + gnc_numeric_create, double_to_gnc_numeric, string_to_gnc_numeric, \ + gnc_numeric_to_string class GnuCashCoreClass(ClassFromFunctions): _module = gnucash_core_c @@ -271,26 +273,56 @@ class GncNumeric(GnuCashCoreClass): Look at gnc-numeric.h to see how to use these """ - def __init__(self, num=0, denom=1, **kargs): - """Constructor that allows you to set the numerator and denominator or - leave them blank with a default value of 0 (not a good idea since there - is currently no way to alter the value after instantiation) + def __init__(self, *args, **kargs): + """Constructor that supports the following formats: + * No arguments defaulting to zero: eg. GncNumeric() == 0/1 + * A integer: e.g. GncNumeric(1) == 1/1 + * Numerator and denominator intager pair: eg. GncNumeric(1, 2) == 1/2 + * A floating point number: e.g. GncNumeric(0.5) == 1/2 + * A floating point number with defined conversion: e.g. + GncNumeric(0.5, GNC_DENOM_AUTO, + GNC_HOW_DENOM_FIXED | GNC_HOW_RND_NEVER) == 1/2 + * A string: e.g. GncNumeric("1/2") == 1/2 """ - GnuCashCoreClass.__init__(self, num, denom, **kargs) - #if INSTANCE_ARG in kargs: - # GnuCashCoreClass.__init__(**kargs) - #else: - # self.set_denom(denom) # currently undefined - # self.set_num(num) # currently undefined + if 'instance' not in kargs: + kargs['instance'] = GncNumeric.__args_to_instance(args) + GnuCashCoreClass.__init__(self, [], **kargs) + + @staticmethod + def __args_to_instance(args): + if len(args) == 0: + return gnc_numeric_zero() + elif len(args) == 1: + arg = args[0] + if type(arg) == int: + return gnc_numeric_create(arg ,1) + elif type(arg) == float: + return double_to_gnc_numeric(arg, GNC_DENOM_AUTO, GNC_HOW_DENOM_FIXED | GNC_HOW_RND_NEVER) + elif type(arg) == str: + instance = gnc_numeric_zero() + if not string_to_gnc_numeric(arg, instance): + raise TypeError('Failed to convert to GncNumeric: ' + str(args)) + return instance + else: + raise TypeError('Only single int/float/str allowed: ' + str(args)) + elif len(args) == 2: + if type(args[0]) == int and type(args[1]) == int: + return gnc_numeric_create(*args) + else: + raise TypeError('Only two ints allowed: ' + str(args)) + elif len(args) == 3: + if type(args[0]) == float \ + and type(args[1]) == type(GNC_DENOM_AUTO) \ + and type(args[2]) == type(GNC_HOW_DENOM_FIXED): + return double_to_gnc_numeric(*args) + else: + raise TypeError('Only (float, GNC_HOW_RND_*, GNC_HOW_RND_*, GNC_HOW_RND_*) allowed: ' + str(args)) + else: + raise TypeError('Required single int/float/str or two ints: ' + str(args)) def __unicode__(self): """Returns a human readable numeric value string as UTF8.""" - if self.denom() == 0: - return "Division by zero" - else: - value_float = self.to_double() - value_str = u"{0:.{1}f}".format(value_float,2) ## The second argument is the precision. It would be nice to be able to make it configurable. - return value_str + return gnc_numeric_to_string(self.instance) def __str__(self): """returns a human readable numeric value string as bytes.""" diff --git a/bindings/python/tests/runTests.py.in b/bindings/python/tests/runTests.py.in index 0a1d269201..60d040fc4b 100755 --- a/bindings/python/tests/runTests.py.in +++ b/bindings/python/tests/runTests.py.in @@ -13,9 +13,10 @@ from test_split import TestSplit from test_transaction import TestTransaction from test_business import TestBusiness from test_commodity import TestCommodity, TestCommodityNamespace +from test_numeric import TestGncNumeric def test_main(): - test_support.run_unittest(TestBook, TestAccount, TestSplit, TestTransaction, TestBusiness, TestCommodity, TestCommodityNamespace) + test_support.run_unittest(TestBook, TestAccount, TestSplit, TestTransaction, TestBusiness, TestCommodity, TestCommodityNamespace, TestGncNumeric) if __name__ == '__main__': test_main() diff --git a/bindings/python/tests/test_account.py b/bindings/python/tests/test_account.py index b05f1718a2..5ba972e127 100644 --- a/bindings/python/tests/test_account.py +++ b/bindings/python/tests/test_account.py @@ -40,7 +40,7 @@ class TestAccount( AccountSession ): s1a = Split(self.book) s1a.SetParent(tx) s1a.SetAccount(self.account) - s1a.SetAmount(GncNumeric(1.0)) + s1a.SetAmount(GncNumeric(1.3)) s1a.SetValue(GncNumeric(100.0)) s1b = Split(self.book) @@ -52,7 +52,7 @@ class TestAccount( AccountSession ): s2a = Split(self.book) s2a.SetParent(tx) s2a.SetAccount(self.account) - s2a.SetAmount(GncNumeric(-0.5)) + s2a.SetAmount(GncNumeric(-1.3)) s2a.SetValue(GncNumeric(-100.0)) s2b = Split(self.book) diff --git a/src/optional/python-bindings/tests/test_numeric.py b/src/optional/python-bindings/tests/test_numeric.py new file mode 100644 index 0000000000..3d8f9da8a7 --- /dev/null +++ b/src/optional/python-bindings/tests/test_numeric.py @@ -0,0 +1,96 @@ +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) + + # Safest outcome at current. This can be fixed but correct bounds checks + # are required to ensure gint64 is not overflowed. + def test_from_long(self): + with self.assertRaises(TypeError): + GncNumeric(3L) + + 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_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)) + +if __name__ == '__main__': + main()