0

I would need to format a python Decimal object to have atleast two decimals, but no more than 5. Is there a reliable way to do this? Examples:

1.6 --> 1.60
1.678 --> 1.678
1.98765 --> 1.98765

If there are more than two decimals, it is vital that it does not get truncated to only two decimals.

Håkan
  • 13
  • 6
  • Decimals as numbers are the same representation. You can transform them into string and use string formatting operations to achieve this. For that, you can use the following question: https://stackoverflow.com/questions/40999973/how-to-pad-a-numeric-string-with-zeros-to-the-right-in-python – Alexander Santos Aug 03 '21 at 14:33
  • @AlexanderSantos I am sorry, but the question I asked is not a duplicate, as truncation is not an option. – Håkan Aug 03 '21 at 14:43
  • It looks to me like there are two parts to this question - one, determining the correct number of digits and two, quantizing the values to that number of digits. The second is answered by the accepted answer on the marked duplicate (use the `quantize()` method - there are multiple ways to construct a `decimal.Decimal` value of the correct exponent to use as the `quantize()` arg) but the first is not. – Peter DeGlopper Aug 03 '21 at 14:46

2 Answers2

0

It looks to me like there are two parts to this question - one, determining the correct number of digits and two, quantizing the values to that number of digits.

To do the first, I would get the current exponent using the as_tuple() method. Unless I'm overlooking something simpler.

>>> import decimal
>>> d = decimal.Decimal("1.678")
>>> d.as_tuple().exponent
-3

>>> d2 = decimal.Decimal("1.6")
>>> d2.as_tuple().exponent
-1

So from that you can compute the desired exponent:

MAX_EXPONENT = -2
MIN_EXPONENT = -5
def desired_exponent(d):
    current_exponent = d.as_tuple().exponent
    return min(MAX_EXPONENT, max(MIN_EXPONENT, current_exponent))

The second is answered by the accepted answer on the marked duplicate - use the quantize() method. You'll need to construct a Decimal value with the desired exponent you can provide as the argument to quantize(). There are multiple ways to do that, but two simple ones are exponentiating decimal.Decimal("10") or using the tuple constructor for decimal.Decimal().

>>> quant_arg = decimal.Decimal("10") ** -2
>>> decimal.Decimal("1.6").quantize(quant_arg)
Decimal('1.60')

Or:

>>> quant_arg = decimal.Decimal((0, (), -2))
>>> decimal.Decimal("1.6").quantize(quant_arg)
Decimal('1.60')

I used -2 as a literal there, you'd want to use the calculated value of desired_exponent.

There are multiple ways to organize this code, I think the parts that are not obvious are a) accessing the current exponent of a decimal value and b) some of the ways of constructing an arg for quantize(). And this is all assuming you need the actual decimal objects, and aren't just outputting them - if this is a question just about output formatting re-quantizing is probably overkill.

wjandrea
  • 28,235
  • 9
  • 60
  • 81
Peter DeGlopper
  • 36,326
  • 7
  • 90
  • 83
  • Is quantize a costly operation? Right now my main concern is stability and maintainability, but I am open to suggestions. It would be good if python had this built-in. I have made a solution now based on your suggestions. – Håkan Aug 03 '21 at 16:51
  • It creates a new `decimal.Decimal` object, so it's not too bad but there's some data copying. I wouldn't let that stop me unless I were doing this for extremely large numbers of objects, personally - recommending formatting over quantizing if you're only using this for output formatting is just trying to choose the right level to change things - like using a truncating format string instead of `int()` if I wanted just the integer part of a float. – Peter DeGlopper Aug 03 '21 at 16:59
0

Here is the code I use now:

def unitAmount(value):
    """Format a Decimal to match -?[0-9]{1,15}(,[0-9]{2,5})?
    Minimum two decimals, max 5.
    """
    decimals = value.as_tuple().exponent
    if decimals == -1:  # For values like 1.6 --> 1.60
        value = value.quantize(Decimal('1.00'))
    elif decimals < -5:  # For values like 1.1234567.. --> 1.12345
        value = value.quantize(Decimal('1.00000'))
    return value
Håkan
  • 13
  • 6
  • Do you ever have input that might have an exponent of 0 or higher, eg `value = decimal.Decimal("10")`? I might check `if decimals >= -1:` personally. – Peter DeGlopper Aug 03 '21 at 18:05
  • The function is used for an invoicing-software and specifies the unit price. For some reason the standard allows integers, but not decimals with only one decimal. A comma is used as the decimal point. – Håkan Aug 05 '21 at 10:31