gooderp18绿色标准版
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

341 line
16KB

  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. import builtins
  3. import math
  4. __all__ = [
  5. "float_compare",
  6. "float_is_zero",
  7. "float_repr",
  8. "float_round",
  9. "float_split",
  10. "float_split_str",
  11. ]
  12. def round(f):
  13. # P3's builtin round differs from P2 in the following manner:
  14. # * it rounds half to even rather than up (away from 0)
  15. # * round(-0.) loses the sign (it returns -0 rather than 0)
  16. # * round(x) returns an int rather than a float
  17. #
  18. # this compatibility shim implements Python 2's round in terms of
  19. # Python 3's so that important rounding error under P3 can be
  20. # trivially fixed, assuming the P2 behaviour to be debugged and
  21. # correct.
  22. roundf = builtins.round(f)
  23. if builtins.round(f + 1) - roundf != 1:
  24. return f + math.copysign(0.5, f)
  25. # copysign ensures round(-0.) -> -0 *and* result is a float
  26. return math.copysign(roundf, f)
  27. def _float_check_precision(precision_digits=None, precision_rounding=None):
  28. if precision_rounding is not None and precision_digits is None:
  29. assert precision_rounding > 0,\
  30. f"precision_rounding must be positive, got {precision_rounding}"
  31. elif precision_digits is not None and precision_rounding is None:
  32. # TODO: `int`s will also get the `is_integer` method starting from python 3.12
  33. assert float(precision_digits).is_integer() and precision_digits >= 0,\
  34. f"precision_digits must be a non-negative integer, got {precision_digits}"
  35. precision_rounding = 10 ** -precision_digits
  36. else:
  37. msg = "exactly one of precision_digits and precision_rounding must be specified"
  38. raise AssertionError(msg)
  39. return precision_rounding
  40. def float_round(value, precision_digits=None, precision_rounding=None, rounding_method='HALF-UP'):
  41. """Return ``value`` rounded to ``precision_digits`` decimal digits,
  42. minimizing IEEE-754 floating point representation errors, and applying
  43. the tie-breaking rule selected with ``rounding_method``, by default
  44. HALF-UP (away from zero).
  45. Precision must be given by ``precision_digits`` or ``precision_rounding``,
  46. not both!
  47. :param float value: the value to round
  48. :param int precision_digits: number of fractional digits to round to.
  49. :param float precision_rounding: decimal number representing the minimum
  50. non-zero value at the desired precision (for example, 0.01 for a
  51. 2-digit precision).
  52. :param rounding_method: the rounding method used:
  53. - 'HALF-UP' will round to the closest number with ties going away from zero.
  54. - 'HALF-DOWN' will round to the closest number with ties going towards zero.
  55. - 'HALF_EVEN' will round to the closest number with ties going to the closest
  56. even number.
  57. - 'UP' will always round away from 0.
  58. - 'DOWN' will always round towards 0.
  59. :return: rounded float
  60. """
  61. rounding_factor = _float_check_precision(precision_digits=precision_digits,
  62. precision_rounding=precision_rounding)
  63. if rounding_factor == 0 or value == 0:
  64. return 0.0
  65. # NORMALIZE - ROUND - DENORMALIZE
  66. # In order to easily support rounding to arbitrary 'steps' (e.g. coin values),
  67. # we normalize the value before rounding it as an integer, and de-normalize
  68. # after rounding: e.g. float_round(1.3, precision_rounding=.5) == 1.5
  69. def normalize(val):
  70. return val / rounding_factor
  71. def denormalize(val):
  72. return val * rounding_factor
  73. # inverting small rounding factors reduces rounding errors
  74. if rounding_factor < 1:
  75. rounding_factor = float_invert(rounding_factor)
  76. normalize, denormalize = denormalize, normalize
  77. normalized_value = normalize(value)
  78. # Due to IEEE-754 float/double representation limits, the approximation of the
  79. # real value may be slightly below the tie limit, resulting in an error of
  80. # 1 unit in the last place (ulp) after rounding.
  81. # For example 2.675 == 2.6749999999999998.
  82. # To correct this, we add a very small epsilon value, scaled to the
  83. # the order of magnitude of the value, to tip the tie-break in the right
  84. # direction.
  85. # Credit: discussion with OpenERP community members on bug 882036
  86. epsilon_magnitude = math.log2(abs(normalized_value))
  87. # `2**(epsilon_magnitude - 52)` would be the minimal size, but we increase it to be
  88. # more tolerant of inaccuracies accumulated after multiple floating point operations
  89. epsilon = 2**(epsilon_magnitude - 50)
  90. match rounding_method:
  91. case 'HALF-UP': # 0.5 rounds away from 0
  92. result = round(normalized_value + math.copysign(epsilon, normalized_value))
  93. case 'HALF-EVEN': # 0.5 rounds towards closest even number
  94. integral = math.floor(normalized_value)
  95. remainder = abs(normalized_value - integral)
  96. is_half = abs(0.5 - remainder) < epsilon
  97. # if is_half & integral is odd, add odd bit to make it even
  98. result = integral + (integral & 1) if is_half else round(normalized_value)
  99. case 'HALF-DOWN': # 0.5 rounds towards 0
  100. result = round(normalized_value - math.copysign(epsilon, normalized_value))
  101. case 'UP': # round to number furthest from zero
  102. result = math.trunc(normalized_value + math.copysign(1 - epsilon, normalized_value))
  103. case 'DOWN': # round to number closest to zero
  104. result = math.trunc(normalized_value + math.copysign(epsilon, normalized_value))
  105. case _:
  106. msg = f"unknown rounding method: {rounding_method}"
  107. raise ValueError(msg)
  108. return denormalize(result)
  109. def float_is_zero(value, precision_digits=None, precision_rounding=None):
  110. """Returns true if ``value`` is small enough to be treated as
  111. zero at the given precision (smaller than the corresponding *epsilon*).
  112. The precision (``10**-precision_digits`` or ``precision_rounding``)
  113. is used as the zero *epsilon*: values less than that are considered
  114. to be zero.
  115. Precision must be given by ``precision_digits`` or ``precision_rounding``,
  116. not both!
  117. Warning: ``float_is_zero(value1-value2)`` is not equivalent to
  118. ``float_compare(value1,value2) == 0``, as the former will round after
  119. computing the difference, while the latter will round before, giving
  120. different results for e.g. 0.006 and 0.002 at 2 digits precision.
  121. :param int precision_digits: number of fractional digits to round to.
  122. :param float precision_rounding: decimal number representing the minimum
  123. non-zero value at the desired precision (for example, 0.01 for a
  124. 2-digit precision).
  125. :param float value: value to compare with the precision's zero
  126. :return: True if ``value`` is considered zero
  127. """
  128. epsilon = _float_check_precision(precision_digits=precision_digits,
  129. precision_rounding=precision_rounding)
  130. return value == 0.0 or abs(float_round(value, precision_rounding=epsilon)) < epsilon
  131. def float_compare(value1, value2, precision_digits=None, precision_rounding=None):
  132. """Compare ``value1`` and ``value2`` after rounding them according to the
  133. given precision. A value is considered lower/greater than another value
  134. if their rounded value is different. This is not the same as having a
  135. non-zero difference!
  136. Precision must be given by ``precision_digits`` or ``precision_rounding``,
  137. not both!
  138. Example: 1.432 and 1.431 are equal at 2 digits precision,
  139. so this method would return 0
  140. However 0.006 and 0.002 are considered different (this method returns 1)
  141. because they respectively round to 0.01 and 0.0, even though
  142. 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.
  143. Warning: ``float_is_zero(value1-value2)`` is not equivalent to
  144. ``float_compare(value1,value2) == 0``, as the former will round after
  145. computing the difference, while the latter will round before, giving
  146. different results for e.g. 0.006 and 0.002 at 2 digits precision.
  147. :param float value1: first value to compare
  148. :param float value2: second value to compare
  149. :param int precision_digits: number of fractional digits to round to.
  150. :param float precision_rounding: decimal number representing the minimum
  151. non-zero value at the desired precision (for example, 0.01 for a
  152. 2-digit precision).
  153. :return: (resp.) -1, 0 or 1, if ``value1`` is (resp.) lower than,
  154. equal to, or greater than ``value2``, at the given precision.
  155. """
  156. rounding_factor = _float_check_precision(precision_digits=precision_digits,
  157. precision_rounding=precision_rounding)
  158. # equal numbers round equally, so we can skip that step
  159. # doing this after _float_check_precision to validate parameters first
  160. if value1 == value2:
  161. return 0
  162. value1 = float_round(value1, precision_rounding=rounding_factor)
  163. value2 = float_round(value2, precision_rounding=rounding_factor)
  164. delta = value1 - value2
  165. if float_is_zero(delta, precision_rounding=rounding_factor):
  166. return 0
  167. return -1 if delta < 0.0 else 1
  168. def float_repr(value, precision_digits):
  169. """Returns a string representation of a float with the
  170. given number of fractional digits. This should not be
  171. used to perform a rounding operation (this is done via
  172. :func:`~.float_round`), but only to produce a suitable
  173. string representation for a float.
  174. :param float value:
  175. :param int precision_digits: number of fractional digits to include in the output
  176. """
  177. # Can't use str() here because it seems to have an intrinsic
  178. # rounding to 12 significant digits, which causes a loss of
  179. # precision. e.g. str(123456789.1234) == str(123456789.123)!!
  180. return "%.*f" % (precision_digits, value)
  181. def float_split_str(value, precision_digits):
  182. """Splits the given float 'value' in its unitary and decimal parts,
  183. returning each of them as a string, rounding the value using
  184. the provided ``precision_digits`` argument.
  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. In case ``precision_digits`` is zero, an empty string is returned for
  188. the decimal places.
  189. Examples:
  190. 1.432 with precision 2 => ('1', '43')
  191. 1.49 with precision 1 => ('1', '5')
  192. 1.1 with precision 3 => ('1', '100')
  193. 1.12 with precision 0 => ('1', '')
  194. :param float value: value to split.
  195. :param int precision_digits: number of fractional digits to round to.
  196. :return: returns the tuple(<unitary part>, <decimal part>) of the given value
  197. :rtype: tuple(str, str)
  198. """
  199. value = float_round(value, precision_digits=precision_digits)
  200. value_repr = float_repr(value, precision_digits)
  201. return tuple(value_repr.split('.')) if precision_digits else (value_repr, '')
  202. def float_split(value, precision_digits):
  203. """ same as float_split_str() except that it returns the unitary and decimal
  204. parts as integers instead of strings. In case ``precision_digits`` is zero,
  205. 0 is always returned as decimal part.
  206. :rtype: tuple(int, int)
  207. """
  208. units, cents = float_split_str(value, precision_digits)
  209. if not cents:
  210. return int(units), 0
  211. return int(units), int(cents)
  212. def json_float_round(value, precision_digits, rounding_method='HALF-UP'):
  213. """Not suitable for float calculations! Similar to float_repr except that it
  214. returns a float suitable for json dump
  215. This may be necessary to produce "exact" representations of rounded float
  216. values during serialization, such as what is done by `json.dumps()`.
  217. Unfortunately `json.dumps` does not allow any form of custom float representation,
  218. nor any custom types, everything is serialized from the basic JSON types.
  219. :param int precision_digits: number of fractional digits to round to.
  220. :param rounding_method: the rounding method used: 'HALF-UP', 'UP' or 'DOWN',
  221. the first one rounding up to the closest number with the rule that
  222. number>=0.5 is rounded up to 1, the second always rounding up and the
  223. latest one always rounding down.
  224. :return: a rounded float value that must not be used for calculations, but
  225. is ready to be serialized in JSON with minimal chances of
  226. representation errors.
  227. """
  228. rounded_value = float_round(value, precision_digits=precision_digits, rounding_method=rounding_method)
  229. rounded_repr = float_repr(rounded_value, precision_digits=precision_digits)
  230. # As of Python 3.1, rounded_repr should be the shortest representation for our
  231. # rounded float, so we create a new float whose repr is expected
  232. # to be the same value, or a value that is semantically identical
  233. # and will be used in the json serialization.
  234. # e.g. if rounded_repr is '3.1750', the new float repr could be 3.175
  235. # but not 3.174999999999322452.
  236. # Cfr. bpo-1580: https://bugs.python.org/issue1580
  237. return float(rounded_repr)
  238. _INVERTDICT = {
  239. 1e-1: 1e+1, 1e-2: 1e+2, 1e-3: 1e+3, 1e-4: 1e+4, 1e-5: 1e+5,
  240. 1e-6: 1e+6, 1e-7: 1e+7, 1e-8: 1e+8, 1e-9: 1e+9, 1e-10: 1e+10,
  241. 2e-1: 5e+0, 2e-2: 5e+1, 2e-3: 5e+2, 2e-4: 5e+3, 2e-5: 5e+4,
  242. 2e-6: 5e+5, 2e-7: 5e+6, 2e-8: 5e+7, 2e-9: 5e+8, 2e-10: 5e+9,
  243. 5e-1: 2e+0, 5e-2: 2e+1, 5e-3: 2e+2, 5e-4: 2e+3, 5e-5: 2e+4,
  244. 5e-6: 2e+5, 5e-7: 2e+6, 5e-8: 2e+7, 5e-9: 2e+8, 5e-10: 2e+9,
  245. }
  246. def float_invert(value):
  247. """Inverts a floating point number with increased accuracy.
  248. :param float value: value to invert.
  249. :param bool store: whether store the result in memory for future calls.
  250. :return: rounded float.
  251. """
  252. result = _INVERTDICT.get(value)
  253. if result is None:
  254. coefficient, exponent = f'{value:.15e}'.split('e')
  255. # invert exponent by changing sign, and coefficient by dividing by its square
  256. result = float(f'{coefficient}e{-int(exponent)}') / float(coefficient)**2
  257. return result
  258. if __name__ == "__main__":
  259. import time
  260. start = time.time()
  261. count = 0
  262. def try_round(amount, expected, precision_digits=3):
  263. result = float_repr(float_round(amount, precision_digits=precision_digits),
  264. precision_digits=precision_digits)
  265. if result != expected:
  266. print('###!!! Rounding error: got %s , expected %s' % (result, expected))
  267. return complex(1, 1)
  268. return 1
  269. # Extended float range test, inspired by Cloves Almeida's test on bug #882036.
  270. fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555]
  271. expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
  272. precisions = [2, 2, 2, 2, 2, 2, 3, 4]
  273. for magnitude in range(7):
  274. for frac, exp, prec in zip(fractions, expecteds, precisions):
  275. for sign in [-1, 1]:
  276. for x in range(0, 10000, 97):
  277. n = x * 10**magnitude
  278. f = sign * (n + frac)
  279. f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp
  280. count += try_round(f, f_exp, precision_digits=prec)
  281. stop = time.time()
  282. count, errors = int(count.real), int(count.imag)
  283. # Micro-bench results:
  284. # 47130 round calls in 0.422306060791 secs, with Python 2.6.7 on Core i3 x64
  285. # with decimal:
  286. # 47130 round calls in 6.612248100021 secs, with Python 2.6.7 on Core i3 x64
  287. print(count, " round calls, ", errors, "errors, done in ", (stop-start), 'secs')
上海开阖软件有限公司 沪ICP备12045867号-1