summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCedric Snauwaert <csn@openerp.com>2014-09-23 17:39:14 +0200
committerOlivier Dony <odo@openerp.com>2014-09-24 17:11:25 +0200
commitd4972ffdb6b9356a524eef1dbc11f455ff4473f2 (patch)
tree157ec0170efdd29fb0da369dcaf2a32a8673f762
parent1933e926ff7457b6f116296190ec55eb908de9b3 (diff)
[FIX] product,float_utils: perform ceiling via float_round with new rounding_method UP
Modified product ceiling() to use float_round() with special mode for rounding UP (away from zero), avoiding pathological cases where float representations errors were ceiling to the superior unit. Also added correspding tests for rounding_method=UP Fixes issue #1125, and replaces PR #1126.
-rw-r--r--addons/product/_common.py5
-rw-r--r--openerp/addons/base/test/base_test.yml13
-rw-r--r--openerp/tools/float_utils.py29
3 files changed, 34 insertions, 13 deletions
diff --git a/addons/product/_common.py b/addons/product/_common.py
index c05dcee66a2..f44f6b11a4c 100644
--- a/addons/product/_common.py
+++ b/addons/product/_common.py
@@ -20,9 +20,6 @@
##############################################################################
from openerp import tools
-import math
-
-
def rounding(f, r):
# TODO for trunk: log deprecation warning
# _logger.warning("Deprecated rounding method, please use tools.float_round to round floats.")
@@ -32,4 +29,4 @@ def rounding(f, r):
def ceiling(f, r):
if not r:
return f
- return math.ceil(f / r) * r
+ return tools.float_round(f, precision_rounding=r, rounding_method='UP')
diff --git a/openerp/addons/base/test/base_test.yml b/openerp/addons/base/test/base_test.yml
index 11f243592fa..5a30bc22529 100644
--- a/openerp/addons/base/test/base_test.yml
+++ b/openerp/addons/base/test/base_test.yml
@@ -198,8 +198,8 @@
-
!python {model: res.currency}: |
from tools import float_compare, float_is_zero, float_round, float_repr
- def try_round(amount, expected, precision_digits=3, float_round=float_round, float_repr=float_repr):
- result = float_repr(float_round(amount, precision_digits=precision_digits),
+ def try_round(amount, expected, precision_digits=3, float_round=float_round, float_repr=float_repr, rounding_method='HALF-UP'):
+ result = float_repr(float_round(amount, precision_digits=precision_digits, rounding_method=rounding_method),
precision_digits=precision_digits)
assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected)
try_round(2.6745, '2.675')
@@ -213,6 +213,15 @@
try_round(457.4554, '457.455')
try_round(-457.4554, '-457.455')
+ # Try some rounding value with rounding method UP instead of HALF-UP
+ # We use 8.175 because when normalizing 8.175 with precision_digits=3 it gives
+ # us 8175,0000000001234 as value, and if not handle correctly the rounding UP
+ # value will be incorrect (should be 8,175 and not 8,176)
+ try_round(8.175, '8.175', rounding_method='UP')
+ try_round(8.1751, '8.176', rounding_method='UP')
+ try_round(-8.175, '-8.175', rounding_method='UP')
+ try_round(-8.1751, '-8.175', rounding_method='UP')
+
# Extended float range test, inspired by Cloves Almeida's test on bug #882036.
fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555]
expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
diff --git a/openerp/tools/float_utils.py b/openerp/tools/float_utils.py
index 5c934114e3c..79f5a34c7f6 100644
--- a/openerp/tools/float_utils.py
+++ b/openerp/tools/float_utils.py
@@ -29,10 +29,11 @@ def _float_check_precision(precision_digits=None, precision_rounding=None):
return 10 ** -precision_digits
return precision_rounding
-def float_round(value, precision_digits=None, precision_rounding=None):
- """Return ``value`` rounded to ``precision_digits``
- decimal digits, minimizing IEEE-754 floating point representation
- errors, and applying HALF-UP (away from zero) tie-breaking rule.
+def float_round(value, precision_digits=None, precision_rounding=None, rounding_method='HALF-UP'):
+ """Return ``value`` rounded to ``precision_digits`` decimal digits,
+ minimizing IEEE-754 floating point representation errors, and applying
+ the tie-breaking rule selected with ``rounding_method``, by default
+ HALF-UP (away from zero).
Precision must be given by ``precision_digits`` or ``precision_rounding``,
not both!
@@ -41,6 +42,9 @@ def float_round(value, precision_digits=None, precision_rounding=None):
: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 rounding_method: the rounding method used: 'HALF-UP' or 'UP', the first
+ one rounding up to the closest number with the rule that number>=0.5 is
+ rounded up to 1, and the latest one always rounding up.
:return: rounded float
"""
rounding_factor = _float_check_precision(precision_digits=precision_digits,
@@ -52,7 +56,7 @@ def float_round(value, precision_digits=None, precision_rounding=None):
# we normalize the value before rounding it as an integer, and de-normalize
# after rounding: e.g. float_round(1.3, precision_rounding=.5) == 1.5
- # TIE-BREAKING: HALF-UP
+ # TIE-BREAKING: HALF-UP (for normal rounding)
# We want to apply HALF-UP tie-breaking rules, i.e. 0.5 rounds away from 0.
# Due to IEE754 float/double representation limits, the approximation of the
# real value may be slightly below the tie limit, resulting in an error of
@@ -66,8 +70,19 @@ def float_round(value, precision_digits=None, precision_rounding=None):
normalized_value = value / rounding_factor # normalize
epsilon_magnitude = math.log(abs(normalized_value), 2)
epsilon = 2**(epsilon_magnitude-53)
- normalized_value += cmp(normalized_value,0) * epsilon
- rounded_value = round(normalized_value) # round to integer
+ if rounding_method == 'HALF-UP':
+ normalized_value += cmp(normalized_value,0) * epsilon
+ rounded_value = round(normalized_value) # round to integer
+
+ # TIE-BREAKING: UP (for ceiling operations)
+ # When rounding the value up, we instead subtract the epsilon value
+ # as the the approximation of the real value may be slightly *above* the
+ # tie limit, this would result in incorrectly rounding up to the next number
+
+ elif rounding_method == 'UP':
+ normalized_value -= cmp(normalized_value,0) * epsilon
+ rounded_value = math.ceil(normalized_value) # ceil to integer
+
result = rounded_value * rounding_factor # de-normalize
return result