decimal128 icon indicating copy to clipboard operation
decimal128 copied to clipboard

arithmetic operator functions are missing

Open JohnAD opened this issue 4 years ago • 16 comments

  • [ ] == Equality. Specifically, an equality check that ignores significant digits. For equality checks that take significance into account, there is already the === operator.
  • [ ] != Not Equality.
  • [x] + Addition.
  • [x] - Subtraction.
  • [ ] * Multiplication.
  • [ ] / Division.
  • [ ] div Integer Division.
  • [ ] mod Modulo.
  • [ ] < Less-than.
  • [ ] <= Less-than-or-equal.
  • [ ] > Greater-than.
  • [ ] >= Greater-than-or-equal.

While encoding/decoding and simple import/export from native data types is supported, the basic ability to do math with the library is missing.

These functions cannot resort to simply converting to either float64 or int64 since those types lack both the precision and the needed scale for the math to properly work.

I've started a document that explains, in the context of this library, how significance and rounding should be handled. See https://github.com/JohnAD/decimal128/blob/master/explaining-op-significance.rst I'm not a mathematician and all coments are welcome regarding it.

JohnAD avatar May 09 '20 03:05 JohnAD

See I had to do this to solve management issues nim_dcml

how to define the specificity of a dcml (8,2) 10 digit area including 2 precision

your approach is interesting

AS400JPLPC avatar May 10 '20 01:05 AS400JPLPC

@AS400JPLPC

how to define the specificity of a dcml (8,2) 10 digit area including 2 precision

your approach is interesting

Thanks!

I been thinking of adding a "scale=n" optional parameter to the newDecimal128 procedure to make it easier for programmers that want to set a specific precision based on the digits after the decimal point.

  let a = newDecimal(14, scale=2)

  assert $a == "14.00"
  assert a.getPrecision == 4
  assert.a.getScale == 2

  let b = newDecimal("1E6", scale=2)

  assert $b == "1000000.00"
  assert b.getPrecision == 9
  assert b.getScale == 2

  let c = newDecimal("3.1493", scale=2)
  assert $c == "3.15"
  assert c.getPrecision == 3
  assert c.getScale == 2

Essentially, the number of significant digits would be expanded or shrunk to the number needed; regardless of the input number.

I suspect this would make the library easier to use by programmers writing financial software.

JohnAD avatar May 10 '20 21:05 JohnAD

YESSSSSS ;) because it is essential to define dp but also to have the possibility of making dp "round" or not "round"

I had cases or with Renault for example on thousands of invoices (the invoices were read mechanically and the programs refused rounding that was a big debate with signatures etc.) It was necessary to truncate and others or the rounding had to be done. Big companies play with this ... (ref: OS AS400 took this problem into account) With that you should cover a very large number of cases. Even if it requires you to add code I think it's necessary

AS400JPLPC avatar May 11 '20 08:05 AS400JPLPC

Yet another example: I also worked in the banking sector. The dp is set to 15 and then brought back to 2 and a lot of figures are made on the calculations that are not humanly possible on the% etc. they make a lot of money. But when you work you have to be as fair as possible and work with real values then truncated for the first time and round up but on millions of transactions this represents a lot of money.

AS400JPLPC avatar May 11 '20 08:05 AS400JPLPC

Allowing other rounding algrorithms (or simply truncation) should be possible, but it is challenging from the point of view of operators since there is no way to add arguments. For example:

proc add*(left, right: Decimal128, rounding: RoundingMethod = rmBankers): Decimal128 =
  # ...

let answer = add(a, b, rounding = rmTruncate)

would be easy to do, but with:

proc `+`*(left, right: Decimal128): Decimal128 =
  # ...

let answer = a + b

there is no way to add a parameter to the plus (+) symbol.

Nor would I want to use a global variable at run-time as that makes threading unsafe.

Just thinking out loud....

Perhaps default to bankers rounding (round-to-even), but allow for a macro that changes any scoped code to change the method. For example,

let a = newDecimal("1.00", scale=2)
let b = newDecimal("6.135")

assert $(a + b) == "7.14"  # the "3" in "7.135" is not even, so it is rounded up

useRoundingMethod(rmTruncate):
  assert $(a + b) == "7.13"  # the rounding method is simple truncation

And then perhaps allow the global default to be overridden as a compiler flag:

$ nim c -d:decimal_rounding_method=rmTruncate example.nim

In fact, as a safety, the code could contain a compile-time check:

  when decimal_rounding_method != "rmTruncate":
    raise newException(Error, "please compile this program with '-d:decimal_rounding_method=rmTruncate'"

JohnAD avatar May 11 '20 22:05 JohnAD

Thank you for answering

reflection: I would be you, I would not add a variable in the description as you do, but I would give the possibility of adding a "settrunc" or "setround" and I would store as attribute the only addition is the dp scale part it would not give you much modification because it is only when you extract the value that it has an impact the internal calculations, for the calculations you should not touch it

this is how I worked and I never had to complain, the tax auditors were amazed during the changeover for the euro on millions of transactions I only had a difference of 3cts well below tolerance

I made this principle in a software that I develop designer type ncursw for terminal. I added variables, but finally from account to use it is better to make "set ..."

Differentiate between definition variable and attribute

My English is googled i hope you can understand

AS400JPLPC avatar May 12 '20 09:05 AS400JPLPC

I'm still speculating. Perhaps I could create a subclass for specific currencies. This would have the benefit of enforcing best practices. Perhaps something like:

import decimal128
import decimal128/EUR

let a = newEUR(1)
let b = newEUR("6.135")
let c = newEUR("2E3")

assert $a == "1.000000000000000"
assert $b == "6.135000000000000"
assert $c == "2000.000000000000000"

let answer = a + b + c

assert $answer = "2007.135000000000000"
assert answer.getScale == 15

var finalAnswer = answer.truncate     # returns a Decimal128 not a EUR
assert $finalAnswer == "2007.13"

finalAnswer = answer.round            # returns a Decimal128 not a EUR
assert $finalAsnwer == "2007.14"

finalAnswer = answer.round(0)
assert $finalAnswer == "2007"

The idea being that each currency (EUR, USD, etc.) would have it's own standards. And, because they are different types, mixing currencies does not work; reminding the programmer that they need to convert currencies to do math between them.

Since Decimal128 has 34 possible digits, using 15 for the fractional part leaves 19 for the whole number. So the EUR type would be limited to about ten quintillion Euros. That is probably plenty. :smile:

JohnAD avatar May 12 '20 16:05 JohnAD

do not only see the financial history, there are also quantity problems I buy 2 tonnes of galvanized steel and I make screws, I must know the cost of a screw, compared to the weight with all the propagation external costs, and value all this, the screw does not even weigh 2grm I have to make quotes I can not imagine you only solve the financial problems, and especially that I sell in trade € 15 for 6 screws. ..

If you solve the problem for the dp the rest for everyone to make their cake ... rest assured if I was younger mdrrr I will have done another job;) mdrrrr

let a = newDecimal("1.00", scale=2) ;)

there are also all the stories of unit conversion 1000 containers .... how many watches, what is the distribution, knowing that I buy them by weight and sell them by unit I worked in the printing press, everyone knows that the ink has a cost, a weight of paper which also its important, we had Lepton, Unilever, they worked, at the smallest unit, the whole problem was know what was needed as a raw material ... There I do not tell you the mantissa, excel impossible, and the market was for colossal sums, the weight for example of a label, and us to know if it was profitable , everything was a story of scale without loss of value QT / € ... etc

AS400JPLPC avatar May 12 '20 22:05 AS400JPLPC

I think you understand, in addition if you offer something as much as it covers a maximum of possibility. I find your thing good because in pure nim

AS400JPLPC avatar May 12 '20 22:05 AS400JPLPC

Addition and subtraction now supported.

JohnAD avatar Jun 14 '20 19:06 JohnAD

cela évolue // it evolves ;)

AS400JPLPC avatar Jun 15 '20 11:06 AS400JPLPC

@JohnAD Nice library, Thank you.

Any near future plans about multiplication?

inv2004 avatar Dec 15 '20 22:12 inv2004

Started working on it. Thanks for the prompt! I'm also thinking of renaming "precision" to "significance" as too many other decimal libraries mis-use the term as an indicator of how many digits are after the decimal place.

I'm also going be adding the concept of "newPerfectDecimal()" which is a decimal with infinite significance. An integer quantity or a mathematical constant would be a perfect examples of that.

JohnAD avatar Dec 20 '20 19:12 JohnAD

@JohnAD Thank you.

What do you think about https://github.com/status-im/nim-decimal comparable with the lib? Because I migrated to it (maybe temporary :) )

inv2004 avatar Dec 20 '20 20:12 inv2004

It is a good library and I'm familiar with it. If I didn't need the IEEE 754-2008 compliance, I'd probably be using it.

There are pros/cons to both.

The good parts of nim-decimal:

  • It is arbitrarily sized. It can store the number in 64/128/256 bit form automatically.
  • It is an older library with regular development. status-im is a company that is not likely going anywhere and they regularly work on it.
  • It supports fixed scaling. (Which they call precision, even thought that word does not mean that. Most libraries do that, so it is no big deal I suppose.)
  • It uses mpdecimal C library underneath, which is the one used by Python 3.3+. As such, it is highly optimized. It will probably always be faster than this library.

The good parts of this library:

  • It is imported/exported in the fixed 128-bit IEEE sub-spec used by BSON and some DB libraries.
  • It supports both forms of "precision": scaling (numbers past the decimal place) and significance (the number of significant digits.)
  • Pure nim. It does not rely on a chip-specific C-library.

The big downsides of this library:

  • I've not finished writing the arithmetic parts of it yet. Not having multiply and divide is a big problem.
  • It only supports "bankers rounding" aka "HALF_EVEN". Adding other forms of rounding will be easy, but I've not written it yet.

For the moment, I recommend sticking with nim-decimal if you are making something for production use until I at least have multiplication, division, and value-based (==) equality done.

JohnAD avatar Dec 20 '20 22:12 JohnAD

Hello, I would like you to complete the division and == please it would be a shame to leave it in this state

AS400JPLPC avatar Jul 07 '22 21:07 AS400JPLPC