On Thu, 8 Oct 2020 at 16:59, Random832 random832@fastmail.com wrote:

I was making a "convert Fraction to Decimal, exactly if possible" function and ran into a wall: it's not possible to do some of the necessary operations with exact precision in decimal:

- multiplication
- division where the result can be represented exactly [the divisor is an integer whose prime factors are only two and five, or a rational number whose numerator qualifies]
I assume there's some way around it that I haven't spent enough time to figure out [create a temporary context with sufficint digits for multiplication, and work out the reciprocal power of 10 by hand to use this multiplication to implement division], but I feel like these exact operations should be supported in the standard library.

It should be possible to do this by bounding the number of digits required for an exact operation. We can count the number of digits in the mantissa of a Decimal like this:

In [100]: num_digits = lambda d: len(d.as_tuple().digits)

In [101]: num_digits(Decimal('12.34')) Out[101]: 4

If d3 = d1 * d2 then num_digits(d3) <= num_digits(d1) + num_digits(d2) so we can easily bound the number of digits needed for an exact multiplication. We can create a context for a number of digits and use it for multiplication like so:

In [106]: ctx = getcontext().copy()

In [107]: ctx.prec = 4

In [108]: ctx.multiply(Decimal('991'), Decimal('12')) Out[108]: Decimal('1.189E+4')

So then the result without rounding is:

In [109]: d2 = Decimal('991')

In [110]: d3 = Decimal('12')

In [111]: ctx.prec = num_digits(d2) + num_digits(d3)

In [112]: ctx.multiply(Decimal('991'), Decimal('12')) Out[112]: Decimal('11892')

For division it is a little more complicated. You say the division is exact so for some nonnegative integer k and integer n the divisor is either:

a) 2**k * 10**n b) 5**k * 10**n

Dividing/multiplying by 10 does not require any increase in precision but dividing by 2 or 5 requires at most 1 extra digit so:

In [114]: d = Decimal('123.4')

In [115]: ctx.prec = num_digits(d) + 1

In [116]: ctx.divide(d, 2) Out[116]: Decimal('61.7')

Of course to make that usable you'll need to calculate k.

Also you'll want to set the Inexact trap:

In [119]: ctx.traps[Inexact] = True

In [120]: ctx.divide(d, 3) --------------------------------------------------------------------------- Inexact Traceback (most recent call last) <ipython-input-120-9e7694c7c9ce> in <module> ----> 1 ctx.divide(d, 3)

Inexact: [<class 'decimal.Inexact'>]

For smallish Decimals it will probably be faster to convert to Rational but for sufficiently large exponents Rational will be very slow.

-- Oscar