This is a Dart implementation of the Money pattern, as described in [Fowler PoEAA]:
A large proportion of the computers in this world manipulate money, so it’s always puzzled me that money isn’t actually a first class data type in any mainstream programming language. The lack of a type causes problems, the most obvious surrounding currencies. If all your calculations are done in a single currency, this isn’t a huge problem, but once you involve multiple currencies you want to avoid adding your dollars to your yen without taking the currency differences into account. The more subtle problem is with rounding. Monetary calculations are often rounded to the smallest currency unit. When you do this it’s easy to lose pennies (or your local equivalent) because of rounding errors.
— Fowler, M., D. Rice, M. Foemmel, E. Hieatt, R. Mee, and R. Stafford, Patterns of Enterprise Application Architecture, Addison-Wesley, 2002.
Actual implementation uses BigInt
to represent amount of money in the
smallest subunits of a currency. This enables computation of any arbitrary
amount of money in any currency.
- Creating a Money Value
- Comparison
- Arithmetic Operations
- Allocation
- Working with Currency
- Money Coding
Money
can be instantiated providing amount in the minimal subunits of
currency (e.g. cents):
// Create a currency:
final usd = Currency.withCodeAndPrecision('USD', 2);
// Create a money value:
let fiveDollars = Money.withSubunits(BigInt.from(500), usd);
Equality operator (==
) returns true
when both operands are in the same
currency and have equal amount.
fiveDollars == fiveDollars; // => true
fiveDollars == sevenDollars; // => false
fiveDollars == fiveEuros; // => false (different currencies)
Money values can be compared with operators <
, <=
, >
, >=
, or method
compareTo()
from the interface Comparable<Money>
.
These operators and method compareTo()
can be used
only between money values in the same currency. Runtime error will be thrown
on attempt to compare values in different currencies.
fiveDollars < sevenDollars; // => true
fiveDollars > sevenDollars; // => false
fiveEuros < fiveDollars; // throws ArgumentError!
To check that money value has an expected currency use methods
isInCurrency(Currency)
and isInSameCurrencyAs(Money)
:
fiveDollars.isInCurrency(usd); // => true
fiveDollars.isInCurrency(eur); // => false
fiveDollars.isInSameCurrencyAs(sevenDollars); // => true
fiveDollars.isInSameCurrencyAs(fiveEuros); // => false
To check if some money amount is a credit, a debit or zero, use predicates:
Money.isNegative
— returnstrue
only if amount is less than0
.Money.isPositive
— returnstrue
only if amount is greater than0
.Money.isZero
— returnstrue
only if amount is0
.
Money
provides next arithmetic operators:
- unary
-()
+(Money)
-(Money)
*(num)
/(num)
Operators +
and -
must be used with operands in same currency,
ArgumentError
will be thrown otherwise.
final tenDollars = fiveDollars + fiveDollars;
final zeroDollars = fiveDollars - fiveDollars;
Operators *
, /
receive a num
as the second operand. Both operators use
schoolbook rounding to round result up to a minimal subunit of a currency.
final fifteenCents = Money.withSubunits(BigInt.from(15), usd);
final thirtyCents = fifteenCents * 2; // $0.30
final eightCents = fifteenCents * 0.5; // $0.08 (rounded from 0.075)
Let our company have made a profit of 5 cents, which has ro be divided amongst a company (70%) and an investor (30%). Cents cant' be divided, so We can't give 3.5 and 1.5 cents. If we round up, the company gets 4 cents, the investor gets 2, which means we need to conjure up an additional cent.
The best solution to avoid this pitfall is to use allocation according to ratios.
final profit = Money.withSubunits(BigInt.from(5), usd); // 5¢
var allocation = profit.allocationAccordingTo([70, 30]);
assert(allocation[0] == Money.withSubunits(BigInt.from(4), usd)); // 4¢
assert(allocation[1] == Money.withSubunits(BigInt.from(1), usd)); // 1¢
// The order of ratios is important:
allocation = profit.allocationAccordingTo([30, 70]);
assert(allocation[0] == Money.withSubunits(BigInt.from(2), usd)); // 2¢
assert(allocation[1] == Money.withSubunits(BigInt.from(3), usd)); // 3¢
An amount of money can be allocated to N targets using allocateTo()
.
final value = Money.withSubunits(BigInt.from(800), usd); // $8.00
final allocation = value.allocationTo(3);
assert(allocation[0] == Money.withSubunits(BigInt.from(267), usd)); // $2.67
assert(allocation[1] == Money.withSubunits(BigInt.from(267), usd)); // $2.67
assert(allocation[2] == Money.withSubunits(BigInt.from(266), usd)); // $2.66
Currency
value-type carries the most important information about a currency:
code
and precision
(number of decimal places):
final usd = Currency.withCodeAndPrecision('USD', 2);
print(usd.code); // => USD
print(usd.precision); // => 2
As a value-object, currency can be checked for equality (==
) and used
as a key for map.
Usually you will not instantiate a Currency
each time you need one.
Instead you can have some directory with currencies used in the application.
The interface Currencies
is provided by the package for this purpose:
abstract class Currencies {
/// Returns a [Currency] if found or `null`.
Currency find(String code);
}
NOTE: The method find()
is synchronous! If you need to fetch currency
from a database or external service — make a component with asynchronous API
for fetching a whole directory of currencies at once.
The package also provides a few implementations of Currencies
.
You can instantiate a directory from an Iterable<Currency>
:
final currencies = Currencies.from([
Currency.withCodeAndPrecision('USD', 2),
Currency.withCodeAndPrecision('EUR', 2),
Currency.withCodeAndPrecision('BTC', 8),
Currency.withCodeAndPrecision('ETH', 18),
// ...
]);
Or aggregate other directories:
final currencies = Currencies.aggregating([
Currencies.from([usd, eur]),
Currencies.from([btc, eth]),
// ...
]);
API for encoding/decoding a money value enables an application to store value in a database or send over the network.
A money value can be encoded to any type. For example it can be coded as a string in the format like "USD 5.00".
class MyMoneyEncoder implements MoneyEncoder<String> {
String encode(MoneyData data) {
// Receives MoneyData DTO and produces
// a string representation of money value...
}
}
final encoded = fiveDollars.encodedBy(MyMoneyEncoder());
// Now we can save `encoded` to database...
class MyMoneyDecoder implements MoneyDecoder<String> {
Currencies _currencies;
MyMoneyDecoder(this._currencies) {
if (_currencies == null) {
throw ArgumentError.notNull('currencies');
}
}
/// Returns decoded [MoneyData] or throws a [FormatException].
MoneyData decode(String encoded) {
// If `encoded` has an invalid format throws FormatException;
// Extracts currency code from `encoded`:
final currencyCode = ...;
// Tries to find information about a currency:
final currency = _currencies.find(currencyCode);
if (currency == null) {
throw FormatException('Unknown currency: $currencyCode.');
}
// Using `currency.precision`, extracts subunits from `encoded`:
final subunits = ...;
return MoneyData.from(subunits, currency);
}
}
try {
final value = Money.decoding('USD 5.00', MyMoneyDecoder(myCurrencies));
// ...
} on FormatException {
// ...
}