Skip to content

A Money class, based on the TDD implementation of Kent Beck

License

Notifications You must be signed in to change notification settings

polyadic/funcky-money

Repository files navigation

funcky-money

Build Licence: MIT

Funcky.Money is an implementation of a versatile Money concept which addresses a lot of the problems you encounter when working with money quantities and currencies.

The implementation uses a lot of the concepts from the TDD implementation of Kent Beck in his book: Test-Driven Development by Example.

Package

NuGet package

How can Money be Funcky?

The Money approach is distinctly functional especially in comparison to the Money Pattern by Martin Fowler which just gives up when it comes to currencies.

The basic idea is, instead of trying to calculate the final amount in one step, we start with an Expression Tree (AST: Abstract Syntax Tree) of the operations we want to perform with Money objects.

This allows us to decouple the assembly and the evaluation of our calculations, our calculation are lazy and allow deferred execution.

Why not a money bag?

While the Money as an expression tree holds structural information which is ultimatly rarely necessary, the equality on a Money expression ultimatly needs a in almost all cases an exchange rate if more than one currency is involved. Therefore I do not accept this argument as an improvement. Especially if we support more than just sums (multiply, distribute) the money bag is very limited.

Quickstart

Construct a money object

var dollar = new Money(2.99m, Currency.USD);
var francs = Money.CHF(0.95m);

Create money expressions

var sum1 = dollar + francs;
var sum2 = francs.Add(Money.CHF(5));

var product1 = dollar * 3;
var product2 = francs.Multiply(1.5m);

// distribute the money into 3 equal parts
var distribution1 = product2.Distribute(3);

// distribute the money according to the given proportions in units of 0.05
var distribution2 = product2.Distribute(new []{7, 2, 1}, 0.05m);

Evaluate a MoneyExpression

Evaluate returns a money object according to the evaluation context.

var francs = francs.Add(Money.CHF(5)).Evaluate();

var context = MoneyEvaluationContext.Builder.Default
                .WithTargetCurrency(Currency.CHF)
                .WithExchangeRate(Currency.USD, 0.9004m)
                .WithRounding(RoundingStrategy.BankersRounding(0.05m))
                .Build();

var dollar = (dollar + francs).Evaluate(context);

For more examples, consult the testing project.

Requirements

These is the evolving list of TDD requirements which led to the implementation.

  • Use decimal Amount.
  • Support multiple currencies.
  • Add two Moneys in the same Currency (5USD + 9USD).
  • Cleanup Currency (XML handling should be extracted).
  • Add two Moneys with a different Currency (5USD + 10CHF).
  • Two moneys without Exchange rates should not Evaluate to a result.
  • Two moneys are equal if they have the same Currency and the same Amount.
  • Ability to round to 0.05 / distribute 1CHF as [0.35, 0.35, 0.30]
  • Evaluation passes through precision to result.
  • Override precision on evaluation.
  • Support different Exchange rates (evaluation).
  • Every construction of Money currently rounds to two digits, while this is interesting for 5.7f, it has bad effects in evaluation. We should remove the rounding again.
  • The default MidpointRounding mechanism is bankers rounding (MidpointRounding.ToEven).
  • Multiply a Money with a real number (int, and decimal).
  • Distribute Money equally into n slices (1CHF into 3 slices: [0.33, 0.33, 0.34]).
  • Distribute Money proportionally (1 CHF in 1:5 -> [0.17, 0.83]).
  • Extract distribution of money into a strategy which is injected.
  • Support ISO 4217 Currencies.
  • Support calculations smaller than the minor unit? (on Money and EvaluationVisitor)
  • ToString supports correct cultural formatting and units.
  • Parse Money from string considering cultural formatting and units.
  • Support operators on the IMoneyExpression interface.
  • Convert currencies as late as possible (keep Moneybags per currency in the EvaluationVisitor).
  • Static constructor for most used Currencies, this could inject rules like precision: Money.CHF(2.00m)
  • To avoid rounding problems on construction, Money can only be constructed from decimal and int.
  • Add possibility to delegate the acquisition of exchange rates. (IBank interface)
  • There are a few throw Exception calls in the code which should be refined to specific exceptions.
  • There needs to be a NoRounding strategy, maybe provide an Interface IRoundingStrategy with a few given implementations.
  • Evaluation arithmetic Money operations can use different rounding mechanism.
  • Distribution with NoRounding strategy should distribute exactly according to the precision.
  • Distribution wich cannot exactly distribute money throws an ImpossibleDistributionException
  • Rounding only happens at the end of an evaluation.
  • Fix tests failing in other locales.
  • Write property tests using FsCheck.
  • Rounding is done at the end of every evaluation according to the rounding strategy.
  • The user can use arbitray rounding function, he just needs to implement the AbstractRoundingStratgey.
  • We do not round a Money on construction only on evaluation. You can create a 0.01m Money even if the precision is 0.1m.
  • Money distribution has a precision member, use that instead of the contrived Precision on Rounding.
  • Add unary and binary minus and the division operator.
  • The context has a smallest distribution unit.
  • A dimensionless factor can be calculated by dividing two money objects.

Decisions

  • We construct Money objects only from decimal and int. The decision how to handle external rounding problems should be done before construction of a Money object.
  • We keep Add, Multiply, etc because no all supported frameworks allow default implementations on the interface.
  • We prepare a distribution strategy but do not make it chosable at this point.
  • We support the following operators: unary + and -, and the binary operators ==, !=, +, -, * and /.
  • You can divide two different currencies only with the Divide(IMoneyExpression, IMoneyExpression, Option<MoneyEvaluationContext>) method where you have to give a MoneyEvaluationContext with the necessary exchange rates.
  • We removed the neutral Money element (Zero), there are too many unsolved problems. If we add it again we should probaly make a NeutralElement : IMoneyExpression.

Open Decisions

  • Implicit type conversion
    • Should A Money be constructible implicitly from a decimal?
    • Should adding a number to a money be possible (fiveDollars + 2.00m)?