2011-12-16 16:04:26 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
|
|
#
|
|
|
|
# OpenERP, Open Source Business Applications
|
|
|
|
# Copyright (c) 2011 OpenERP S.A. <http://openerp.com>
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
##############################################################################
|
|
|
|
|
2011-12-20 16:34:20 +00:00
|
|
|
import math
|
|
|
|
|
2011-12-16 16:04:26 +00:00
|
|
|
def _float_check_precision(precision_digits=None, precision_rounding=None):
|
|
|
|
assert (precision_digits is not None or precision_rounding is not None) and \
|
|
|
|
not (precision_digits and precision_rounding),\
|
|
|
|
"exactly one of precision_digits and precision_rounding must be specified"
|
|
|
|
if precision_digits is not None:
|
|
|
|
return 10 ** -precision_digits
|
|
|
|
return precision_rounding
|
|
|
|
|
2014-10-08 14:10:52 +00:00
|
|
|
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).
|
2011-12-16 16:04:26 +00:00
|
|
|
Precision must be given by ``precision_digits`` or ``precision_rounding``,
|
|
|
|
not both!
|
|
|
|
|
|
|
|
:param float value: the value to round
|
2011-12-20 23:47:40 +00:00
|
|
|
:param int precision_digits: number of fractional digits to round to.
|
2011-12-16 16:04:26 +00:00
|
|
|
: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).
|
2014-10-08 14:10:52 +00:00
|
|
|
: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.
|
2011-12-16 16:04:26 +00:00
|
|
|
:return: rounded float
|
|
|
|
"""
|
|
|
|
rounding_factor = _float_check_precision(precision_digits=precision_digits,
|
|
|
|
precision_rounding=precision_rounding)
|
2011-12-20 16:34:20 +00:00
|
|
|
if rounding_factor == 0 or value == 0: return 0.0
|
2011-12-20 23:47:40 +00:00
|
|
|
|
|
|
|
# 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
|
|
|
|
|
2014-10-08 14:10:52 +00:00
|
|
|
# TIE-BREAKING: HALF-UP (for normal rounding)
|
2011-12-20 23:47:40 +00:00
|
|
|
# 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)
|
2014-10-08 14:10:52 +00:00
|
|
|
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
|
|
|
|
# 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
|
|
|
|
|
2011-12-20 23:47:40 +00:00
|
|
|
result = rounded_value * rounding_factor # de-normalize
|
2011-12-20 16:34:20 +00:00
|
|
|
return result
|
2011-12-16 16:04:26 +00:00
|
|
|
|
|
|
|
def float_is_zero(value, precision_digits=None, precision_rounding=None):
|
|
|
|
"""Returns true if ``value`` is small enough to be treated as
|
2011-12-20 23:47:40 +00:00
|
|
|
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.
|
2011-12-16 16:04:26 +00:00
|
|
|
Precision must be given by ``precision_digits`` or ``precision_rounding``,
|
2011-12-20 23:47:40 +00:00
|
|
|
not both!
|
2011-12-16 16:04:26 +00:00
|
|
|
|
2011-12-20 23:47:40 +00:00
|
|
|
Warning: ``float_is_zero(value1-value2)`` is not equivalent to
|
2011-12-16 16:04:26 +00:00
|
|
|
``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.
|
|
|
|
|
2011-12-20 23:47:40 +00:00
|
|
|
:param int precision_digits: number of fractional digits to round to.
|
2011-12-16 16:04:26 +00:00
|
|
|
: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).
|
2011-12-20 23:47:40 +00:00
|
|
|
:param float value: value to compare with the precision's zero
|
|
|
|
:return: True if ``value`` is considered zero
|
2011-12-16 16:04:26 +00:00
|
|
|
"""
|
2011-12-19 10:57:18 +00:00
|
|
|
epsilon = _float_check_precision(precision_digits=precision_digits,
|
2011-12-16 16:04:26 +00:00
|
|
|
precision_rounding=precision_rounding)
|
2011-12-19 10:57:18 +00:00
|
|
|
return abs(float_round(value, precision_rounding=epsilon)) < epsilon
|
2011-12-16 16:04:26 +00:00
|
|
|
|
|
|
|
def float_compare(value1, value2, precision_digits=None, precision_rounding=None):
|
|
|
|
"""Compare ``value1`` and ``value2`` after rounding them according to the
|
|
|
|
given precision. A value is considered lower/greater than another value
|
|
|
|
if their rounded value is different. This is not the same as having a
|
|
|
|
non-zero difference!
|
2011-12-19 10:57:18 +00:00
|
|
|
Precision must be given by ``precision_digits`` or ``precision_rounding``,
|
|
|
|
not both!
|
2011-12-16 16:04:26 +00:00
|
|
|
|
2011-12-19 10:57:18 +00:00
|
|
|
Example: 1.432 and 1.431 are equal at 2 digits precision,
|
2011-12-16 16:04:26 +00:00
|
|
|
so this method would return 0
|
2011-12-20 23:47:40 +00:00
|
|
|
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.
|
2011-12-16 16:04:26 +00:00
|
|
|
|
2011-12-20 23:47:40 +00:00
|
|
|
Warning: ``float_is_zero(value1-value2)`` is not equivalent to
|
2011-12-19 10:57:18 +00:00
|
|
|
``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.
|
2011-12-16 16:04:26 +00:00
|
|
|
|
2011-12-20 23:47:40 +00:00
|
|
|
:param int precision_digits: number of fractional digits to round to.
|
2011-12-16 16:04:26 +00:00
|
|
|
: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 value1: first value to compare
|
|
|
|
:param float value2: second value to compare
|
|
|
|
:return: (resp.) -1, 0 or 1, if ``value1`` is (resp.) lower than,
|
|
|
|
equal to, or greater than ``value2``, at the given precision.
|
|
|
|
"""
|
|
|
|
rounding_factor = _float_check_precision(precision_digits=precision_digits,
|
|
|
|
precision_rounding=precision_rounding)
|
|
|
|
value1 = float_round(value1, precision_rounding=rounding_factor)
|
|
|
|
value2 = float_round(value2, precision_rounding=rounding_factor)
|
|
|
|
delta = value1 - value2
|
|
|
|
if float_is_zero(delta, precision_rounding=rounding_factor): return 0
|
2011-12-20 16:34:20 +00:00
|
|
|
return -1 if delta < 0.0 else 1
|
|
|
|
|
2011-12-20 23:47:40 +00:00
|
|
|
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.
|
2011-12-20 16:34:20 +00:00
|
|
|
|
2011-12-20 23:47:40 +00:00
|
|
|
: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
|
2011-12-20 16:34:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
|
|
import time
|
|
|
|
start = time.time()
|
|
|
|
count = 0
|
|
|
|
errors = 0
|
|
|
|
|
|
|
|
def try_round(amount, expected, precision_digits=3):
|
|
|
|
global count, errors; count += 1
|
2011-12-20 23:47:40 +00:00
|
|
|
result = float_repr(float_round(amount, precision_digits=precision_digits),
|
|
|
|
precision_digits=precision_digits)
|
|
|
|
if result != expected:
|
2011-12-20 16:34:20 +00:00
|
|
|
errors += 1
|
2011-12-20 23:47:40 +00:00
|
|
|
print '###!!! Rounding error: got %s , expected %s' % (result, expected)
|
2011-12-20 16:34:20 +00:00
|
|
|
|
|
|
|
# Extended float range test, inspired by Cloves Almeida's test on bug #882036.
|
|
|
|
fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555]
|
2011-12-20 23:47:40 +00:00
|
|
|
expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
|
2011-12-20 16:34:20 +00:00
|
|
|
precisions = [2, 2, 2, 2, 2, 2, 3, 4]
|
2011-12-20 23:47:40 +00:00
|
|
|
for magnitude in range(7):
|
2011-12-20 16:34:20 +00:00
|
|
|
for i in xrange(len(fractions)):
|
|
|
|
frac, exp, prec = fractions[i], expecteds[i], precisions[i]
|
|
|
|
for sign in [-1,1]:
|
2011-12-20 23:47:40 +00:00
|
|
|
for x in xrange(0,10000,97):
|
2011-12-20 16:34:20 +00:00
|
|
|
n = x * 10**magnitude
|
|
|
|
f = sign * (n + frac)
|
|
|
|
f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp
|
|
|
|
try_round(f, f_exp, precision_digits=prec)
|
|
|
|
|
|
|
|
stop = time.time()
|
|
|
|
|
|
|
|
# Micro-bench results:
|
|
|
|
# 47130 round calls in 0.422306060791 secs, with Python 2.6.7 on Core i3 x64
|
|
|
|
# with decimal:
|
|
|
|
# 47130 round calls in 6.612248100021 secs, with Python 2.6.7 on Core i3 x64
|
|
|
|
print count, " round calls, ", errors, "errors, done in ", (stop-start), 'secs'
|