/money.dart

Dart implementation of Fowler's Money pattern.

Primary LanguageDartMIT LicenseMIT

Money

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

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);

Comparison

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!

Currency Predicates

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

Value Sign Predicates

To check if some money amount is a credit, a debit or zero, use predicates:

  • Money.isNegative — returns true only if amount is less than 0.
  • Money.isPositive — returns true only if amount is greater than 0.
  • Money.isZero — returns true only if amount is 0.

Arithmetic Operations

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)

Allocation

Allocation According to Ratios

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¢

Allocation to N Targets

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

Working with Currency

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.

Directory of Currencies

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]),
  // ...
]);

Money Coding

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".

Encoding

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...

Decoding

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 {
  // ...
}