[IMP] float_utils: simplified code, added float_repr
lp bug: https://launchpad.net/bugs/882036 fixed bzr revid: odo@openerp.com-20111220234740-kotcgoz3opcbkx4b
This commit is contained in:
parent
707bf0d8aa
commit
759ccd9845
|
@ -148,16 +148,20 @@
|
|||
"Float precision tests: verify that float rounding methods are working correctly via res.currency"
|
||||
-
|
||||
!python {model: res.currency}: |
|
||||
from tools import float_repr
|
||||
from math import log10
|
||||
currency = self.browse(cr, uid, ref('base.EUR'))
|
||||
def try_round(amount, expected, self=self, cr=cr, currency=currency):
|
||||
result = str(self.round(cr, 1, currency, amount))
|
||||
def try_round(amount, expected, self=self, cr=cr, currency=currency, float_repr=float_repr,
|
||||
log10=log10):
|
||||
digits = max(0,-int(log10(currency.rounding)))
|
||||
result = float_repr(self.round(cr, 1, currency, amount), precision_digits=digits)
|
||||
assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected)
|
||||
try_round(2.674,'2.67')
|
||||
try_round(2.675,'2.68') # in Python 2.7.2, round(2.675,2) gives 2.67
|
||||
try_round(-2.675,'-2.68') # in Python 2.7.2, round(2.675,2) gives 2.67
|
||||
try_round(0.001,'0.0')
|
||||
try_round(-0.001,'-0.0')
|
||||
try_round(0.0049,'0.0') # 0.0049 is closer to 0 than to 0.01, so should round down
|
||||
try_round(0.001,'0.00')
|
||||
try_round(-0.001,'-0.00')
|
||||
try_round(0.0049,'0.00') # 0.0049 is closer to 0 than to 0.01, so should round down
|
||||
try_round(0.005,'0.01') # the rule is to round half away from zero
|
||||
try_round(-0.005,'-0.01') # the rule is to round half away from zero
|
||||
|
||||
|
@ -195,16 +199,17 @@
|
|||
"Float precision tests: verify that float rounding methods are working correctly via tools"
|
||||
-
|
||||
!python {model: res.currency}: |
|
||||
from tools import float_compare, float_is_zero, float_round
|
||||
def try_round(amount, expected, precision_digits=3, float_round=float_round):
|
||||
result = str(float_round(amount, precision_digits=precision_digits))
|
||||
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),
|
||||
precision_digits=precision_digits)
|
||||
assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected)
|
||||
try_round(2.6745, '2.675')
|
||||
try_round(-2.6745, '-2.675')
|
||||
try_round(2.6744, '2.674')
|
||||
try_round(-2.6744, '-2.674')
|
||||
try_round(0.0004, '0.0')
|
||||
try_round(-0.0004, '-0.0')
|
||||
try_round(0.0004, '0.000')
|
||||
try_round(-0.0004, '-0.000')
|
||||
try_round(357.4555, '357.456')
|
||||
try_round(-357.4555, '-357.456')
|
||||
try_round(457.4554, '457.455')
|
||||
|
@ -212,16 +217,15 @@
|
|||
|
||||
# Extended float range test, inspired by Cloves Almeida's test on bug #882036.
|
||||
fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555]
|
||||
expecteds = ['.0', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
|
||||
expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
|
||||
precisions = [2, 2, 2, 2, 2, 2, 3, 4]
|
||||
# We can't go higher than 12 significant digits with a fractional part, so
|
||||
# that means max magnitude = 5 with precision 4 if sample values (x) go up to 10000
|
||||
# because that gives 5 + 4 + 4 = 13 significant digits.
|
||||
for magnitude in range(5):
|
||||
# Note: max precision for double floats is 53 bits of precision or
|
||||
# 17 significant decimal digits
|
||||
for magnitude in range(7):
|
||||
for i in xrange(len(fractions)):
|
||||
frac, exp, prec = fractions[i], expecteds[i], precisions[i]
|
||||
for sign in [-1,1]:
|
||||
for x in xrange(0,10000,34):
|
||||
for x in xrange(0,10000,97):
|
||||
n = x * 10**magnitude
|
||||
f = sign * (n + frac)
|
||||
f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp
|
||||
|
@ -252,15 +256,16 @@
|
|||
try_compare(657.4444, 657.445, -1)
|
||||
try_compare(-657.4444, -657.445, 1)
|
||||
|
||||
# Rounding to different precision_roundings
|
||||
def try_round(amount, expected, precision_rounding=None, float_round=float_round):
|
||||
result = str(float_round(amount, precision_rounding=precision_rounding))
|
||||
# Rounding to unusual rounding units (e.g. coin values)
|
||||
def try_round(amount, expected, precision_rounding=None, float_round=float_round, float_repr=float_repr):
|
||||
result = float_repr(float_round(amount, precision_rounding=precision_rounding),
|
||||
precision_digits=2)
|
||||
assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected)
|
||||
try_round(-457.4554, '-457.45', precision_rounding=0.05)
|
||||
try_round(457.444, '457.5', precision_rounding=0.5)
|
||||
try_round(457.3, '455.0', precision_rounding=5)
|
||||
try_round(457.5, '460.0', precision_rounding=5)
|
||||
try_round(457.1, '456.0', precision_rounding=3)
|
||||
try_round(457.444, '457.50', precision_rounding=0.5)
|
||||
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)
|
||||
|
||||
-
|
||||
"Float precision tests: verify that invalid parameters are forbidden"
|
||||
|
|
|
@ -45,7 +45,7 @@ import openerp
|
|||
import openerp.netsvc as netsvc
|
||||
import openerp.tools as tools
|
||||
from openerp.tools.translate import _
|
||||
from openerp.tools import float_round
|
||||
from openerp.tools import float_round, float_repr
|
||||
|
||||
def _symbol_set(symb):
|
||||
if symb == None or symb == False:
|
||||
|
@ -240,7 +240,8 @@ class float(_column):
|
|||
if self.digits_compute:
|
||||
self.digits = self.digits_compute(cr)
|
||||
precision, scale = self.digits
|
||||
self._symbol_set = ('%s', lambda x: str(float_round(__builtin__.float(x or 0.0), precision_digits=scale)))
|
||||
self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), precision_digits=scale),
|
||||
precision_digits=scale))
|
||||
|
||||
class date(_column):
|
||||
_type = 'date'
|
||||
|
@ -994,7 +995,8 @@ class function(_column):
|
|||
if self.digits_compute:
|
||||
self.digits = self.digits_compute(cr)
|
||||
precision, scale = self.digits
|
||||
self._symbol_set = ('%s', lambda x: str(float_round(__builtin__.float(x or 0.0), precision_digits=scale)))
|
||||
self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0), precision_digits=scale),
|
||||
precision_digits=scale))
|
||||
|
||||
def search(self, cr, uid, obj, name, args, context=None):
|
||||
if not self._fnct_search:
|
||||
|
|
|
@ -19,15 +19,7 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
import logging
|
||||
import math
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
# When a number crosses this threshold, significant decimal
|
||||
# digits may be lost when trying to render the float value, due to
|
||||
# Python's float implementation.
|
||||
# e.g. str(10060000.45556) == '10060000.4556' => lost 1 digit!
|
||||
SIGNIFICANT_DIGITS_SCALE_LIMIT = math.log(10**12, 2) # 10**12 ~= 2**39.86
|
||||
|
||||
def _float_check_precision(precision_digits=None, precision_rounding=None):
|
||||
assert (precision_digits is not None or precision_rounding is not None) and \
|
||||
|
@ -40,20 +32,12 @@ def _float_check_precision(precision_digits=None, precision_rounding=None):
|
|||
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.
|
||||
errors, and applying HALF-UP (away from zero) tie-breaking rule.
|
||||
Precision must be given by ``precision_digits`` or ``precision_rounding``,
|
||||
not both!
|
||||
|
||||
To illustrate how this is different from the default round() builtin,
|
||||
here is an example (depends on Python version, here is for v2.7.2 x64)::
|
||||
|
||||
>>> round_float(2.675)
|
||||
2.68
|
||||
>>> round(2.675,2)
|
||||
2.67
|
||||
|
||||
:param float value: the value to round
|
||||
:param int precision_digits: number of decimal digits to round to.
|
||||
:param int precision_digits: number of fractional 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).
|
||||
|
@ -62,58 +46,51 @@ def float_round(value, precision_digits=None, precision_rounding=None):
|
|||
rounding_factor = _float_check_precision(precision_digits=precision_digits,
|
||||
precision_rounding=precision_rounding)
|
||||
if rounding_factor == 0 or value == 0: return 0.0
|
||||
# scale up by rounding_factor, in order to implement rounding to arbitrary
|
||||
# `units` or 'rounding_factors'.
|
||||
# Example: if rounding_factor is 0.5, 1.3 should round to 1.5
|
||||
# So we'll do this: scaled_value = 1.3 / 0.5 = 2.6
|
||||
# int_rounded = round(2.6) = 3
|
||||
# result = 3 * 0.5 = 1.5
|
||||
# Also, .5 is a binary fraction, so this automatically solves some tricky
|
||||
# cases when rounding_factor is a negative power of 10. E.g 2.6745 is
|
||||
# difficult to round to 0.001 because it does not have an exact IEEE754
|
||||
# representation, but 2674.5 is simple to round to 2675 because both
|
||||
# are exactly represented.
|
||||
scaled_value = value / rounding_factor
|
||||
# Despite the advantage of rounding to .5 binary fractions, we still need
|
||||
# to add a small epsilon value to take care of cases where the float repr
|
||||
# is slightly too far below .5 to properly round *up* automatically.
|
||||
# That epsilon needs to be scaled according to the order of magnitude of
|
||||
# the value. (Credit: discussed with several community members on bug 882036)
|
||||
epsilon_scale = math.log(abs(scaled_value), 2)
|
||||
frac_part, _ = math.modf(scaled_value)
|
||||
if frac_part and epsilon_scale > SIGNIFICANT_DIGITS_SCALE_LIMIT:
|
||||
print 'Float rounding of %r to %r precision requires too many '\
|
||||
'significant digits, a loss of precision may occur in the '\
|
||||
'least significant digits' % (value,rounding_factor)
|
||||
logging.getLogger('float_utils')\
|
||||
.warning('Float rounding of %r to %r precision requires too many '
|
||||
'significant digits, a loss of precision may occur in the '
|
||||
'least significant digits', value, rounding_factor)
|
||||
epsilon = 2**(epsilon_scale-50)
|
||||
scaled_value += cmp(scaled_value,0) * epsilon
|
||||
rounded_value = round(scaled_value)
|
||||
result = rounded_value * rounding_factor
|
||||
|
||||
# NORMALIZE - ROUND - DENORMALIZE
|
||||
# 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
|
||||
# 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.
|
||||
# For example 2.675 == 2.6749999999999998.
|
||||
# To correct this, we add a very small epsilon value, scaled to the
|
||||
# the order of magnitude of the value, to tip the tie-break in the right
|
||||
# direction.
|
||||
# Credit: discussion with OpenERP community members on bug 882036
|
||||
|
||||
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
|
||||
result = rounded_value * rounding_factor # de-normalize
|
||||
return result
|
||||
|
||||
def float_is_zero(value, precision_digits=None, precision_rounding=None):
|
||||
"""Returns true if ``value`` is small enough to be treated as
|
||||
zero at the given precision (smaller than the given *epsilon*).
|
||||
zero at the given precision (smaller than the corresponding *epsilon*).
|
||||
The precision (``10**-precision_digits`` or ``precision_rounding``)
|
||||
is used as the zero *epsilon*: values less than that are considered
|
||||
to be zero.
|
||||
Precision must be given by ``precision_digits`` or ``precision_rounding``,
|
||||
not both! Here the precision (``10**-precision_digits`` or
|
||||
``precision_rounding``) is used as the zero *epsilon*: values smaller
|
||||
than that are considered to be zero.
|
||||
not both!
|
||||
|
||||
Warning: ``float_is_zero(value1-value2)`` is not always equivalent to
|
||||
Warning: ``float_is_zero(value1-value2)`` is not equivalent to
|
||||
``float_compare(value1,value2) == 0``, as the former will round after
|
||||
computing the difference, while the latter will round before, giving
|
||||
different results for e.g. 0.006 and 0.002 at 2 digits precision.
|
||||
|
||||
:param int precision_digits: number of decimal digits to round to.
|
||||
:param int precision_digits: number of fractional 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 value: value to compare with currency's zero
|
||||
:return: True if ``value`` is considered 0
|
||||
:param float value: value to compare with the precision's zero
|
||||
:return: True if ``value`` is considered zero
|
||||
"""
|
||||
epsilon = _float_check_precision(precision_digits=precision_digits,
|
||||
precision_rounding=precision_rounding)
|
||||
|
@ -129,16 +106,16 @@ def float_compare(value1, value2, precision_digits=None, precision_rounding=None
|
|||
|
||||
Example: 1.432 and 1.431 are equal at 2 digits precision,
|
||||
so this method would return 0
|
||||
However 0.006 and 0.002 are considered different (method returns 1) because
|
||||
they respectively round to 0.01 and 0.0, even though 0.006-0.002 = 0.004
|
||||
which would be considered zero at 2 digits precision.
|
||||
However 0.006 and 0.002 are considered different (this method returns 1)
|
||||
because they respectively round to 0.01 and 0.0, even though
|
||||
0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.
|
||||
|
||||
Warning: ``float_is_zero(value1-value2)`` is not always equivalent to
|
||||
Warning: ``float_is_zero(value1-value2)`` is not equivalent to
|
||||
``float_compare(value1,value2) == 0``, as the former will round after
|
||||
computing the difference, while the latter will round before, giving
|
||||
different results for e.g. 0.006 and 0.002 at 2 digits precision.
|
||||
|
||||
:param int precision_digits: number of decimal digits to round to.
|
||||
:param int precision_digits: number of fractional 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).
|
||||
|
@ -155,8 +132,20 @@ def float_compare(value1, value2, precision_digits=None, precision_rounding=None
|
|||
if float_is_zero(delta, precision_rounding=rounding_factor): return 0
|
||||
return -1 if delta < 0.0 else 1
|
||||
|
||||
def float_repr(value, precision_digits):
|
||||
"""Returns a string representation of a float with the
|
||||
the given number of fractional digits. This should not be
|
||||
used to perform a rounding operation (this is done via
|
||||
:meth:`~.float_round`), but only to produce a suitable
|
||||
string representation for a float.
|
||||
|
||||
|
||||
:param int precision_digits: number of fractional digits to
|
||||
include in the output
|
||||
"""
|
||||
# Can't use str() here because it seems to have an intrisic
|
||||
# rounding to 12 significant digits, which causes a loss of
|
||||
# precision. e.g. str(123456789.1234) == str(123456789.123)!!
|
||||
return ("%%.%sf" % precision_digits) % value
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -168,20 +157,21 @@ if __name__ == "__main__":
|
|||
|
||||
def try_round(amount, expected, precision_digits=3):
|
||||
global count, errors; count += 1
|
||||
result = float_round(amount, precision_digits=precision_digits)
|
||||
if str(result) != expected:
|
||||
result = float_repr(float_round(amount, precision_digits=precision_digits),
|
||||
precision_digits=precision_digits)
|
||||
if result != expected:
|
||||
errors += 1
|
||||
print '###!!! Rounding error: got %s or %s, expected %s' % (str(result), repr(result), expected)
|
||||
print '###!!! Rounding error: got %s , expected %s' % (result, expected)
|
||||
|
||||
# Extended float range test, inspired by Cloves Almeida's test on bug #882036.
|
||||
fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555]
|
||||
expecteds = ['.0', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
|
||||
expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
|
||||
precisions = [2, 2, 2, 2, 2, 2, 3, 4]
|
||||
for magnitude in range(5):
|
||||
for magnitude in range(7):
|
||||
for i in xrange(len(fractions)):
|
||||
frac, exp, prec = fractions[i], expecteds[i], precisions[i]
|
||||
for sign in [-1,1]:
|
||||
for x in xrange(0,10000,17):
|
||||
for x in xrange(0,10000,97):
|
||||
n = x * 10**magnitude
|
||||
f = sign * (n + frac)
|
||||
f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp
|
||||
|
|
Loading…
Reference in New Issue