diff --git a/src/sciform/format_utils/make_strings.py b/src/sciform/format_utils/make_strings.py index 7ef69ad0..eeb1e782 100644 --- a/src/sciform/format_utils/make_strings.py +++ b/src/sciform/format_utils/make_strings.py @@ -98,8 +98,6 @@ def construct_num_str( def construct_val_unc_str( # noqa: PLR0913 val_mantissa_str: str, unc_mantissa_str: str, - val_mantissa: Decimal, - unc_mantissa: Decimal, decimal_separator: DecimalSeparatorEnums, *, paren_uncertainty: bool, @@ -113,17 +111,12 @@ def construct_val_unc_str( # noqa: PLR0913 pm_symb = f" {pm_symb} " val_unc_str = f"{val_mantissa_str}{pm_symb}{unc_mantissa_str}" else: - if ( - paren_uncertainty_trim - and unc_mantissa.is_finite() - and val_mantissa.is_finite() - and 0 < unc_mantissa < abs(val_mantissa) - ): + if paren_uncertainty_trim: """ - Don't strip the unc_mantissa_str if val_mantissa is non-finite. - Don't strip the unc_mantissa_str if unc_mantissa == 0 (because then the - empty string would remain). - Don't left strip the unc_mantissa_str if unc_mantissa >= val_mantissa + Strip out all leading zeros and separators except the + decimal separator if it has non-zero numbers to the left. + (000_000.004 567) -> (4567) + (0_123.456 789) -> (123.456) """ for separator in SeparatorEnum: if separator != decimal_separator: diff --git a/src/sciform/format_utils/rounding.py b/src/sciform/format_utils/rounding.py index edf524a7..9d7e2347 100644 --- a/src/sciform/format_utils/rounding.py +++ b/src/sciform/format_utils/rounding.py @@ -81,12 +81,13 @@ def round_val_unc( val: Decimal, unc: Decimal, ndigits: int | NDigitsEnum, + round_mode: RoundModeEnum, ) -> tuple[Decimal, Decimal, int]: """Simultaneously round the value and uncertainty.""" if unc.is_finite() and unc != 0: round_digit = get_round_dec_place( unc, - RoundModeEnum.SIG_FIG, + round_mode, ndigits, ) unc_rounded = round(unc, -round_digit) @@ -97,7 +98,7 @@ def round_val_unc( elif val.is_finite() and val != 0: round_digit = get_round_dec_place( val, - RoundModeEnum.SIG_FIG, + round_mode, ndigits, ) unc_rounded = unc diff --git a/src/sciform/formatting/number_formatting.py b/src/sciform/formatting/number_formatting.py index 2998e034..d602d06a 100644 --- a/src/sciform/formatting/number_formatting.py +++ b/src/sciform/formatting/number_formatting.py @@ -2,10 +2,10 @@ from __future__ import annotations +import re from dataclasses import replace from decimal import Decimal from typing import TYPE_CHECKING, cast -from warnings import warn from sciform.api.formatted_number import FormattedNumber from sciform.format_utils.exponents import get_exp_str, get_val_unc_exp @@ -28,6 +28,7 @@ ExpFormatEnum, ExpModeEnum, ExpValEnum, + NDigitsEnum, RoundModeEnum, SignModeEnum, ) @@ -38,6 +39,47 @@ from sciform.options.input_options import InputOptions +def re_round_mantissa_exp_decomposition( + number: Number, + exp_mode: ExpModeEnum, + input_exp_val: int | ExpValEnum, + round_mode: RoundModeEnum, + ndigits: int | NDigitsEnum, +) -> tuple[Decimal, int, int, int]: + """Decompose a number into a mantissa and exponent using repeated rounding.""" + first_mantissa, first_exp_val, base = get_mantissa_exp_base( + number, + exp_mode, + input_exp_val, + ) + first_round_digit = get_round_dec_place(first_mantissa, round_mode, ndigits) + first_mantissa_rounded = round(first_mantissa, -first_round_digit) + + number_rounded = first_mantissa_rounded * base ** Decimal(first_exp_val) + + """ + Repeat mantissa + exponent discovery after rounding in case rounding + altered the required exponent. + """ + second_mantissa, exp_val, _ = get_mantissa_exp_base( + number_rounded, exp_mode, input_exp_val + ) + round_digit = get_round_dec_place(second_mantissa, round_mode, ndigits) + mantissa = round(second_mantissa, -round_digit) + mantissa = cast(Decimal, mantissa) + + if mantissa == 0: + """ + This catches an edge case involving negative ndigits when the + resulting mantissa is zero after the second rounding. This + result is technically correct (e.g. 0e+03 = 0e+00), but sciform + always presents zero values with an exponent of zero. + """ + exp_val = 0 + + return mantissa, exp_val, base, round_digit + + def format_from_options( value: Number, uncertainty: Number | None = None, @@ -117,38 +159,15 @@ def format_num(num: Decimal, options: FinalizedOptions) -> str: num *= 100 num = num.normalize() - exp_val = options.exp_val - round_mode = options.round_mode - exp_mode = options.exp_mode - ndigits = options.ndigits - mantissa, temp_exp_val, base = get_mantissa_exp_base(num, exp_mode, exp_val) - round_digit = get_round_dec_place(mantissa, round_mode, ndigits) - mantissa_rounded = round(mantissa, -round_digit) - - """ - Repeat mantissa + exponent discovery after rounding in case rounding - altered the required exponent. - """ - rounded_num = mantissa_rounded * Decimal(base) ** Decimal(temp_exp_val) - mantissa, exp_val, base = get_mantissa_exp_base(rounded_num, exp_mode, exp_val) - round_digit = get_round_dec_place(mantissa, round_mode, ndigits) - mantissa_rounded = round(mantissa, -int(round_digit)) - mantissa_rounded = cast(Decimal, mantissa_rounded) - - if mantissa_rounded == 0: - """ - This catches an edge case involving negative ndigits when the - resulting mantissa is zero after the second rounding. This - result is technically correct (e.g. 0e+03 = 0e+00), but sciform - always presents zero values with an exponent of zero. - """ - exp_val = 0 + mantissa, exp_val, base, round_dec_place = re_round_mantissa_exp_decomposition( + num, options.exp_mode, options.exp_val, options.round_mode, options.ndigits + ) left_pad_char = options.left_pad_char.value mantissa_str = construct_num_str( - mantissa_rounded.normalize(), + mantissa.normalize(), options.left_pad_dec_place, - round_digit, + round_dec_place, options.sign_mode, left_pad_char, ) @@ -166,7 +185,7 @@ def format_num(num: Decimal, options: FinalizedOptions) -> str: exp_str = get_exp_str( exp_val=exp_val, - exp_mode=exp_mode, + exp_mode=options.exp_mode, exp_format=options.exp_format, capitalize=options.capitalize, superscript=options.superscript, @@ -190,13 +209,6 @@ def format_val_unc(val: Decimal, unc: Decimal, options: FinalizedOptions) -> str ) raise NotImplementedError(msg) - if options.round_mode is RoundModeEnum.DEC_PLACE: - msg = ( - "Precision round mode not available for value/uncertainty formatting. " - "Rounding is always applied as significant figures for the uncertainty." - ) - warn(msg, stacklevel=2) - unc = abs(unc) if exp_mode is ExpModeEnum.PERCENT: val *= 100 @@ -220,11 +232,13 @@ def format_val_unc(val: Decimal, unc: Decimal, options: FinalizedOptions) -> str val, unc, options.ndigits, + options.round_mode, ) val_rounded, unc_rounded, round_digit = round_val_unc( val_rounded, unc_rounded, options.ndigits, + options.round_mode, ) exp_val = get_val_unc_exp( @@ -234,6 +248,11 @@ def format_val_unc(val: Decimal, unc: Decimal, options: FinalizedOptions) -> str options.exp_val, ) + if options.round_mode is RoundModeEnum.SIG_FIG: + ndigits = -round_digit + exp_val + else: + ndigits = options.ndigits + val_mantissa, _, _ = get_mantissa_exp_base( val_rounded, exp_mode=exp_mode, @@ -252,8 +271,6 @@ def format_val_unc(val: Decimal, unc: Decimal, options: FinalizedOptions) -> str left_pad_matching=options.left_pad_matching, ) - ndigits = -round_digit + exp_val - """ We will format the val and unc mantissas * using decimal place rounding mode with the ndigits calculated @@ -289,15 +306,21 @@ def format_val_unc(val: Decimal, unc: Decimal, options: FinalizedOptions) -> str val_mantissa_str = parse_mantissa_from_ascii_exp_str(val_mantissa_exp_str) unc_mantissa_str = parse_mantissa_from_ascii_exp_str(unc_mantissa_exp_str) + paren_uncertainty_trim = ( + options.paren_uncertainty_trim + and val.is_finite() + and unc.is_finite() + and unc < abs(val) + and re.search(r"[1-9]", unc_mantissa_str) is not None + ) + val_unc_str = construct_val_unc_str( val_mantissa_str=val_mantissa_str, unc_mantissa_str=unc_mantissa_str, - val_mantissa=val_mantissa, - unc_mantissa=unc_mantissa, decimal_separator=options.decimal_separator, paren_uncertainty=options.paren_uncertainty, pm_whitespace=options.pm_whitespace, - paren_uncertainty_trim=options.paren_uncertainty_trim, + paren_uncertainty_trim=paren_uncertainty_trim, ) if val.is_finite() or unc.is_finite() or options.nan_inf_exp: diff --git a/tests/feature/test_val_unc_formatter.py b/tests/feature/test_val_unc_formatter.py index 6baa90e4..3be210a4 100644 --- a/tests/feature/test_val_unc_formatter.py +++ b/tests/feature/test_val_unc_formatter.py @@ -95,6 +95,14 @@ def test_paren_unc_invalid_unc(self): [ (Formatter(paren_uncertainty=True), "0(0)"), (Formatter(paren_uncertainty=True, ndigits=3), "0(0)"), + ( + Formatter( + paren_uncertainty=True, + round_mode="dec_place", + ndigits=3, + ), + "0.000(0.000)", + ), ], ), ( @@ -102,6 +110,14 @@ def test_paren_unc_invalid_unc(self): [ (Formatter(paren_uncertainty=True), "0(inf)"), (Formatter(paren_uncertainty=True, ndigits=3), "0(inf)"), + ( + Formatter( + paren_uncertainty=True, + round_mode="dec_place", + ndigits=3, + ), + "0.000(inf)", + ), ], ), ] @@ -426,10 +442,6 @@ def test_pdg_sig_figs(self): self.run_val_unc_formatter_cases(cases_list) - def test_dec_place_warn(self): - formatter = Formatter(round_mode="dec_place") - self.assertWarns(Warning, formatter, 42, 24) - def test_left_pad_matching(self): formatter = Formatter(left_pad_matching=True) result = formatter(123, 0.123) diff --git a/tests/feature/test_val_unc_fsml.py b/tests/feature/test_val_unc_fsml.py index b824c80c..fbcb7858 100644 --- a/tests/feature/test_val_unc_fsml.py +++ b/tests/feature/test_val_unc_fsml.py @@ -34,6 +34,15 @@ def test_fixed(self): ("!3f", "123.456 ± 0.789"), ("!4f", "123.4560 ± 0.7890"), ("!2f()", "123.46(79)"), + (".-3f", "0 ± 0"), + (".-2f", "100 ± 0"), + (".-1f", "120 ± 0"), + (".0f", "123 ± 1"), + (".1f", "123.5 ± 0.8"), + (".2f", "123.46 ± 0.79"), + (".3f", "123.456 ± 0.789"), + (".4f", "123.4560 ± 0.7890"), + (".2f()", "123.46(79)"), ], ), ( @@ -65,6 +74,24 @@ def test_fixed(self): ("!5f()", "0.79(123.46)"), ("!6f()", "0.789(123.456)"), ("!7f()", "0.7890(123.4560)"), + (".1f", "0.8 ± 123.5"), + (".2f", "0.79 ± 123.46"), + (".3f", "0.789 ± 123.456"), + (".4f", "0.7890 ± 123.4560"), + (".5f", "0.78900 ± 123.45600"), + (".6f", "0.789000 ± 123.456000"), + (".7f", "0.7890000 ± 123.4560000"), + (".-3f()", "0(0)"), + (".-2f()", "0(100)"), + (".-1f()", "0(120)"), + (".0f()", "1(123)"), + (".1f()", "0.8(123.5)"), + (".2f()", "0.79(123.46)"), + (".3f()", "0.789(123.456)"), + (".4f()", "0.7890(123.4560)"), + (".5f()", "0.78900(123.45600)"), + (".6f()", "0.789000(123.456000)"), + (".7f()", "0.7890000(123.4560000)"), ], ), ( @@ -196,6 +223,11 @@ def test_engineering_shifted(self): ("#!3r", "(0.123456 ± 0.000789)e+03"), ("#!4r", "(0.1234560 ± 0.0007890)e+03"), ("#!2r()", "0.12346(79)e+03"), + ("#.1r", "(0.1 ± 0.0)e+03"), + ("#.2r", "(0.12 ± 0.00)e+03"), + ("#.3r", "(0.123 ± 0.001)e+03"), + ("#.4r", "(0.1235 ± 0.0008)e+03"), + ("#.2r()", "0.12(0.00)e+03"), ], ), ( diff --git a/tests/unit/format_utils/test_make_strings.py b/tests/unit/format_utils/test_make_strings.py index 1c28edf0..7c1c152d 100644 --- a/tests/unit/format_utils/test_make_strings.py +++ b/tests/unit/format_utils/test_make_strings.py @@ -143,8 +143,6 @@ def test_construct_val_unc_str(self): { "val_mantissa_str": "123.456", "unc_mantissa_str": "0.123", - "val_mantissa": Decimal("123.456"), - "unc_mantissa": Decimal("0.123"), "decimal_separator": ".", "paren_uncertainty": False, "pm_whitespace": True, @@ -156,8 +154,6 @@ def test_construct_val_unc_str(self): { "val_mantissa_str": "123.456", "unc_mantissa_str": "0.123", - "val_mantissa": Decimal("123.456"), - "unc_mantissa": Decimal("0.123"), "decimal_separator": ".", "paren_uncertainty": False, "pm_whitespace": False, @@ -169,8 +165,6 @@ def test_construct_val_unc_str(self): { "val_mantissa_str": "123,456", "unc_mantissa_str": "0,123", - "val_mantissa": Decimal("123.456"), - "unc_mantissa": Decimal("0.123"), "decimal_separator": ",", "paren_uncertainty": False, "pm_whitespace": True, @@ -182,8 +176,6 @@ def test_construct_val_unc_str(self): { "val_mantissa_str": "123.456", "unc_mantissa_str": "0.123", - "val_mantissa": Decimal("123.456"), - "unc_mantissa": Decimal("0.123"), "decimal_separator": ".", "paren_uncertainty": False, "pm_whitespace": True, @@ -195,8 +187,6 @@ def test_construct_val_unc_str(self): { "val_mantissa_str": "123,456", "unc_mantissa_str": "0,123", - "val_mantissa": Decimal("123.456"), - "unc_mantissa": Decimal("0.123"), "decimal_separator": ",", "paren_uncertainty": True, "pm_whitespace": True, @@ -208,8 +198,6 @@ def test_construct_val_unc_str(self): { "val_mantissa_str": "123,456", "unc_mantissa_str": "0,123", - "val_mantissa": Decimal("123.456"), - "unc_mantissa": Decimal("0.123"), "decimal_separator": ",", "paren_uncertainty": True, "pm_whitespace": True, @@ -221,8 +209,6 @@ def test_construct_val_unc_str(self): { "val_mantissa_str": "123,456,789.123_456", "unc_mantissa_str": "0.123_456", - "val_mantissa": Decimal("123456789.123456"), - "unc_mantissa": Decimal("0.123456"), "decimal_separator": ",", "paren_uncertainty": True, "pm_whitespace": True, @@ -234,8 +220,6 @@ def test_construct_val_unc_str(self): { "val_mantissa_str": "123,456,789.123_456", "unc_mantissa_str": "0.123_456", - "val_mantissa": Decimal("123456789.123456"), - "unc_mantissa": Decimal("0.123456"), "decimal_separator": ",", "paren_uncertainty": True, "pm_whitespace": True, @@ -247,12 +231,10 @@ def test_construct_val_unc_str(self): { "val_mantissa_str": "0.123", "unc_mantissa_str": "123,456.456", - "val_mantissa": Decimal("0.123"), - "unc_mantissa": Decimal("123456.456"), "decimal_separator": ",", "paren_uncertainty": True, "pm_whitespace": True, - "paren_uncertainty_trim": True, + "paren_uncertainty_trim": False, }, "0.123(123,456.456)", ), diff --git a/tests/unit/format_utils/test_rounding_utils.py b/tests/unit/format_utils/test_rounding_utils.py index f84163c1..1e4ce374 100644 --- a/tests/unit/format_utils/test_rounding_utils.py +++ b/tests/unit/format_utils/test_rounding_utils.py @@ -252,6 +252,7 @@ def test_round_val_unc(self): val, unc, ndigits, + RoundModeEnum.SIG_FIG, ) with self.subTest( val=val,