Skip to content

Instantly share code, notes, and snippets.

@christophercrouzet
Created February 21, 2020 02:13
Show Gist options
  • Save christophercrouzet/daf745a564e66b0541d6c5ebac8ade2d to your computer and use it in GitHub Desktop.
Save christophercrouzet/daf745a564e66b0541d6c5ebac8ade2d to your computer and use it in GitHub Desktop.
#!/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