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

What library features should BigDecimal have?

Open littledan opened this issue 6 years ago • 31 comments

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!)

littledan avatar Nov 13 '19 19:11 littledan

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.

chicoxyzzy avatar Nov 14 '19 03:11 chicoxyzzy

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.

chicoxyzzy avatar Nov 14 '19 13:11 chicoxyzzy

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

chicoxyzzy avatar Nov 14 '19 14:11 chicoxyzzy

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?)

littledan avatar Nov 14 '19 15:11 littledan

Yes, rounding modes are very useful indeed!

For reference:

chicoxyzzy avatar Nov 14 '19 18:11 chicoxyzzy

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?

littledan avatar Nov 14 '19 18:11 littledan

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.

chicoxyzzy avatar Nov 14 '19 19:11 chicoxyzzy

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.

MaxGraey avatar Nov 14 '19 19:11 MaxGraey

Well, JS Number arithmetic operations round half-even, so maybe you have used it!

littledan avatar Nov 14 '19 19:11 littledan

abs is another frequently used function

chicoxyzzy avatar Nov 14 '19 19:11 chicoxyzzy

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 avatar Nov 15 '19 13:11 qzb

@qzb it's in the IEEE 754 standard, I believe

chicoxyzzy avatar Nov 15 '19 13:11 chicoxyzzy

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: rounding

[-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

MaxGraey avatar Nov 15 '19 14:11 MaxGraey

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.

littledan avatar Nov 15 '19 16:11 littledan

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 **.

littledan avatar Jan 08 '20 17:01 littledan

What do you mean, omit the restricted form? Are you referring to fabrice’s suggestion of only positive exponents?

ljharb avatar Jan 08 '20 18:01 ljharb

@ljharb Yes.

littledan avatar Jan 08 '20 18:01 littledan

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.

peteroupc avatar Apr 22 '20 04:04 peteroupc

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.

jessealama avatar Aug 29 '23 13:08 jessealama

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?

jessealama avatar Aug 29 '23 14:08 jessealama

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?

jessealama avatar Aug 29 '23 14:08 jessealama

Exponentiation is very critical, and has an operator, **. I don’t see how it can be excluded.

ljharb avatar Aug 29 '23 14:08 ljharb

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.

shuckster avatar Aug 29 '23 15:08 shuckster

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.

jessealama avatar Aug 30 '23 09:08 jessealama

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.

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!

jessealama avatar Aug 30 '23 09:08 jessealama

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.

ljharb avatar Aug 30 '23 18:08 ljharb

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)?

jessealama avatar Aug 31 '23 09:08 jessealama

The former, since decimal numbers without exponentiation seems untenable to me.

ljharb avatar Aug 31 '23 14:08 ljharb

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.)

jessealama avatar Sep 01 '23 07:09 jessealama

The primary benefit to me for Decimal is if it can replace Number entirely.

ljharb avatar Sep 01 '23 14:09 ljharb