Is there any difference between `Decimal("-nan")` and `Decimal("nan")`?

I stumbled on strange nan behavior.

from decimal import Decimal

nan = Decimal("nan")
mnan = Decimal("-nan")

print(nan)
# NaN

print(mnan)
# -NaN

print(mnan == nan)
# False

print(mnan is nan)
# False

So it looks like Decimal("-nan") might be different than Decimal("nan"). But we also have

print(nan.is_nan())
# True

print(mnan.is_nan())
# True

They both pass is_nan() checks, unsurprisingly.

But then we must recall that Decimal("nan") has strange comparison behavior.

nan2 = Decimal("nan")

print(nan == nan2)
# False

print(nan is nan2)
# False

So mnan compares to nan the same as nan2 compares to nan. From this perspective it seems like Decimal("-nan") behaves exactly like Decimal("-nan") except for the __repr__ which includes the minus sign for Decimal("-nan").

Does Decimal("-nan") matter? I have a number formatting library and I’m wondering if it could ever matter that I preserve the minus sign on Decimal("-nan"). I know nan is defined in some technical specifications. Is -nan specified?

Note that

print(float("-nan"))
# nan

So for floats the -nan seems to be cast to just nan.

Here are some other ways that Decimal("nan") and Decimal("-nan") are distinguishable besides their __repr__:

>>> Decimal("nan").is_signed()
False
>>> Decimal("-nan").is_signed()
True

>>> Decimal(2).copy_sign(Decimal("nan"))
Decimal('2')
>>> Decimal(2).copy_sign(Decimal("-nan"))
Decimal('-2')

>>> Decimal("nan").copy_negate()
Decimal('-NaN')
>>> Decimal("-nan").copy_negate()
Decimal('NaN')

>>> Decimal("nan").compare_total(Decimal("0"))
Decimal('1')
>>> Decimal("-nan").compare_total(Decimal("0"))
Decimal('-1')

>>> Decimal("nan").compare_total(Decimal("-nan"))
Decimal('1')
>>> Decimal("-nan").compare_total(Decimal("nan"))
Decimal('-1')

I suppose it depends. From the specs:

[…] the sign of a NaN has no meaning, although it may be considered part of the diagnostic information.

It seems that float('-nan') does construct a signed NaN.

>>> from math import copysign
>>> copysign(222, float('-nan'))
-222.0
>>> copysign(222, float('nan'))
222.0
1 Like

At least in IEEE 754 (which, for most purposes, is the only floating-point format you need to worry about), there is an entire family of NaN values. They are distinguished by have all of the exponent bits set to 1, and at least one fraction bit set to 1. (All-1 exponent and all-0 fraction is infinity.) So half of all NaNs are “positive” (sign bit 0), and the other half are “negative” (sign bit 1). Things like float("nan") and float("-nan") return a particular representative signed NaN.

>>> import struct
>>> struct.pack("!d", float("nan"))
b'\x7f\xf8\x00\x00\x00\x00\x00\x00'
>>> struct.pack("!d", float("-nan"))
b'\xff\xf8\x00\x00\x00\x00\x00\x00'
2 Likes