This is kind of a merger of other issues, in particular #3 (Scales and Money classes) and #4 (Money contexts), that happen to be tightly related.
I figured we've reached a point where we have a common view of a few things, and several conflicting ideas on others. I'll try to summarize the current status of the brainstorming.
I hope this issue will help us find some common ground.
What we (seem to) agree on
- There should be a single Money class (no Money/BigMoney split)
- A Money is composed of a BigDecimal and a Currency
- the BigDecimal can have any scale, as required by the application
- the Currency is composed of a currency code, and a default scale; this scale can be used to provide a default scale in factory methods, and dictates how some methods such as
getAmountMinor()
behave, independently of the scale of the Money itself
- A Money should never embed a rounding mode, the caller of an operation on a Money should always be in control of rounding
- Operations on a Money should be able to round by steps, to support cash increments of 1.00 units (step 100, CZK) or 0.05 units (step 5, CHF); steps for ISO currencies will not be hardcoded in the library, and will be left up to the developer
- Some use cases can require several consecutive operations on monies, where you do not want to apply any kind of scaling or rounding until the very last step; we should therefore provide a calculator based on BigRational, that doesn't take any scale, step or rounding mode until the
getResult()
method is called
What we disagree on
Actually there is just one main friction point: operations. Should all operations require a full context (target scale, step, and rounding mode), or should a Money instance dictate what the result scale & step are?
I tried to summarize your points of view here, feel free to correct me if I'm wrong:
-
@jkuchar and I like the idea of storing the step in the Money itself. All operations on a Money would yield another Money with the same scale & step capabilities:
$money = Money::of(50, 'USD'); // USD 50.00
$money = $money->dividedBy(3, RoundingMode::HALF_UP); // USD 16.67
$money = Money::of(50, 'CZK', /* step */ 100); // CZK 50.00
$money = $money->dividedBy(3, RoundingMode::DOWN); // CZK 16.00
$money = $money->dividedBy(3, RoundingMode::DOWN); // CZK 5.00
The rationale behind this is that usually, an application deals with a fixed scale for a given currency (e.g. the currency's default scale for an online shop, or a higher scale for a Forex trading website), and the need to juggle scales in the middle of a series of operations seems very unusual. I personally feel like the need for a sudden change of scale might be an indicator that you're trying to do something that would be a better fit for a BigRational-based calculator.
Note that we could allow an optional context to be provided, to allow overriding the current scale & step. This would just not be mandatory.
Critics include the fact that plus()
may throw an exception when adding an amount whose scale is larger than that of the left operand, instead of returning a Money with an adjusted scale (as BigDecimal would do), and that the result depends on the order of the operands (USD 1.20 + USD 0.1 = USD 1.30
while USD 0.1 + USD 1.20 = USD 1.3
, and USD 1.21 + USD 0.1 = USD 1.31
while USD 0.1 + USD 1.21 = Exception
). I replied here.
-
@jiripudil is not against this behaviour, but suggests that we'd throw an exception when adding together monies of different scales & steps. I replied here.
-
Finally, @VasekPurchart sees Money as an "anemic" value object, that ideally would not contain any operations. He's not fully against having operations on Money though, but in this case suggests that all operations would have to provide the full context: scale, step, and rounding mode. (Note: this is pretty much what we have today and what I was trying to get away from).
What others are doing
This is just an overview of what I could investigate in a reasonable timeframe. If you know other libraries that you think deserve to be mentioned, please let me know.
This PHP library offers a single class, Money, that only stores amounts in integer form, so in "minor units". Currencies are only defined by a currency code. No scale is involved. Multiplication and division take an optional rounding mode, defaulting to HALF_UP.
This popular library offers two implementations, Money
and BigMoney
:
- Money always has the default scale for the currency. The result of an operation always has the same scale:
plus()
must add an amount that is compatible with this scale or you get an exception, and dividedBy()
must provide a rounding mode.
- BigMoney has a flexible scale.
BigMoney.plus()
adjusts the scale of the result, effectively acting like a BigDecimal with a Currency: USD 25.95 + 3.021 = USD 28.971
. BigMoney.dividedBy()
returns a BigMoney with the same scale as the left operand: USD 1.00 / 3, rounded DOWN = USD 0.33
. You cannot choose the scale of the result.
This is the new money interface that is now part of Java from version 9 onwards. Java 9 is due to be released tomorrow, 21 September 2017; you can hardly dream of a fresher API! It's been created by a team of experts, several of them working for Credit Suisse.
This API defines a MonetaryAmount interface that Money classes must implement. MonetaryAmount instances embed a MonetaryContext that defines "the numeric capabilities, e.g. the supported precision and maximal scale, as well as the common implementation flavor."
According to the source code documentation, operations like add(), multiply() and divide() take a single argument, the operand. The result's scale is adjusted just like a BigDecimal would do, but an exception can be thrown if the scale of the result exceeds the max scale defined by the context.
The reference implementation, Moneta, provides several classes:
- Money, based on BigDecimal
- FastMoney, based on
long
- RoundedMoney, based on BigDecimal like Money, but additionally embedding a custom rounding implementation, that is applied to every operation result. The resulting monies, in turn, embed this rounding for further calculations.
I gave Moneta a try:
CurrencyUnit usDollar = Monetary.getCurrency("USD");
BigDecimal amount = new BigDecimal("100.00");
System.out.println(Money.of(amount, usDollar).divide(3));
System.out.println(FastMoney.of(amount, usDollar).divide(3));
INFO: Using custom MathContext: precision=256, roundingMode=HALF_EVEN
USD 33.33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
USD 33.33333
These defaults are plain nonsense if you ask me.
Monies have the scale of the number they were instantiated with. Adding two monies of different scales will return a Money with the maximum of the two scales. Dividing a Money will return a number with a fixed number of decimals, rounded:
>>> m = Money("100.0", "USD");
>>> m
USD 100.0
>>> m + Money("1.23", "USD");
USD 101.23
>>> m/3
USD 33.33333333333333333333333333
Ping @martinknor, @fortis and @fprochazka again, now is the time to make your voice heard before the first beta release!