179 lines
6.6 KiB
Python
179 lines
6.6 KiB
Python
import decimal
|
|
import numbers
|
|
import itertools
|
|
import re
|
|
|
|
__all__ = [
|
|
'TRUNCATE',
|
|
'ROUND',
|
|
'ROUND_UP',
|
|
'ROUND_DOWN',
|
|
'DECIMAL_PLACES',
|
|
'SIGNIFICANT_DIGITS',
|
|
'TICK_SIZE',
|
|
'NO_PADDING',
|
|
'PAD_WITH_ZERO',
|
|
'decimal_to_precision',
|
|
]
|
|
|
|
|
|
# rounding mode
|
|
TRUNCATE = 0
|
|
ROUND = 1
|
|
ROUND_UP = 2
|
|
ROUND_DOWN = 3
|
|
|
|
# digits counting mode
|
|
DECIMAL_PLACES = 2
|
|
SIGNIFICANT_DIGITS = 3
|
|
TICK_SIZE = 4
|
|
|
|
# padding mode
|
|
NO_PADDING = 5
|
|
PAD_WITH_ZERO = 6
|
|
|
|
|
|
def decimal_to_precision(n, rounding_mode=ROUND, precision=None, counting_mode=DECIMAL_PLACES, padding_mode=NO_PADDING):
|
|
assert precision is not None, 'precision should not be None'
|
|
|
|
if isinstance(precision, str):
|
|
precision = float(precision)
|
|
assert isinstance(precision, float) or isinstance(precision, decimal.Decimal) or isinstance(precision, numbers.Integral), 'precision has an invalid number'
|
|
|
|
if counting_mode == TICK_SIZE:
|
|
assert precision > 0, 'negative or zero precision can not be used with TICK_SIZE precisionMode'
|
|
else:
|
|
assert isinstance(precision, numbers.Integral)
|
|
|
|
assert rounding_mode in [TRUNCATE, ROUND]
|
|
assert counting_mode in [DECIMAL_PLACES, SIGNIFICANT_DIGITS, TICK_SIZE]
|
|
assert padding_mode in [NO_PADDING, PAD_WITH_ZERO]
|
|
# end of checks
|
|
|
|
context = decimal.getcontext()
|
|
|
|
if counting_mode != TICK_SIZE:
|
|
precision = min(context.prec - 2, precision)
|
|
|
|
# all default except decimal.Underflow (raised when a number is rounded to zero)
|
|
context.traps[decimal.Underflow] = True
|
|
context.rounding = decimal.ROUND_HALF_UP # rounds 0.5 away from zero
|
|
|
|
dec = decimal.Decimal(str(n))
|
|
precision_dec = decimal.Decimal(str(precision))
|
|
string = '{:f}'.format(dec) # convert to string using .format to avoid engineering notation
|
|
precise = None
|
|
|
|
def power_of_10(x):
|
|
return decimal.Decimal('10') ** (-x)
|
|
|
|
if precision < 0:
|
|
if counting_mode == TICK_SIZE:
|
|
raise ValueError('TICK_SIZE cant be used with negative numPrecisionDigits')
|
|
to_nearest = power_of_10(precision)
|
|
if rounding_mode == ROUND:
|
|
return "{:f}".format(to_nearest * decimal.Decimal(decimal_to_precision(dec / to_nearest, rounding_mode, 0, DECIMAL_PLACES, padding_mode)))
|
|
elif rounding_mode == TRUNCATE:
|
|
return decimal_to_precision(dec - dec % to_nearest, rounding_mode, 0, DECIMAL_PLACES, padding_mode)
|
|
|
|
if counting_mode == TICK_SIZE:
|
|
# python modulo with negative numbers behaves different than js/php, so use abs first
|
|
missing = abs(dec) % precision_dec
|
|
if missing != 0:
|
|
if rounding_mode == ROUND:
|
|
if dec > 0:
|
|
if missing >= precision_dec / 2:
|
|
dec = dec - missing + precision_dec
|
|
else:
|
|
dec = dec - missing
|
|
else:
|
|
if missing >= precision_dec / 2:
|
|
dec = dec + missing - precision_dec
|
|
else:
|
|
dec = dec + missing
|
|
elif rounding_mode == TRUNCATE:
|
|
if dec < 0:
|
|
dec = dec + missing
|
|
else:
|
|
dec = dec - missing
|
|
parts = re.sub(r'0+$', '', '{:f}'.format(precision_dec)).split('.')
|
|
if len(parts) > 1:
|
|
new_precision = len(parts[1])
|
|
else:
|
|
match = re.search(r'0+$', parts[0])
|
|
if match is None:
|
|
new_precision = 0
|
|
else:
|
|
new_precision = - len(match.group(0))
|
|
return decimal_to_precision('{:f}'.format(dec), ROUND, new_precision, DECIMAL_PLACES, padding_mode)
|
|
|
|
if rounding_mode == ROUND:
|
|
if counting_mode == DECIMAL_PLACES:
|
|
precise = '{:f}'.format(dec.quantize(power_of_10(precision))) # ROUND_HALF_EVEN is default context
|
|
elif counting_mode == SIGNIFICANT_DIGITS:
|
|
q = precision - dec.adjusted() - 1
|
|
sigfig = power_of_10(q)
|
|
if q < 0:
|
|
string_to_precision = string[:precision]
|
|
# string_to_precision is '' when we have zero precision
|
|
below = sigfig * decimal.Decimal(string_to_precision if string_to_precision else '0')
|
|
above = below + sigfig
|
|
precise = '{:f}'.format(min((below, above), key=lambda x: abs(x - dec)))
|
|
else:
|
|
precise = '{:f}'.format(dec.quantize(sigfig))
|
|
if precise.startswith('-0') and all(c in '0.' for c in precise[1:]):
|
|
precise = precise[1:]
|
|
|
|
elif rounding_mode == TRUNCATE:
|
|
# Slice a string
|
|
if counting_mode == DECIMAL_PLACES:
|
|
before, after = string.split('.') if '.' in string else (string, '')
|
|
precise = before + '.' + after[:precision]
|
|
elif counting_mode == SIGNIFICANT_DIGITS:
|
|
if precision == 0:
|
|
return '0'
|
|
dot = string.index('.') if '.' in string else len(string)
|
|
start = dot - dec.adjusted()
|
|
end = start + precision
|
|
# need to clarify these conditionals
|
|
if dot >= end:
|
|
end -= 1
|
|
if precision >= len(string.replace('.', '')):
|
|
precise = string
|
|
else:
|
|
precise = string[:end].ljust(dot, '0')
|
|
if precise.startswith('-0') and all(c in '0.' for c in precise[1:]):
|
|
precise = precise[1:]
|
|
precise = precise.rstrip('.')
|
|
|
|
if padding_mode == NO_PADDING:
|
|
return precise.rstrip('0').rstrip('.') if '.' in precise else precise
|
|
elif padding_mode == PAD_WITH_ZERO:
|
|
if '.' in precise:
|
|
if counting_mode == DECIMAL_PLACES:
|
|
before, after = precise.split('.')
|
|
return before + '.' + after.ljust(precision, '0')
|
|
|
|
elif counting_mode == SIGNIFICANT_DIGITS:
|
|
fsfg = len(list(itertools.takewhile(lambda x: x == '.' or x == '0', precise)))
|
|
if '.' in precise[fsfg:]:
|
|
precision += 1
|
|
return precise[:fsfg] + precise[fsfg:].rstrip('0').ljust(precision, '0')
|
|
else:
|
|
if counting_mode == SIGNIFICANT_DIGITS:
|
|
if precision > len(precise):
|
|
return precise + '.' + (precision - len(precise)) * '0'
|
|
elif counting_mode == DECIMAL_PLACES:
|
|
if precision > 0:
|
|
return precise + '.' + precision * '0'
|
|
return precise
|
|
|
|
|
|
def number_to_string(x):
|
|
# avoids scientific notation for too large and too small numbers
|
|
if x is None:
|
|
return None
|
|
d = decimal.Decimal(str(x))
|
|
formatted = '{:f}'.format(d)
|
|
return formatted.rstrip('0').rstrip('.') if '.' in formatted else formatted
|