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?
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.
double
s 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:
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.
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:
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:
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.