python-babel / babel

The official repository for Babel, the Python Internationalization Library
http://babel.pocoo.org/
BSD 3-Clause "New" or "Revised" License
1.29k stars 433 forks source link

Allow precision specification. #1010

Closed olunusib closed 7 months ago

olunusib commented 1 year ago

When bypassing decimal quantization, we should be able to specify precision. Fixes #893.

olunusib commented 1 year ago

This seems to overlap with the previously (#494, #577) soft-deprecated frac_prec override... Would it be enough to set pattern.frac_prec = (precision, precision) for the pattern instead of adding the new parameter here in the bowels of Pattern?

Further, I think an exception should be raised when trying to use the mutually exclusive arguments?

@akx,

gdiscry commented 10 months ago

I was looking in a similar issue. In my case, I also wanted to use the implicit precision coming with a str or Decimal value, even if the least significant digits are 0.

I came up with the following function to compute the value of the force_frac override for NumberPattern.apply:

def _get_frac_prec(
    pattern: babel.numbers.NumberPattern,
    value: float | decimal.Decimal | str,
    currency: str | None = None,
    currency_digits: bool = True,
    decimal_quantization: bool = True,
    decimal_normalization: bool = True,
    precision: int | None = None,
) -> tuple[int, int]:
    if precision is not None:
        min_prec = max_prec = precision
    elif currency and currency_digits:
        min_prec = max_prec = babel.numbers.get_currency_precision(currency)
    else:
        min_prec, max_prec = pattern.frac_prec

    if decimal_quantization and (
        pattern.exp_prec is None
        or min_prec != 0
        or max_prec != 0
    ):
        return min_prec, max_prec

    # Duplicates the beginning of NumberPattern.apply
    if not isinstance(value, decimal.Decimal):
        value = decimal.Decimal(str(value))
    value = value.scaleb(pattern.scale)

    num_prec = get_decimal_precision(value, normalized=decimal_normalization)
    return max(num_prec, min_prec), max(num_prec, max_prec)

Which requires to add a new parameter to get_decimal_precision to disable normalization:

def get_decimal_precision(number: decimal.Decimal, normalized: bool = True) -> int:
    assert isinstance(number, decimal.Decimal)
    if normalized:
        number = number.normalize()
    exponent = number.as_tuple().exponent
    # Note: DecimalTuple.exponent can be 'n' (qNaN), 'N' (sNaN), or 'F' (Infinity)
    if not isinstance(exponent, int) or exponent >= 0:
        return 0
    return abs(exponent)

When decimal_quantization is enabled, precision sets the exact number of digits after the decimal point. When disabled, precision sets the minimal number of digits, but there can be more digits (with decimal_normalization removing or keeping the non-significant 0).

FelixSchwarz commented 7 months ago

@Olunusib Did you close this on purpose or just resignated? I found this PR useful and it was some functionality that would be really useful to have.

olunusib commented 7 months ago

@FelixSchwarz It was inadvertently closed. I'll get it back up very soon.

clj commented 5 months ago

Hey @Olunusib, I'm just wondering about getting this back up? Seems like some generally useful functionality and it would help me get rid of the warnings I am getting because I'm currently using force_frac to achieve the same as this PR would allow me to do. :)