To start with, quoting Kahan is ironic
. He was the driving force behind 754, and a tireless advocate for it mandating nearest/even as the default rounding mode. For example, here’s one of his papers that has no real point other than to beat to death how nearest/even produces “the right” result in a toy program where almost any other approach gets it wrong. It’s not a “toy” paper, though - he put real time and brainpower into writing it.
That’s just wrong; e.g.,
>>> from decimal import Decimal as D
>>> D(1) / 3
Decimal('0.3333333333333333333333333333')
The infinitely precise result has an infinitely long decimal representation. decimal correctly rounds it to the current context precision (28).
>>> import decimal
>>> decimal.getcontext().prec
28
I was intimately involved at the time the first 754 standard was released, back in the mid 1980s. I’m quoting from 754-1985:
Section 4.1: “An implementation of this standard shall provide round to nearest as the default rounding mode.”
In the language of standards, “shall” has a precise technical meaning, best paraphrased as “mandatory”. Not a suggestion, not an encouragement, not “a nice to have”, but a non-negotiable requirement. In contrast, e.g., “should” is just an encouragement.
You’re quoting instead from the 754-2008 revision, made over 2 decades later, and about 4 times the length. But you’re misstating its similar clause:
Section 4.3.3 “The roundTiesToEven rounding-direction attribute shall be the default rounding-direction attribute for results in binary formats. The default rounding-direction attribute for results in decimal formats is language defined, but should be roundTiesToEven.”
There’s no similar text in the original 754 because the original didn’t say one syllable about non-binary arithmetic. Note that “shall” again means there is no possible dispute about the required nearest/even default rounding for binary formats. The “should” for decimal formats is unfortunate - that one is just a suggestion. However, note that it’s suggesting the rounding mode you hate even for decimal arithmetic.
But it doesn’t matter, because 754-2008 is largely ignored in favor of the still later IEEE-854 standard, which also incorporates IBM’s General Decimal Arithmetic standard.
That mandates some different “contexts” for decimal arithmetic. The “basic” context requires round-half-up rounding, 9 digits of precision, and enables traps on things like overflow and invalid operation. The “extended” contexts are for “serious” use, and require round-half-even rounding, disable all traps by default, and come in a number of variants depending on how much precision you want.
Python’s decimal module implements all of that, and, far more, imposes no meaningful limit on how much precision you can ask for.
>>> import decimal
>>> decimal.getcontext().prec = decimal.MAX_PREC
>>> decimal.Decimal(1) / 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
MemoryError
>>> decimal.getcontext().prec
999999999999999999
I asked for quadrillions of digits of precision, which is why I ran out RAM for computing 1/3.
For the rest, I don’t see a real point in continuing. You dismiss the SO post I linked to because it’s not “a proof”. True enough! But you have no proof either. Common sense and the vast bulk of empirical evidence aren’t that hard to find on the web. The fellow who wrote the SO post is a long-time valued contributor to Python, NumPy, and SciPy, and has a doctorate in number theory from Harvard. He’s acutely aware of the difference between contriving data to “prove a point” and presenting simple data typical of a point he’s trying to make. His post included the disclaimer “under suitable assumptions on the distribution of the inputs”, but he didn’t belabor it - the post was long enough without it.
If you think his point is not applicable to typical data, how about you find a study demonstrating that half-up delivers better results than nearest-even on any “real world” data set? Not piles of words, but empirical, non-contrived, evidence. I doubt one exists.
In the meantime, sounds like you’d be happiest with decimal’s “basic” context (as noted above, it uses half-up rounding by default). So use it.
>>> import decimal
>>> decimal.setcontext(decimal.BasicContext)
>>> round(decimal.Decimal("0.285"), 2)
Decimal('0.29')