[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
This commit is contained in:
committed by
Nicolas Martinelli
parent
43bf9ff29d
commit
8ed7570b1c
@@ -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 """
|
||||
|
||||
+15
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user