From dcb5ff6eacc63cebdc03f8615b18a002cb6ad23d Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 14 Nov 2011 17:36:17 +0100 Subject: [PATCH] [IMP] res.currency,base: factor out float rounding methods in tools to allow use without currency Pending: addition of tests bzr revid: odo@openerp.com-20111114163617-kw7u8rxcie7m2hj5 --- openerp/addons/base/res/res_currency.py | 41 ++++++++++--- openerp/tools/misc.py | 79 ++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index 32aa387e828..c0a2bebc104 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -24,7 +24,7 @@ import netsvc from osv import fields, osv import tools -from tools.misc import currency +from tools.misc import currency, float_round, float_is_zero, float_compare from tools.translate import _ CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?') @@ -127,15 +127,40 @@ class res_currency(osv.osv): return [(x['id'], tools.ustr(x['name']) + (x['symbol'] and (' (' + tools.ustr(x['symbol']) + ')') or '')) for x in reads] def round(self, cr, uid, currency, amount): - if currency.rounding == 0: - return 0.0 - else: - # /!\ First member below must be rounded to full unit! - # Do not pass a rounding digits value to round() - return round(amount / currency.rounding) * currency.rounding + """Return ``amount`` rounded according to ``currency``'s + rounding rules, also minimizing IEEE-754 floating point + representation errors. + + :param browse_record currency: currency for which we are rounding + :param float amount: the amount to round + :return: rounded float + """ + return float_round(amount, precision_rounding=currency.rounding) + + def compare_amounts(self, cr, uid, currency, amount1, amount2): + """Compare ``amount1`` and ``amount2`` according to ``currency``'s + rounding rules, and return (resp.) -1, 0 or 1, if ``amount1`` + is (resp.) lower than, equal to, or greater than ``amount2``. + + For example 1.432 and 1.431 are equal if currency is rounded to + 2 digits, so this method would return 0 + + :param browse_record currency: currency for which we are rounding + :param float amount1: first amount to compare + :param float amount2: second amount to compare + :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than, + equal to, or greater than ``amount2``, according to + ``currency``'s rounding. + """ + return float_compare(amount1, amount2, precision_rounding=currency.rounding) def is_zero(self, cr, uid, currency, amount): - return abs(self.round(cr, uid, currency, amount)) < currency.rounding + """Returns true if ``amount`` is small enough to be treated as + zero according to ``currency``'s rounding rules. + :param browse_record currency: currency for which we are rounding + :param float amount: amount to compare with currency's zero + """ + return float_is_zero(amount, precision_rounding=currency.rounding) def _get_conversion_rate(self, cr, uid, from_currency, to_currency, context=None): if context is None: diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index dbccbe5b217..ae28567e19b 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -1200,4 +1200,81 @@ class UnquoteEvalContext(defaultdict): def __missing__(self, key): return unquote(key) -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: +def _float_check_precision(precision_digits=None, precision_rounding=None): + assert (precision_digits is not None or precision_rounding is not None) and \ + not (precision_digits and precision_rounding),\ + "exactly one of precision_digits and precision_rounding must be specified" + if precision_digits is not None: + return 10 ** -precision_digits + return precision_rounding + +def float_round(amount, precision_digits=None, precision_rounding=None): + """Return ``amount`` rounded to ``precision_digits`` + decimal digits, minimizing IEEE-754 floating point representation + errors. + Precision must be given by ``precision_digits`` or ``precision_rounding``, + not both! + Example on Python 2.7.2:: + + >>> round_float(2.675) + 2.68 + >>> round(2.675,2) + 2.67 + + :param float amount: the amount to round + :param int precision_digits: number of decimal digits to round to. + :param float precision_rounding: decimal number representing the minimum + non-zero value at the desired precision (for example, 0.01 for a + 2-digit precision). + :return: rounded float + """ + rounding_factor = _float_check_precision(precision_digits=precision_digits, + precision_rounding=precision_rounding) + if rounding_factor == 0: return 0.0 + # /!\ First member below must be rounded to full unit! + # Do not pass rounding digits to round()! + return round(amount / rounding_factor) * rounding_factor + +def float_is_zero(amount, precision_digits=None, precision_rounding=None): + """Returns true if ``amount`` is small enough to be treated as + zero at the given precision. + Precision must be given by ``precision_digits`` or ``precision_rounding``, + not both! + + :param int precision_digits: number of decimal digits to round to. + :param float precision_rounding: decimal number representing the minimum + non-zero value at the desired precision (for example, 0.01 for a + 2-digit precision). + :param float amount: amount to compare with currency's zero + :return: True if ``amount`` is considered 0 + """ + rounding_factor = _float_check_precision(precision_digits=precision_digits, + precision_rounding=precision_rounding) + return abs(float_round(amount, precision_rounding=rounding_factor)) < rounding_factor + +def float_compare(amount1, amount2, precision_digits=None, precision_rounding=None): + """Compare ``amount1`` and ``amount2`` according + to the given precision. + Precision must be given by ``precision_digits`` or ``precision_rounding``, + not both! + + For example 1.432 and 1.431 are equal at 2 digits precision, + so this method would return 0 + + :param int precision_digits: number of decimal digits to round to. + :param float precision_rounding: decimal number representing the minimum + non-zero value at the desired precision (for example, 0.01 for a + 2-digit precision). + :param float amount1: first amount to compare + :param float amount2: second amount to compare + :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than, + equal to, or greater than ``amount2``, at the given precision. + """ + rounding_factor = _float_check_precision(precision_digits=precision_digits, + precision_rounding=precision_rounding) + delta = amount1 - amount2 + if float_is_zero(delta, precision_rounding=rounding_factor): return 0 + delta = float_round(delta, precision_rounding=rounding_factor) + return -1 if delta < 0 else 1 + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file