Search code examples
javacurrencyjava-moneymathcontext

MonetaryAmount with fixed scale


In my application at hand, I have to work with a REST/JSON API that expects monetary values of a given maximum scale; e.g.; all monetary values exchanged via this web API must have at most, say, four decimal digits.

To implement the API, I use Kotlin data classes in which I represent monetary values using the Money implementation of the JSR-354 MonetaryAmount API. Is there a possibility to customize the money type to accept a certain maximum scale only and fail otherwise?

That is, when creating a monetary value from a number or string, like Money.of("200.20234", "EUR") I would like this construction to fail when the input has more than the admissible number of decimal digits. Similarly, I would like arithmetic operations to fail if they result in more decimal digits than prescribed; e.g., when calculating a percentage of the given monetary value, where the resulting value would result in more decimal digits than configured. Neither do I want implicit rounding nor automatic scale extension (more decimal digits). Is there a way to configure a MonetaryContext or underlying MathContext to achieve this?


Solution

  • That is, when creating a monetary value from a number or string, like Money.of(200.20234, "EUR") I would like this construction to fail when the input has more than the admissible number of decimal digits.

    I'm interpreting your question as: "I would like problematic inputs to be rejected". Taking that very slight step in the dark, then we have a problem:

    Your entire API is fundamentally broken here - double just cannot be the way to convey monetary units, period.

    doubles are represented by 64 bits. That means, by fundamental informatic principles, there are only at most 2^64 (that's a number with ~19 digits) unique values that double could possibly represent. And that's a problem; the number line that doubles are designed to represent have an infinite number of values between 0 and 1, let alone between minus infinity and plus infinitive, and 'an infinite amount of values' is a lot more than '1000000000000000000 values'.

    The way double solves this seeming mathematical impossibility is via the blessing system: A specific set of numbers (Slightly less than 2^64 of em) are 'blessed' - representable as double values. All other numbers aren't blessed. double math works by silently rounding every non-blessed value to its nearest blessed value, every time, at every step, silently, and without any possibility to report on when this occurs or by how much the number is being rounded (after all, the amount it was rounded by is itself unlikely to be blessed either).

    Blessed numbers aren't uniformly distributed; there are as many between 0 and 1 as there are between 1 and infinity, for example. As you move away from 1, there are fewer and fewer. Around 2^52, the distance between 2 blessed numbers is larger than 1. This all results in silent errors. Here are 2 specific examples that should immediately convince you that any attempt to mention double in the same project as "money" is best countered by immediately running for the hills, you WILL MESS UP if you ever do this, the only answer is to completely ditch that constructor:

    System.out.println("Did you know 0.1 * 10 is not equal to 1.0?");
    double v = 0.0;
    for (int i = 0; i < 10; i++) v += 0.1;
    System.out.println("Here, have a look: " + (v == 1.0));
    // The above prints FALSE!
    
    long z = 123456789123456789L;
    double x = z;
    System.out.println("Did you know adding 1 to a number doesn't do anything?");
    double y = x + 1;
    System.out.println("Here, have a look: " + (x == y));
    // The above prints TRUE!
    

    There are no exceptions and no methods you can use that easily detect 'uhoh, due to rounding-to-nearest-blessed-number, a rather significant error has been introduced'.

    Specifically for your case, the key question you have to ask yourself is:

    • What is the smallest amount of money such that representing that in the form of a double will get auto-rounded to a different amount of money? - and the answer is probably something around 1234567891234567.89:
    double z = 1234567891234567.89;
    System.out.printf("%.2f\n", z);
    

    This prints: 1234567891234568.00

    SILENTLY!

    Hence, we get to the conclusion:

    If you want this method to reject unsafe input, you need to reject a lot more than any number with more than 2 fractional digits.

    So what do I do instead?

    Ditch double entirely. All monetary units have an atomic unit. For euros and dollars, it's called a 'cent'. For yen it's simply yen. For pounds, its pennies. For bitcoin, its satoshis. For the indian rupee, its paisa (100 paisa = 1 rupee). And so on.

    Most systems are not capable of representing half of an atomic unit. For example, you simply cannot transfer half a cent to a bank account. Hence, the best way to do currency in any application is to limit all operations to this atomic unit - i.e. if you are writing software to divide an account's balance amongst 3 accounts (say, that account is jointly owned and is being disbanded), and it contains 1 euro, the answer is not to give everybody 33 + 1/3 of a eurocent. The answer is instead one of the following:

    • Everybody gets 33 cents; the bank keeps 1 cent.
    • Everybody gets 34 cents; the bank loses 2 cents.
    • The bank uses a random algorithm to choose an arbitrary owner. They get 34 cents; the other 2 get 33 cents.
    • Same as above but instead the account with the lowest account number 'wins' and gets that final cent.
    • The process (e.g. some form submission on a website - the 'disband this account and distribute all funds equally to its owners according to their share' button) should crash, telling the user that this operation cannot be done; they will have to ensure the amount available is exactly divisible amongst the accounts.

    Nothing, not even BigDecimal, can automate this for you: You'd have to write it.

    Hence, the best way to represent monetary amounts is usually as a long or possibly, if you want to represent trillions upon trillions, as a BigInteger. Common advice is to use BigDecimal - this advice is generally wrong. It's very complex to do this and doesn't get you much: You can now represent half a cent but you've now kicked the can down the road in two ways:

    • This does not solve the issue where any interaction with any other system cannot deal with fractions of cents, so you have to round at that point - you haven't gotten around the need to decide on algorithms to deal with fractionals at all.
    • BigDecimal can't deal with a third of a cent either (try it - divide 1 by 3 in BigDecimal. That operation crashes unless you specify a rounding mode, in which case you're still rounding. If you're going to round, round to the atomic unit).

    The only place BigDecimal should come in for monetary stuff is if you really really know what you are doing and have a well defined sensible need + process to represent fractions of atomics, or, very large factors; for example, in foreign exchanges, it can make sense to go from long (containing eurocents) to a BD value, multiply it by the foreign exchange rate (also a BD instance), then go back to cents, rounding as the bank's processes explain (presumably, round in the bank's favour).

    Thus:

    Money m = Money.of(12345, Currency.EUR);
    System.out.println(m);
    

    should be your API, that should print: "€123.45", and your API's signature is:

    public static Money of(long units, Currency currency)
    

    Where Currency is an enum or similar concept that encodes for example how many atomics per unit, and how to render them. As an example of this, it is customary in india to render e.g. a million rupee as:

    ₹10.00.000
    

    You'd have to encode that logic with the currency, you can't write a single generalized method that can do this.