ext-decimal icon indicating copy to clipboard operation
ext-decimal copied to clipboard

Differences between 0 and -0

Open julienfalque opened this issue 3 years ago • 12 comments

Decimal does not consider 0 and -0 to be exactly the same value:

  • 0 is considered positive (isPositive() returns true and isNegative() returns false) while
  • -0 is considered negative (isPositive() returns false and isNegative() returns true).

Also, toString() and toFixed() will include the - sign for -0.

I found two ways to create a Decimal instance with -0:

  • new Decimal('-0')
  • (new Decimal(0))->negate()

While the first is unlikely, the second may occur in case one negates a value without checking whether it's zero first. Given the differences listed above, this might induce unexpected behavior in the code doing this.

According to Wikipedia, 0 can either be considered:

  • neither positive nor negative;
  • both positive and negative.

I would suggest choosing one of these two possibilities and implement methods accordingly, i.e.:

  • if neither positive nor negative:
    • isPositive(): false
    • isNegative(): false
    • isNonNegative(): true
    • isNonPositive(): true
  • if both positive and negative:
    • isStrictlyNegative(): false
    • isStrictlyPositive(): false
    • isPositive(): true
    • isNegative(): true

Also, I think -0 should be converted to 0 internally.

WDYT?

julienfalque avatar Feb 09 '22 10:02 julienfalque

I think this came up because something like -0.1 / 1000 would truncate to 0 if there is not enough precision available to represent that result correctly. Maybe storing the precision alongside the value was a bad design choice. From https://docs.python.org/3/library/decimal.html#decimal.Decimal:

The context precision does not affect how many digits are stored. That is determined exclusively by the number of digits in value. For example, Decimal('3.00000') records all five zeros even if the context precision is only three.

Maybe the implicit context approach in php-decimal (one global shared context) gives way to these small inconsistencies. The idea was to have ((new Decimal("-0.1", 2) / new Decimal("1000000"))->isNegative() return true and display as -0, but I can see how that is confusing. Maybe negative 0 should not exist here and zero be neither positive nor negative.

Should we go with what is practical and intuitive, or what is more correct from a mathematical point of view?

My current intuition is:

  • I think -0 should be converted to 0 internally.
  • isPositive(): false
  • isNegative(): false

rtheunissen avatar Feb 09 '22 19:02 rtheunissen

So you would choose the zero is neither positive nor negative interpretation? Do you think adding isNonNegative()/isNonPositive() methods makes sense? Also, would you considerer this a BC break?

I have no experience in C but, if you don't mind, I'd like to try contributing those changes and make this a learning opportunity :)

julienfalque avatar Feb 10 '22 13:02 julienfalque

Yes I think neither positive nor negative, and would those methods be equivalent to eg. NonNegative = isPositive() or isZero()? I think the compound is nice because there is no ambiguity to the reader. Prefer local reasoning 😊

Feel free to look at the 2.0 branch, should be able to navigate that quite easily. The mpdecimal docs are handy too. We can use this issue for questions and comments.

rtheunissen avatar Feb 10 '22 15:02 rtheunissen

I wasn't able to get all tests passing except on branch 1.x. Are branches master and 2.0 up-to-date?

julienfalque avatar Feb 15 '22 17:02 julienfalque

There is also another way of causing this issue. If a negative decimal value is rounded to 0.00, it will still be negative. See the below attached screenshot: Screenshot_20230425_135641

CreepPork avatar Apr 25 '23 10:04 CreepPork

If a negative decimal value is rounded to 0.00, it will still be negative

The work done here is good but I'm not sure that we should treat -0 and +0 as equal.

https://stackoverflow.com/questions/4083401/negative-zero-in-python

rtheunissen avatar Jan 20 '24 05:01 rtheunissen

Maybe we could do so for formatting, but not calculation?

rtheunissen avatar Jan 20 '24 05:01 rtheunissen

I fail to see a benefit in keeping -0 not equal to 0, even for calculation only.

julienfalque avatar Jan 22 '24 13:01 julienfalque

If you have a negative number and round it incidentally to zero, it would be incorrect to change the sign. I'm sure the python folks thought it through.

I can see the value in always producing a sign-less "0" when formatting, but to have isNegative and arithmetic honor the sign unchanged.

rtheunissen avatar Jan 22 '24 20:01 rtheunissen

If someone would like to render the zero sign, they can do so with checks and a prefix.

rtheunissen avatar Jan 22 '24 20:01 rtheunissen

https://peps.python.org/pep-0682/

rtheunissen avatar Jan 22 '24 20:01 rtheunissen

As the referenced document mentioned, I would also agree that the best solution for this would be an opt-in approach.

As far as I can understand, in 2.x you can already select the default rounding mode, why not this as an opt-in method too?

Because, in version 1.x, I already have too many ->toFixed(2, true, PHP_ROUND_HALF_UP) calls, that it already pollutes the code enough, but if I have to add another operation before that to correctly get the - sign right, then I don't think that's cool.

CreepPork avatar Jan 22 '24 20:01 CreepPork