[FIX] base: support float rounding with rounding_method=UP (ceiling)
Add rounding_method parameter on float_round method to offer HALF-UP (default, usual round) or UP (ceiling) rounding method. Use the second method instead of math.ceil() for product reservations. For UP, the python math.ceil() method uses "torwards infinity" rounding method while we want "away from zero". Therefore we use the absolute value of normalized_value to make sure than -1.8 is rounded to -2.0 and not -1. Fixes #1125 #2793 This is a cherry-pick ofd4972ff
which was reverted at333852e
due to remaining issue with negative values.
This commit is contained in:
parent
354b82bee0
commit
7705f883d2
|
@ -20,9 +20,6 @@
|
||||||
##############################################################################
|
##############################################################################
|
||||||
from openerp import tools
|
from openerp import tools
|
||||||
|
|
||||||
import math
|
|
||||||
|
|
||||||
|
|
||||||
def rounding(f, r):
|
def rounding(f, r):
|
||||||
# TODO for trunk: log deprecation warning
|
# TODO for trunk: log deprecation warning
|
||||||
# _logger.warning("Deprecated rounding method, please use tools.float_round to round floats.")
|
# _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):
|
def ceiling(f, r):
|
||||||
if not r:
|
if not r:
|
||||||
return f
|
return f
|
||||||
return math.ceil(f / r) * r
|
return tools.float_round(f, precision_rounding=r, rounding_method='UP')
|
||||||
|
|
|
@ -198,8 +198,8 @@
|
||||||
-
|
-
|
||||||
!python {model: res.currency}: |
|
!python {model: res.currency}: |
|
||||||
from tools import float_compare, float_is_zero, float_round, float_repr
|
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):
|
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),
|
result = float_repr(float_round(amount, precision_digits=precision_digits, rounding_method=rounding_method),
|
||||||
precision_digits=precision_digits)
|
precision_digits=precision_digits)
|
||||||
assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected)
|
assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected)
|
||||||
try_round(2.6745, '2.675')
|
try_round(2.6745, '2.675')
|
||||||
|
@ -213,6 +213,18 @@
|
||||||
try_round(457.4554, '457.455')
|
try_round(457.4554, '457.455')
|
||||||
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.176', rounding_method='UP')
|
||||||
|
try_round(-6.000, '-6.000', rounding_method='UP')
|
||||||
|
try_round(1.8, '2', 0, rounding_method='UP')
|
||||||
|
try_round(-1.8, '-2', 0, rounding_method='UP')
|
||||||
|
|
||||||
# Extended float range test, inspired by Cloves Almeida's test on bug #882036.
|
# Extended float range test, inspired by Cloves Almeida's test on bug #882036.
|
||||||
fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555]
|
fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555]
|
||||||
expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
|
expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
|
||||||
|
|
|
@ -29,10 +29,11 @@ def _float_check_precision(precision_digits=None, precision_rounding=None):
|
||||||
return 10 ** -precision_digits
|
return 10 ** -precision_digits
|
||||||
return precision_rounding
|
return precision_rounding
|
||||||
|
|
||||||
def float_round(value, precision_digits=None, precision_rounding=None):
|
def float_round(value, precision_digits=None, precision_rounding=None, rounding_method='HALF-UP'):
|
||||||
"""Return ``value`` rounded to ``precision_digits``
|
"""Return ``value`` rounded to ``precision_digits`` decimal digits,
|
||||||
decimal digits, minimizing IEEE-754 floating point representation
|
minimizing IEEE-754 floating point representation errors, and applying
|
||||||
errors, and applying HALF-UP (away from zero) tie-breaking rule.
|
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``,
|
Precision must be given by ``precision_digits`` or ``precision_rounding``,
|
||||||
not both!
|
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
|
:param float precision_rounding: decimal number representing the minimum
|
||||||
non-zero value at the desired precision (for example, 0.01 for a
|
non-zero value at the desired precision (for example, 0.01 for a
|
||||||
2-digit precision).
|
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
|
:return: rounded float
|
||||||
"""
|
"""
|
||||||
rounding_factor = _float_check_precision(precision_digits=precision_digits,
|
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
|
# 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
|
# 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.
|
# 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
|
# 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
|
# real value may be slightly below the tie limit, resulting in an error of
|
||||||
|
@ -66,8 +70,23 @@ def float_round(value, precision_digits=None, precision_rounding=None):
|
||||||
normalized_value = value / rounding_factor # normalize
|
normalized_value = value / rounding_factor # normalize
|
||||||
epsilon_magnitude = math.log(abs(normalized_value), 2)
|
epsilon_magnitude = math.log(abs(normalized_value), 2)
|
||||||
epsilon = 2**(epsilon_magnitude-53)
|
epsilon = 2**(epsilon_magnitude-53)
|
||||||
normalized_value += cmp(normalized_value,0) * epsilon
|
if rounding_method == 'HALF-UP':
|
||||||
rounded_value = round(normalized_value) # round to integer
|
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
|
||||||
|
# The math.ceil operation is applied on the absolute value in order to
|
||||||
|
# round "away from zero" and not "towards infinity", then the sign is
|
||||||
|
# restored.
|
||||||
|
|
||||||
|
elif rounding_method == 'UP':
|
||||||
|
sign = cmp(normalized_value, 0)
|
||||||
|
normalized_value -= sign*epsilon
|
||||||
|
rounded_value = math.ceil(abs(normalized_value))*sign # ceil to integer
|
||||||
|
|
||||||
result = rounded_value * rounding_factor # de-normalize
|
result = rounded_value * rounding_factor # de-normalize
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue