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.
pull/241/head
Guy Taylor 9 years ago committed by John Ralls
parent 5aa048e01d
commit 1ef379a704

@ -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."""

@ -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()

@ -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)

@ -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()
Loading…
Cancel
Save