diff --git a/mpmath/libmp/libmpf.py b/mpmath/libmp/libmpf.py index 9054b6a3a..317203ef1 100644 --- a/mpmath/libmp/libmpf.py +++ b/mpmath/libmp/libmpf.py @@ -1102,28 +1102,39 @@ def to_digits_exp(s, dps, base=10): exponent += len(digits) - fixdps - 1 return sign, digits, exponent -def round_digits(digits, dps, base): +def round_digits(digits, dps, base, rounding=round_nearest): ''' Returns the rounded digits, and the number of places the decimal point was shifted. + + Supports three kinds of rounding: up, down, or nearest. ''' assert len(digits) > dps + assert rounding in (round_nearest, round_up, round_down) + exponent = 0 - rnd_digs = stddigits[(base//2 + base % 2):base] + if rounding == round_down: + return digits[:dps], 0 + elif rounding == round_nearest: + rnd_digs = stddigits[(base//2 + base % 2):base] + else: + rnd_digs = stddigits[:base] + + tie_up = False - round_up = False # The first digit after dps is a 5. if digits[dps] == rnd_digs[0]: for i in range(dps+1, len(digits)): if digits[i] != '0': - round_up = True + tie_up = True break if digits[dps-1] in stddigits[1:base:2]: - round_up = True + tie_up = True + + if not tie_up: - if not round_up: digits = digits[:dps] + stddigits[int(digits[dps], base) - 1] # Rounding up kills some instances of "...99999" @@ -1376,9 +1387,16 @@ def from_str(x, prec=0, rnd=round_fast, base=0): (?P0|[1-9][0-9]*)? (?P[,_])? (?:\.(?P0|[1-9][0-9]*))? + (?P[UDZN])? (?P[eEfFgG]) """, re.DOTALL | re.VERBOSE).fullmatch +_GMPY_ROUND_CHAR_DICT = { + 'U': round_ceiling, + 'D': round_floor, + 'Z': round_down, + 'N': round_nearest + } def calc_padding(nchars, width, align): ''' @@ -1416,6 +1434,7 @@ def read_format_spec(format_spec): 'thousands_separators': '', 'width': -1, 'precision': 6, + 'rounding': round_nearest, 'type': 'f' } @@ -1430,8 +1449,12 @@ def read_format_spec(format_spec): or format_dict['thousands_separators'] format_dict['width'] = int(match['width'] or format_dict['width']) format_dict['precision'] = int(match['precision'] or format_dict['precision']) + rounding_char = match['rounding'] format_dict['type'] = match['type'] or format_dict['type'] + if rounding_char is not None: + format_dict['rounding'] = _GMPY_ROUND_CHAR_DICT[rounding_char] + if match['zeropad'] and match['fill_char']: raise ValueError('Cannot specify both 0-padding and a fill ' 'character') @@ -1454,7 +1477,8 @@ def format_fixed(s, sep_range=3, base=10, alternate=False, - no_neg_0=False): + no_neg_0=False, + rounding=round_nearest): ''' Format a number into fixed point. Returns the sign character, and the string that represents the number in @@ -1491,7 +1515,7 @@ def format_fixed(s, if no_neg_0: sign = '' if sign_spec == '-' else sign_spec else: - digits, exp_add = round_digits(digits, dps, base) + digits, exp_add = round_digits(digits, dps, base, rounding) exponent += exp_add # Here we prepend the corresponding 0s to the digits string, according @@ -1544,7 +1568,8 @@ def format_scientific(s, base=10, capitalize=False, alternate=False, - no_neg_0=False): + rounding=round_nearest): + sep = 'E' if capitalize else 'e' @@ -1556,7 +1581,7 @@ def format_scientific(s, if sign != '-' and sign_spec != '-': sign = sign_spec - digits, exp_add = round_digits(digits, dps, base) + digits, exp_add = round_digits(digits, dps, base, rounding) exponent += exp_add if strip_zeros: @@ -1594,6 +1619,13 @@ def format_mpf(num, format_spec): strip_last_zero = False strip_zeros = False + if format_dict['rounding'] == round_ceiling: + rounding = round_down if num[0] else round_up + elif format_dict['rounding'] == round_floor: + rounding = round_up if num[0] else round_down + else: + rounding = format_dict['rounding'] + if fmt_type == 'g': if not format_dict['alternate']: strip_last_zero = True @@ -1621,7 +1653,8 @@ def format_mpf(num, format_spec): sep_range=3, base=10, alternate=format_dict['alternate'], - no_neg_0=format_dict['no_neg_0'] + no_neg_0=format_dict['no_neg_0'], + rounding=rounding ) else: # The format type is scientific sign, digits = format_scientific( @@ -1632,7 +1665,7 @@ def format_mpf(num, format_spec): base=10, capitalize=capitalize, alternate=format_dict['alternate'], - no_neg_0=format_dict['no_neg_0'] + rounding=rounding ) nchars = len(digits) + len(sign) diff --git a/mpmath/tests/test_format.py b/mpmath/tests/test_format.py index e208d1ba1..76d0752ff 100644 --- a/mpmath/tests/test_format.py +++ b/mpmath/tests/test_format.py @@ -504,6 +504,91 @@ def test_mpf_fmt(): assert f"{mp.mpf('0.24'):=+020.2e}" == '+000000000002.40e-01' assert f"{mp.mpf('0.24'):=+020.2g}" == '+0000000000000000.24' + # Tests for different kinds of rounding + num = mp.mpf('-1.23456789999901234567') + assert f"{num:=.2Uf}" == "-1.23" + assert f"{num:=.2Df}" == "-1.24" + assert f"{num:=.2Zf}" == "-1.23" + assert f"{num:=.2Nf}" == "-1.23" + + assert f"{num:=.3Uf}" == "-1.234" + assert f"{num:=.3Df}" == "-1.235" + assert f"{num:=.3Zf}" == "-1.234" + assert f"{num:=.3Nf}" == "-1.235" + + assert f"{num:=.10Uf}" == "-1.2345678999" + assert f"{num:=.10Df}" == "-1.2345679000" + assert f"{num:=.10Zf}" == "-1.2345678999" + assert f"{num:=.10Nf}" == "-1.2345679000" + + num = mp.mpf('1.23456789999901234567') + assert f"{num:=.2Uf}" == "1.24" + assert f"{num:=.2Df}" == "1.23" + assert f"{num:=.2Zf}" == "1.23" + assert f"{num:=.2Nf}" == "1.23" + + assert f"{num:=.3Uf}" == "1.235" + assert f"{num:=.3Df}" == "1.234" + assert f"{num:=.3Zf}" == "1.234" + assert f"{num:=.3Nf}" == "1.235" + + assert f"{num:=.10Uf}" == "1.2345679000" + assert f"{num:=.10Df}" == "1.2345678999" + assert f"{num:=.10Zf}" == "1.2345678999" + assert f"{num:=.10Nf}" == "1.2345679000" + + num = mp.mpf('-123.456789999901234567') + assert f"{num:=.2Ue}" == "-1.23e+02" + assert f"{num:=.2De}" == "-1.24e+02" + assert f"{num:=.2Ze}" == "-1.23e+02" + assert f"{num:=.2Ne}" == "-1.23e+02" + + assert f"{num:=.3Ue}" == "-1.234e+02" + assert f"{num:=.3De}" == "-1.235e+02" + assert f"{num:=.3Ze}" == "-1.234e+02" + assert f"{num:=.3Ne}" == "-1.235e+02" + + assert f"{num:=.10Ue}" == "-1.2345678999e+02" + assert f"{num:=.10De}" == "-1.2345679000e+02" + assert f"{num:=.10Ze}" == "-1.2345678999e+02" + assert f"{num:=.10Ne}" == "-1.2345679000e+02" + + num = mp.mpf('123456.789999901234567') + assert f"{num:=.2Ue}" == "1.24e+05" + assert f"{num:=.2De}" == "1.23e+05" + assert f"{num:=.2Ze}" == "1.23e+05" + assert f"{num:=.2Ne}" == "1.23e+05" + + assert f"{num:=.3Ue}" == "1.235e+05" + assert f"{num:=.3De}" == "1.234e+05" + assert f"{num:=.3Ze}" == "1.234e+05" + assert f"{num:=.3Ne}" == "1.235e+05" + + assert f"{num:=.10Ue}" == "1.2345679000e+05" + assert f"{num:=.10De}" == "1.2345678999e+05" + assert f"{num:=.10Ze}" == "1.2345678999e+05" + assert f"{num:=.10Ne}" == "1.2345679000e+05" + + assert f"{mp.mpf('123.456'):.2Ug}" == "1.3e+02" + assert f"{mp.mpf('123.456'):.2Dg}" == "1.2e+02" + assert f"{mp.mpf('123.456'):.2Zg}" == "1.2e+02" + assert f"{mp.mpf('123.456'):.2Ng}" == "1.2e+02" + + assert f"{mp.mpf('-123.456'):.2Ug}" == "-1.2e+02" + assert f"{mp.mpf('-123.456'):.2Dg}" == "-1.3e+02" + assert f"{mp.mpf('-123.456'):.2Zg}" == "-1.2e+02" + assert f"{mp.mpf('-123.456'):.2Ng}" == "-1.2e+02" + + assert f"{mp.mpf('123.456'):.5Ug}" == "123.46" + assert f"{mp.mpf('123.456'):.5Dg}" == "123.45" + assert f"{mp.mpf('123.456'):.5Zg}" == "123.45" + assert f"{mp.mpf('123.456'):.5Ng}" == "123.46" + + assert f"{mp.mpf('-123.456'):.5Ug}" == "-123.45" + assert f"{mp.mpf('-123.456'):.5Dg}" == "-123.46" + assert f"{mp.mpf('-123.456'):.5Zg}" == "-123.45" + assert f"{mp.mpf('-123.456'):.5Ng}" == "-123.46" + def test_mpf_fmt_special(): assert f'{inf:f}' == 'inf'