money 1.0.0-alpha.1 copy "money: ^1.0.0-alpha.1" to clipboard
money: ^1.0.0-alpha.1 copied to clipboard

outdated

Dart implementation of Fowler's Money pattern

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

This 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 {
  // ...
}
13
likes
15
pub points
67%
popularity

Publisher

verified publisherlitgroup.ru

Dart implementation of Fowler's Money pattern

Repository (GitHub)
View/report issues

License

MIT (LICENSE)

More

Packages that depend on money