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