proposal-decimal
proposal-decimal copied to clipboard
What library features should BigDecimal have?
On BigDecimal.prototype, what sorts of methods should we have, for mathematical calculations? toPrecision, toExponential and toFixed (analogous to Number) seem like givens. Other possibilities:
quantum?compareTotal? (discussed in 11-year-old es-discuss threads)partition?divmod?div? (discussed in #13)round?sqrt? trig fns?- Others? (Add a comment with a suggestion!)
For some use cases it would be very useful to have BigDecimal.prototype.decimalPlaces. Note that decimalPlaces methods from BigNumber.js and from Decimal.js will return 0 for .00000 as 0 and .00000 are indistinguishable in those libraries.
round, floor and ceil would be really useful. BigDecimal keeps number of decimal places, so all of them should accept parameter to get necessary precision. It could be a number of decimal places itself (or integer part if negative), i.e.
BigDecimal(.1234567).round(4) === 0.1235m;
// or
Object.is(BigDecimal(123.4567).round(-1), 120m);
, or an exponent parameter
BigDecimal(.1234567).round(-4) === 0.1235m;
// or
Object.is(BigDecimal(123.4567).round(1), 120m);
The latter seems more intuitive to me.
Thinking again, number of decimal places could be more intuitive if we'll have decimalPlaces method
// `round` takes a number of decimal places
bd1.round(bd2.decimalPlaces()); // instead of `-bd2.decimalPlaces()`
// also, `bd2.decimalPlaces` getter?
Alternatively we can introduce precision (bikeshed the name)
// `round` takes an exponent
bd1.round(bd2.precision()); // or `bd2.precision` getter
If we have a round method, should we have options for different modes for how to round .5? (e.g., half goes up, half goes down, half goes to even?)
Yes, rounding modes are very useful indeed!
For reference:
Can you say more about use cases for these that you're aware of? I can see that different Decimal libraries have decided to include different numbers of rounding modes. Which should we include?
HALF_UP mode (which is default rounding mode in Decimal.js) could be useful for charts (like candle chart) to make them look better. Math.round in JavaScript uses what's named HALF_CEIL in Decimal.js (this makes -0.5 round to -0), which is not very useful in fintech IMO, but should be present as well. Both UP and DOWN are often used for rounding ask/bid, order price, etc. (though this could be handled in different way with just floor/ceil and additional condition with order side or number sign). I've never used HALF_EVEN AFAIR.
HALF_EVEN useful when you should do several intermediate roundings between operations. Because it has cumulative result to be a true average, and not skewed up or down. Also it uses in statistics and other math. Btw WebAssembly always round-to-nearest ties-to-even which in average has less rounding errors for cumulative operations as I mentioned before.
Well, JS Number arithmetic operations round half-even, so maybe you have used it!
abs is another frequently used function
Well, JS Number arithmetic operations round half-even, so maybe you have used it!
@littledan I didn't knew about that. Could you provide some example to illustrate this?
@qzb it's in the IEEE 754 standard, I believe
btw Math.round is not round half-even, it's just equivalent to Math.floor(x + 0.5) but f64.nearest/f32.nearest in WebAssembly is really rounding to half-even.
I made little snippet to demonstrate this with emulation nearest:

[-2.6, -2.5, -1.5, -1.4, 0, 1.4, 1.5, 2.5, 2.6].map(x => Math.floor(x + 0.5)).reduce((a, b) => a + b, 0)
> 2
// same as Math.round
It's not about Math.round, but rather all operations normally. For example, see the definition of multiplication on Numbers, whose last bullet point mentions
the product is computed and rounded to the nearest representable value using IEEE 754-2008 roundTiesToEven mode.
I think this mode will be important if we want to support high precision numerical calculations, to reduce errors.
QuickJS's 2020-01-05 release includes sqrt and round. The current README mentions pow, which QuickJS omits in that version. In personal communication, Fabrice Bellard writes,
The generic case of the power operator ("**") is quite difficult to implement (it is the most complicated transcendental floating point operation). If you keep the infinite precision design, I suggest to only support positive integer exponents because it corresponds to iterated infinite precision multiplications. Negative integer exponents could be added provided the division is exact.
Including a generic BigDecimal.pow() significantly complicates the implementation because you need exp() and log() to implement it (so you can include BigDecimal.exp and BigDecimal.log at no addional cost !). Then you introduce the question of correctly rounding it or not (correctly rounding is more complicated to implement but gives deterministic results which is good for testing).
If you want to keep the implementation as simple as possible you should omit ** and BigDecimal.pow.
Currently QuickJS currently only supports positive integer exponents in ** and has no support for BigDecimal.pow or other transcendental functions.
For completeness, QuickJS supports BigDecimal.add/sub/mul with an optional rounding object as for the division. It allows to bound the memory if the user needs it. As for BigDecimal.sqrt, maximumFractionDigits is not supported but could be added.
Personally, with this feedback, I'm leaning towards including add/sub/mul as well as pow/exp/log and sqrt, unless we find that the implementations will be way too difficult. Of course everything would be defined to give correct rounding. I'd omit the restricted form of **.
What do you mean, omit the restricted form? Are you referring to fabrice’s suggestion of only positive exponents?
@ljharb Yes.
Of the calculation methods in the current proposal, perhaps the only exotic one is the "partition" method. I haven't seen a method like that in any arbitrary-precision number library, nor does a similar method appear in the General Decimal Arithmetic Specification or any other specification or standard I am aware of. Usually some kind of remainder method or a divide-and-remainder method (e.g., in Java's BigDecimal) occurs in libraries and specifications instead.
In any case, the "partition" method, if its definition is as suggested in https://github.com/tc39/proposal-decimal/issues/13#issuecomment-554771029, could get unwieldy and take unnecessary memory if the number of partitions gets very large.
I'd like to offer some support for square roots as an "advanced" function that ought to be included in the standard library.
One reason for including sqrt (in the context of our leaning toward Decimal128 as the underlying data model for Decimal) is that IEEE 754 actually lists sqrt as a non-optional function to be implemented. (This is not, by itself, a knock-down argument for including sqrt. But I think we should value the years -- decades? -- of research that went into IEEE 754 Decimal128/Decimal64/etc. Surely they have done a lot of analysis of use cases for decimal than we have and there's a good reason why sqrt is in the "must implement" list.)
Another reason: although the bulk of the feedback we've received from JS developers suggests that basic arithmetic (addition, multiplication, etc.) is likely to be sufficient, square roots do indeed pop up occasionally. They are used for computing distances and indeed presenting such numbers for human consumption.
I'd like to argue that we don't need trigonometric functions in Decimal.
The Math object already has the (hyperbolic) trig functions. If one wants to compute the cosine of a Decimal value, the way to do it would be to convert the Decimal value to a string, cast that to a Number, call Math.cos on that, get enough decimal digits of that string by using toFixed, and then (if necessary) convert that string into a Decimal. If one is worried about any possible rounding errors, just call toFixed with an extra decimal digit (or two, to be really safe).
That sounds like a lot of juggling, but it works. I'm having trouble seeing the added value of trig functions in Decimal, given that they already exist out-of-the-box in Math, and are (I assume) battle-tested.
From a mathematical point of view, the trig functions are (almost) never going to produce an exact decimal value (which are all rational numbers). (By the way, this is as true of BigDecimal as it is of Decimal128.) Thus, the exact semantics that Decimal offers is a bit of an illusion when it comes to such functions. The computation of such functions, whether in decimal or binary floats, is going to have to stop after a certain number of digits, and the result is a best-effort approximation.
What are some use cases for trig functions that would require, say, 20 or more decimal digits of precision (something that we could get with Decimal128)? In other words, do Math's trig functions suffer from a problem that could be solved in Decimal?
Just extending my previous argument against the trig functions:
I think Decimal also ought to exclude logarithms (whether natural or base ten) and exponentiation (whether natural exponentiation or a two-argument pow variant). The reasoning for excluding them is the same: battle-tested support already exists for these in Math and I struggle to imagine use cases where one needs even more precision (digits) than are on offer from JS's 64-bit binary floats.
One counterargument I can entertain is that exponentiation and logarithm do show up in some business/financial calculations, such as computing compound interest of an investment, economic growth/decay formulas, and so on. But, again, the counterargument would be: does Math's version of these functions, working with JS Numbers, deliver insufficient accuracy?
Exponentiation is very critical, and has an operator, **. I don’t see how it can be excluded.
does Math's version of these functions, working with JS Numbers, deliver insufficient accuracy?
It's not just a question of accuracy, but of IEEE-754 confusion, especially in the world of finance. Enough so that there are dozens of user-land libs that attempt to address the problem.
The issue is especially prevalent with crypto-currency, where figures regularly sit well beyond the upper-bounds of JavaScript's Number type.
Exponentiation is very critical, and has an operator, **. I don’t see how it can be excluded.
One version that I think would be valuable would be to have exponentiation for integer exponents (negative or not). It's a pretty straightforward implementation.
Exponentiation for non-integer exponents is a bit more exotic. There are some use cases in business/finance calculations. But the question, to my mind, would be whether a Decimal version of this would have any added value compared to Math.exp.
does Math's version of these functions, working with JS Numbers, deliver insufficient accuracy?
It's not just a question of accuracy, but of IEEE-754 confusion, especially in the world of finance. Enough so that there are dozens of user-land libs that attempt to address the problem.
The issue is especially prevalent with crypto-currency, where figures regularly sit well beyond the upper-bounds of JavaScript's
Numbertype.
Quite right! Decimal does aim to provide a big improvement over JS's Number. I'm looking forward to a day where we can handle a lot of digits, secure in the knowledge that we're working with them correctly. What I had in mind, in my recent contributions to this issue, was whether the Decimal API should include more "exotic" things like exponentiation (with non-integer exponents), logarithms, and trigonometric functions. My gut intuition is that, for cryptocurrency applications, it's enough to (1) be able to represent decimals accurately, and (2) do basic arithmetic. My hunch is that such applications probably wouldn't need trigonometric functions, etc. I guess crypto applications are basically no different from other finance/business calculations, the only difference being that many more digits need to be supported. And that's what Decimal definitely will give us. But thinking about mathematical functions beyond basic arithmetic that should be available: If you can give me some pointers which would be reason to believe that trig, logarithms, and exponentiation are used in cryptocurrency, please do let me know!
One version that I think would be valuable would be to have exponentiation for integer exponents
During BigInt, multiple delegates made clear their extreme discomfort with surprising value-dependent semantics for number operations, so I'm not sure if this would be viable.
One version that I think would be valuable would be to have exponentiation for integer exponents
During BigInt, multiple delegates made clear their extreme discomfort with surprising value-dependent semantics for number operations, so I'm not sure if this would be viable.
Just to reformulate: Do you mean that we ought to support a ** b for all Decimal values a and b (which is a valid suggestion), or not support exponentiation at all (because the "easy" approach of restricting the second argument to integers would likely encounter resistance)?
The former, since decimal numbers without exponentiation seems untenable to me.
The line of reasoning make sense. I'm curious to know why a version of Decimal without exponentiation would be untenable.
I find myself conflicted about whether Decimal needs things like exp, log, sqrt, and trig functions. Part of me says "hell yeah!" Another part of me looks at the JS developer survey data and finds that there's very little expressed need for those.
What's your take on the argument that, if one needs exp, log, etc., just use Math? To my eyes, the only value that Decimal could add, concerning those functions, would be that you would get more decimal digits from the results. But do we really need, say, 25+ decimal digits for a call to exp? If we use Math, we get a pretty good result, albeit with somewhat fewer decimal digits. (The argument is restricted to "advanced" functions where an exact result is, in general, unavailable, because the values are almost always irrational numbers. I'm not talking about addition, multiplication, etc., where binary floats typically yield inexact results whereas Decimal yields exact results.)
The primary benefit to me for Decimal is if it can replace Number entirely.