1# -*- coding: utf-8 -*- 2# Part of Odoo. See LICENSE file for full copyright and licensing details. 3 4from __future__ import print_function 5import builtins 6import math 7 8 9def round(f): 10 # P3's builtin round differs from P2 in the following manner: 11 # * it rounds half to even rather than up (away from 0) 12 # * round(-0.) loses the sign (it returns -0 rather than 0) 13 # * round(x) returns an int rather than a float 14 # 15 # this compatibility shim implements Python 2's round in terms of 16 # Python 3's so that important rounding error under P3 can be 17 # trivially fixed, assuming the P2 behaviour to be debugged and 18 # correct. 19 roundf = builtins.round(f) 20 if builtins.round(f + 1) - roundf != 1: 21 return f + math.copysign(0.5, f) 22 # copysign ensures round(-0.) -> -0 *and* result is a float 23 return math.copysign(roundf, f) 24 25def _float_check_precision(precision_digits=None, precision_rounding=None): 26 assert (precision_digits is not None or precision_rounding is not None) and \ 27 not (precision_digits and precision_rounding),\ 28 "exactly one of precision_digits and precision_rounding must be specified" 29 assert precision_rounding is None or precision_rounding > 0,\ 30 "precision_rounding must be positive, got %s" % precision_rounding 31 if precision_digits is not None: 32 return 10 ** -precision_digits 33 return precision_rounding 34 35def float_round(value, precision_digits=None, precision_rounding=None, rounding_method='HALF-UP'): 36 """Return ``value`` rounded to ``precision_digits`` decimal digits, 37 minimizing IEEE-754 floating point representation errors, and applying 38 the tie-breaking rule selected with ``rounding_method``, by default 39 HALF-UP (away from zero). 40 Precision must be given by ``precision_digits`` or ``precision_rounding``, 41 not both! 42 43 :param float value: the value to round 44 :param int precision_digits: number of fractional digits to round to. 45 :param float precision_rounding: decimal number representing the minimum 46 non-zero value at the desired precision (for example, 0.01 for a 47 2-digit precision). 48 :param rounding_method: the rounding method used: 'HALF-UP', 'UP' or 'DOWN', 49 the first one rounding up to the closest number with the rule that 50 number>=0.5 is rounded up to 1, the second always rounding up and the 51 latest one always rounding down. 52 :return: rounded float 53 """ 54 rounding_factor = _float_check_precision(precision_digits=precision_digits, 55 precision_rounding=precision_rounding) 56 if rounding_factor == 0 or value == 0: 57 return 0.0 58 59 # NORMALIZE - ROUND - DENORMALIZE 60 # In order to easily support rounding to arbitrary 'steps' (e.g. coin values), 61 # we normalize the value before rounding it as an integer, and de-normalize 62 # after rounding: e.g. float_round(1.3, precision_rounding=.5) == 1.5 63 # Due to IEE754 float/double representation limits, the approximation of the 64 # real value may be slightly below the tie limit, resulting in an error of 65 # 1 unit in the last place (ulp) after rounding. 66 # For example 2.675 == 2.6749999999999998. 67 # To correct this, we add a very small epsilon value, scaled to the 68 # the order of magnitude of the value, to tip the tie-break in the right 69 # direction. 70 # Credit: discussion with OpenERP community members on bug 882036 71 72 normalized_value = value / rounding_factor # normalize 73 sign = math.copysign(1.0, normalized_value) 74 epsilon_magnitude = math.log(abs(normalized_value), 2) 75 epsilon = 2**(epsilon_magnitude-52) 76 77 # TIE-BREAKING: UP/DOWN (for ceiling[resp. flooring] operations) 78 # When rounding the value up[resp. down], we instead subtract[resp. add] the epsilon value 79 # as the approximation of the real value may be slightly *above* the 80 # tie limit, this would result in incorrectly rounding up[resp. down] to the next number 81 # The math.ceil[resp. math.floor] operation is applied on the absolute value in order to 82 # round "away from zero" and not "towards infinity", then the sign is 83 # restored. 84 85 if rounding_method == 'UP': 86 normalized_value -= sign*epsilon 87 rounded_value = math.ceil(abs(normalized_value)) * sign 88 89 elif rounding_method == 'DOWN': 90 normalized_value += sign*epsilon 91 rounded_value = math.floor(abs(normalized_value)) * sign 92 93 # TIE-BREAKING: HALF-UP (for normal rounding) 94 # We want to apply HALF-UP tie-breaking rules, i.e. 0.5 rounds away from 0. 95 else: 96 normalized_value += math.copysign(epsilon, normalized_value) 97 rounded_value = round(normalized_value) # round to integer 98 99 result = rounded_value * rounding_factor # de-normalize 100 return result 101 102def float_is_zero(value, precision_digits=None, precision_rounding=None): 103 """Returns true if ``value`` is small enough to be treated as 104 zero at the given precision (smaller than the corresponding *epsilon*). 105 The precision (``10**-precision_digits`` or ``precision_rounding``) 106 is used as the zero *epsilon*: values less than that are considered 107 to be zero. 108 Precision must be given by ``precision_digits`` or ``precision_rounding``, 109 not both! 110 111 Warning: ``float_is_zero(value1-value2)`` is not equivalent to 112 ``float_compare(value1,value2) == 0``, as the former will round after 113 computing the difference, while the latter will round before, giving 114 different results for e.g. 0.006 and 0.002 at 2 digits precision. 115 116 :param int precision_digits: number of fractional digits to round to. 117 :param float precision_rounding: decimal number representing the minimum 118 non-zero value at the desired precision (for example, 0.01 for a 119 2-digit precision). 120 :param float value: value to compare with the precision's zero 121 :return: True if ``value`` is considered zero 122 """ 123 epsilon = _float_check_precision(precision_digits=precision_digits, 124 precision_rounding=precision_rounding) 125 return abs(float_round(value, precision_rounding=epsilon)) < epsilon 126 127def float_compare(value1, value2, precision_digits=None, precision_rounding=None): 128 """Compare ``value1`` and ``value2`` after rounding them according to the 129 given precision. A value is considered lower/greater than another value 130 if their rounded value is different. This is not the same as having a 131 non-zero difference! 132 Precision must be given by ``precision_digits`` or ``precision_rounding``, 133 not both! 134 135 Example: 1.432 and 1.431 are equal at 2 digits precision, 136 so this method would return 0 137 However 0.006 and 0.002 are considered different (this method returns 1) 138 because they respectively round to 0.01 and 0.0, even though 139 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision. 140 141 Warning: ``float_is_zero(value1-value2)`` is not equivalent to 142 ``float_compare(value1,value2) == 0``, as the former will round after 143 computing the difference, while the latter will round before, giving 144 different results for e.g. 0.006 and 0.002 at 2 digits precision. 145 146 :param int precision_digits: number of fractional digits to round to. 147 :param float precision_rounding: decimal number representing the minimum 148 non-zero value at the desired precision (for example, 0.01 for a 149 2-digit precision). 150 :param float value1: first value to compare 151 :param float value2: second value to compare 152 :return: (resp.) -1, 0 or 1, if ``value1`` is (resp.) lower than, 153 equal to, or greater than ``value2``, at the given precision. 154 """ 155 rounding_factor = _float_check_precision(precision_digits=precision_digits, 156 precision_rounding=precision_rounding) 157 value1 = float_round(value1, precision_rounding=rounding_factor) 158 value2 = float_round(value2, precision_rounding=rounding_factor) 159 delta = value1 - value2 160 if float_is_zero(delta, precision_rounding=rounding_factor): return 0 161 return -1 if delta < 0.0 else 1 162 163def float_repr(value, precision_digits): 164 """Returns a string representation of a float with the 165 the given number of fractional digits. This should not be 166 used to perform a rounding operation (this is done via 167 :meth:`~.float_round`), but only to produce a suitable 168 string representation for a float. 169 170 :param int precision_digits: number of fractional digits to 171 include in the output 172 """ 173 # Can't use str() here because it seems to have an intrinsic 174 # rounding to 12 significant digits, which causes a loss of 175 # precision. e.g. str(123456789.1234) == str(123456789.123)!! 176 return ("%%.%sf" % precision_digits) % value 177 178_float_repr = float_repr 179 180def float_split_str(value, precision_digits): 181 """Splits the given float 'value' in its unitary and decimal parts, 182 returning each of them as a string, rounding the value using 183 the provided ``precision_digits`` argument. 184 185 The length of the string returned for decimal places will always 186 be equal to ``precision_digits``, adding zeros at the end if needed. 187 188 In case ``precision_digits`` is zero, an empty string is returned for 189 the decimal places. 190 191 Examples: 192 1.432 with precision 2 => ('1', '43') 193 1.49 with precision 1 => ('1', '5') 194 1.1 with precision 3 => ('1', '100') 195 1.12 with precision 0 => ('1', '') 196 197 :param float value: value to split. 198 :param int precision_digits: number of fractional digits to round to. 199 :return: returns the tuple(<unitary part>, <decimal part>) of the given value 200 :rtype: tuple(str, str) 201 """ 202 value = float_round(value, precision_digits=precision_digits) 203 value_repr = float_repr(value, precision_digits) 204 return tuple(value_repr.split('.')) if precision_digits else (value_repr, '') 205 206def float_split(value, precision_digits): 207 """ same as float_split_str() except that it returns the unitary and decimal 208 parts as integers instead of strings. In case ``precision_digits`` is zero, 209 0 is always returned as decimal part. 210 211 :rtype: tuple(int, int) 212 """ 213 units, cents = float_split_str(value, precision_digits) 214 if not cents: 215 return int(units), 0 216 return int(units), int(cents) 217 218 219if __name__ == "__main__": 220 221 import time 222 start = time.time() 223 count = 0 224 errors = 0 225 226 def try_round(amount, expected, precision_digits=3): 227 global count, errors; count += 1 228 result = float_repr(float_round(amount, precision_digits=precision_digits), 229 precision_digits=precision_digits) 230 if result != expected: 231 errors += 1 232 print('###!!! Rounding error: got %s , expected %s' % (result, expected)) 233 234 # Extended float range test, inspired by Cloves Almeida's test on bug #882036. 235 fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555] 236 expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556'] 237 precisions = [2, 2, 2, 2, 2, 2, 3, 4] 238 for magnitude in range(7): 239 for frac, exp, prec in zip(fractions, expecteds, precisions): 240 for sign in [-1,1]: 241 for x in range(0, 10000, 97): 242 n = x * 10**magnitude 243 f = sign * (n + frac) 244 f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp 245 try_round(f, f_exp, precision_digits=prec) 246 247 stop = time.time() 248 249 # Micro-bench results: 250 # 47130 round calls in 0.422306060791 secs, with Python 2.6.7 on Core i3 x64 251 # with decimal: 252 # 47130 round calls in 6.612248100021 secs, with Python 2.6.7 on Core i3 x64 253 print(count, " round calls, ", errors, "errors, done in ", (stop-start), 'secs') 254