summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicolas Martinelli <nim@odoo.com>2018-01-25 13:06:27 +0100
committerNicolas Martinelli <nim-odoo@users.noreply.github.com>2018-01-29 11:06:33 +0100
commit8ed7570b1cc50c55867f75eafb20c1deb89dbf5c (patch)
treef310adaf8c3f41a19275457b73f10b2c3998a85c
parent43bf9ff29d1897405a575fa8643b7f80b5112c24 (diff)
[FIX] tools: float_round DOWN
The result of the following: ``` float_round(2.5, precision_rounding=0.05, rounding_method='DOWN') ``` is 2.45. Instead, 2.5 is expected. In case of the `DOWN` method, epsilon should be added, not subtracted. In the meantime, we introduce a slight reorganization of the method to make it clearer. opw-805008
-rw-r--r--odoo/addons/base/tests/test_float.py18
-rw-r--r--odoo/tools/float_utils.py27
2 files changed, 31 insertions, 14 deletions
diff --git a/odoo/addons/base/tests/test_float.py b/odoo/addons/base/tests/test_float.py
index 725ba48365b..6dc1cda0550 100644
--- a/odoo/addons/base/tests/test_float.py
+++ b/odoo/addons/base/tests/test_float.py
@@ -92,6 +92,18 @@ class TestFloatPrecision(TransactionCase):
try_round(1.8, '2', 0, method='UP')
try_round(-1.8, '-2', 0, method='UP')
+ # Try some rounding value with rounding method DOWN instead of HALF-UP
+ # We use 2.425 because when normalizing 2.425 with precision_digits=3 it gives
+ # us 2424.9999999999995 as value, and if not handle correctly the rounding DOWN
+ # value will be incorrect (should be 2.425 and not 2.424)
+ try_round(2.425, '2.425', method='DOWN')
+ try_round(2.4249, '2.424', method='DOWN')
+ try_round(-2.425, '-2.425', method='DOWN')
+ try_round(-2.4249, '-2.424', method='DOWN')
+ try_round(-2.500, '-2.500', method='DOWN')
+ try_round(1.8, '1', 0, method='DOWN')
+ try_round(-1.8, '-1', 0, method='DOWN')
+
# 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']
@@ -135,8 +147,8 @@ class TestFloatPrecision(TransactionCase):
try_compare(-657.4444, -657.445, 1)
# Rounding to unusual rounding units (e.g. coin values)
- def try_round(amount, expected, precision_rounding=None):
- value = float_round(amount, precision_rounding=precision_rounding)
+ def try_round(amount, expected, precision_rounding=None, method='HALF-UP'):
+ value = float_round(amount, precision_rounding=precision_rounding, rounding_method=method)
result = float_repr(value, precision_digits=2)
self.assertEqual(result, expected, 'Rounding error: got %s, expected %s' % (result, expected))
@@ -145,6 +157,8 @@ class TestFloatPrecision(TransactionCase):
try_round(457.3, '455.00', precision_rounding=5)
try_round(457.5, '460.00', precision_rounding=5)
try_round(457.1, '456.00', precision_rounding=3)
+ try_round(2.5, '2.50', precision_rounding=0.05, method='DOWN')
+ try_round(-2.5, '-2.50', precision_rounding=0.05, method='DOWN')
def test_rounding_04(self):
""" check that proper rounding is performed for float persistence """
diff --git a/odoo/tools/float_utils.py b/odoo/tools/float_utils.py
index bf3f0463c0a..3e13f9ee0bc 100644
--- a/odoo/tools/float_utils.py
+++ b/odoo/tools/float_utils.py
@@ -61,9 +61,6 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_
# In order to easily support rounding to arbitrary 'steps' (e.g. coin values),
# 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 (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
# 1 unit in the last place (ulp) after rounding.
@@ -74,25 +71,31 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_
# Credit: discussion with OpenERP community members on bug 882036
normalized_value = value / rounding_factor # normalize
+ sign = math.copysign(1.0, normalized_value)
epsilon_magnitude = math.log(abs(normalized_value), 2)
epsilon = 2**(epsilon_magnitude-53)
- if rounding_method == 'HALF-UP':
- normalized_value += math.copysign(epsilon, normalized_value)
- rounded_value = round(normalized_value) # round to integer
- # TIE-BREAKING: UP (for ceiling[resp. flooring] operations)
- # When rounding the value up[resp. down], we instead subtract the epsilon value
+ # TIE-BREAKING: UP/DOWN (for ceiling[resp. flooring] operations)
+ # When rounding the value up[resp. down], we instead subtract[resp. add] 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[resp. down] to the next number
# The math.ceil[resp. math.floor] operation is applied on the absolute value in order to
# round "away from zero" and not "towards infinity", then the sign is
# restored.
- else:
- func = math.floor if rounding_method == 'DOWN' else math.ceil
- sign = math.copysign(1.0, normalized_value)
+ if rounding_method == 'UP':
normalized_value -= sign*epsilon
- rounded_value = func(abs(normalized_value)) * sign
+ rounded_value = math.ceil(abs(normalized_value)) * sign
+
+ elif rounding_method == 'DOWN':
+ normalized_value += sign*epsilon
+ rounded_value = math.floor(abs(normalized_value)) * sign
+
+ # TIE-BREAKING: HALF-UP (for normal rounding)
+ # We want to apply HALF-UP tie-breaking rules, i.e. 0.5 rounds away from 0.
+ else:
+ normalized_value += math.copysign(epsilon, normalized_value)
+ rounded_value = round(normalized_value) # round to integer
result = rounded_value * rounding_factor # de-normalize
return result