13

With simple ints:

>>> -45 % 360
315

Whereas, using a decimal.Decimal:

>>> from decimal import Decimal
>>> Decimal('-45') % 360
Decimal('-45')

I would expect to get Decimal('315').

Is there any reason for this? Is there a way to get a consistent behaviour (without patching decimal.Decimal)? (I did not change the context, and cannot find how it could be changed to solve this situation).

zezollo
  • 4,606
  • 5
  • 28
  • 59
  • You make it sound like patching is something extreme and to be avoided, but languages like Python give you a lot of room to do this in your own code (subclasses, magic "dunder" methods, wrappers, etc.). And of course you can always do the simple but effective workaround of writing your own function to use instead of the built-in syntax. – John Y May 08 '18 at 16:26
  • @JohnY Sorry about that, I didn't mean to sound like that; my intent was to let the question not be understood as "How to patch Decimal to get such behaviour?", but instead: "Is there a reason for such a behaviour?" and "Is there any already implemented mean to change this behaviour (e.g. through context settings)?". Maybe I should have mentionned I already implemented a small workaround where I needed it. – zezollo May 10 '18 at 08:44

2 Answers2

11

After a long search (because searching on "%", "mod", "modulo" etc. gives a thousand of results), I finally found that, surprisingly, this is intended:

There are some small differences between arithmetic on Decimal objects and arithmetic on integers and floats. When the remainder operator % is applied to Decimal objects, the sign of the result is the sign of the dividend rather than the sign of the divisor:

>>> (-7) % 4
1
>>> Decimal(-7) % Decimal(4)
Decimal('-3')

I don't know the reason for this, but it looks like it's not possible to change this behaviour (without patching).

Cristian Ciupitu
  • 20,270
  • 7
  • 50
  • 76
zezollo
  • 4,606
  • 5
  • 28
  • 59
  • 2
    Does `Decimal` use `math.fmod()`? They both have the same behaviour. [From the Python documentation](https://docs.python.org/3/library/math.html): "For this reason, function fmod() is generally preferred when working with floats, while Python’s x % y is preferred when working with integers." – Mr. T Jan 19 '18 at 20:59
  • @Piinthesky From what I understand in the library's source code, it does `import math as _math` but then `_math` is only used to build a `Decimal` instance from a `float`. `__mod__` (as well as other methods, like `__truediv__`) calculates the quotient and the remainder from a call to `Decimal._divide()`, that relies on a `divmod` called on integers. Yet it does not produce the same result... Note that: `divmod(-45, 360) == (-1, 315)` but `divmod(Decimal('-45'), Decimal('360')) == (Decimal('-0'), Decimal('-45'))`. – zezollo Jan 20 '18 at 07:04
  • I think @Mr.T is on the right track by mentioning `fmod`. Even if the *implementation* of `Decimal` doesn't touch `fmod` at all, it seems at least plausible that the intention was to emulate the *behavior* of `fmod`. – John Y May 07 '18 at 22:09
  • Sorry for the edits. I thought that adding an excerpt would clarify things enough, but then I discovered it was a bit more complicated, so I added my own answer. – Cristian Ciupitu May 07 '18 at 23:21
8

Python behaves according to IBM's General Decimal Arithmetic Specification.

The remainder is defined as:

remainder takes two operands; it returns the remainder from integer division. […]

the result is the residue of the dividend after the operation of calculating integer division as described for divide-integer, rounded to precision digits if necessary. The sign of the result, if non-zero, is the same as that of the original dividend.

So because Decimal('-45') // D('360') is Decimal('-0'), the remainder can only be Decimal('-45').

Though why is the quotient 0 and not -1? The specification says:

divide-integer takes two operands; it divides two numbers and returns the integer part of the result. […]

the result returned is defined to be that which would result from repeatedly subtracting the divisor from the dividend while the dividend is larger than or equal to the divisor. During this subtraction, the absolute values of both the dividend and the divisor are used: the sign of the final result is the same as that which would result if normal division were used. […]

Notes: […]

  1. The divide-integer and remainder operations are defined so that they may be calculated as a by-product of the standard division operation (described above). The division process is ended as soon as the integer result is available; the residue of the dividend is the remainder.

How many times can you subtract 360 from 45? 0 times. Is an integer result available? It is. Then the quotient is 0 with a minus sign because the divide operation says that

The sign of the result is the exclusive or of the signs of the operands.

As for why the Decimal Specification goes on this route, instead of doing it like in math where the remainder is always positive, I'm speculating that it could be for the simplicity of the subtraction algorithm. No need to check the sign of the operands in order to compute the absolute value of the quotient. Modern implementations probably use more complicated algorithms anyway, but simplicity could be have an important factor back in the days when the standard was taking form and hardware was simpler (way fewer transistors). Fun fact: Intel switched from radix-2 integer division to radix-16 only in 2007 with the release of Penryn.

Cristian Ciupitu
  • 20,270
  • 7
  • 50
  • 76