Created
February 21, 2020 02:13
-
-
Save christophercrouzet/daf745a564e66b0541d6c5ebac8ade2d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
import math | |
_DIGIT_CHAR_OFFSET = ord('0') | |
_SIGN_TABLE = ('-', '') | |
def _round_frac(frac, digits): | |
if len(frac) <= digits or frac[digits] < '5': | |
return (frac[:digits], 0) | |
result = [] | |
carry = 1 | |
for x in reversed(frac[:digits]): | |
x = chr( | |
(ord(x) - _DIGIT_CHAR_OFFSET + carry) % 10 + _DIGIT_CHAR_OFFSET) | |
carry = int(x == '0' and carry > 0) | |
result.append(x) | |
return (''.join(reversed(result)), carry) | |
def _extract_decimal_digits(value, digits): | |
# The number 52 represents the amount of significant decimal digits | |
# printed out when using the module `decimal`. It's somehow matching | |
# the 52 bits used to store the mantissa of a double precision | |
# floating-point but these shouldn't be related. | |
# What really matters is that we do provide a high enough number so that | |
# the `format` function doesn't round the value. | |
return '{:.52f}'.format(value).split('.')[-1][:digits] | |
def format_number(value, digits): | |
sign = int(value >= 0) * 2 - 1 | |
frac, trunc = math.modf(value) | |
trunc = abs(int(trunc)) | |
frac = _extract_decimal_digits(frac, digits + 1) | |
frac, carry = _round_frac(frac, digits) | |
trunc += carry | |
return ('{}{}.{}'.format(_SIGN_TABLE[int(sign * 0.5 + 1)], trunc, frac) | |
.rstrip('0') | |
.rstrip('.')) | |
# ------------------------------------------------------------------------------ | |
def test(value, digits, expected=None): | |
rounded_default = ('{{:.{}f}}'.format(digits).format(value) | |
.rstrip('0') | |
.rstrip('.')) | |
rounded_custom = format_number(value, digits) | |
if expected is None: | |
assert rounded_default == rounded_custom | |
else: | |
print("{:+.16f} -> {}".format(value, rounded_custom)) | |
assert rounded_default == rounded_custom == expected | |
def run_custom_tests(): | |
print("# testing rounding to 3 digits") | |
print('-' * 80) | |
test(0, 3, '0') | |
test(1, 3, '1') | |
test(1.5, 3, '1.5') | |
test(-0, 3, '0') | |
test(-1, 3, '-1') | |
test(-1.5, 3, '-1.5') | |
print('-' * 80) | |
test(1.001, 3, '1.001') | |
test(1.099, 3, '1.099') | |
test(1.999, 3, '1.999') | |
print('-' * 80) | |
test(1.0001, 3, '1') | |
test(1.0099, 3, '1.01') | |
test(1.9999, 3, '2') | |
print('-' * 80) | |
test(-1.001, 3, '-1.001') | |
test(-1.099, 3, '-1.099') | |
test(-1.999, 3, '-1.999') | |
print('-' * 80) | |
test(-1.0001, 3, '-1') | |
test(-1.0099, 3, '-1.01') | |
test(-1.9999, 3, '-2') | |
print('-' * 80) | |
test(1.2345e-1, 3, '0.123') | |
test(-1.2345e-1, 3, '-0.123') | |
def run_random_tests(): | |
import random | |
random.seed(0) | |
for _ in range(1000): | |
test(random.uniform(-1e-9, 1e9), 3) | |
def main(): | |
run_custom_tests() | |
run_random_tests() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment