Dealing with money in software is difficult and dangerous. This post contains an overview of the problems you will run into eventually when writing software that deals with money. It describes potential representations, relevant tradeoffs, and recommends ways of correctly implementing common operations. The post is prescriptive, so that you can use it to write your own library for dealing with amounts of money.
This post is an update of my previous post about dealing with money in software. It newly includes an overview of possible representations, better recommendations, and an addendum of tests.
Problems with money
Money is a surprisingly complex, but more importantly very dangerous subject for programs to deal with. It is hard to get right, but worth paying close attention to.
There are all sorts of intuitive and unintuitive pitfalls that you will want to avoid. (This list of falsehoods is a good place to appreciating how complex of a topic money in software is.) Not all problems will become evident immediately, and some might even never bite you, but chances are you will run into each one at some point in the long term.
This section lists common problems that you may run into when performing common operations on money.
Creating or destroying money through errors
Numeric calculations produce erroneous results when choosing a finitelysized representation. This is because arbitrary amounts of precision requires an arbitrary amount of time and space.
IEEE754 Floatingpoint numbers, for example, suffer from this problem. As an example, let us have a look at the value $0.1$, which cannot represented accurately using an IEEE754 floating point number. Indeed, it is represented as follows instead:
>>> "%.55f" % 0.1 '0.1000000000000000055511151231257827021181583404541015625'
As a result, the following example of the common addition operation produces a silent error:
λ> 0.1 + 0.1 + 0.1 0.30000000000000004
This means that when you get a dime from a customer three times, and store it as a floating number, you will have effectively created 0.00000000000000004
USD out of thin air. This might not seem like much (even though it does not need to be much, it is still wrong), but this error grows as the values in your calculations grow. There is an entire field of mathematics dedicated to thinking about what happens to the errors depending on what types of calculations you are performing. If you are not intimately familiar with this field, stay far away from representations that may have errors.
Silent Overflow and Underflow
This second problem is another form of numerical errors that stem from a finitelysized representation. It can happen if you try to store amounts of money as a fixedwidth integral number of dollar cents, and naively perform computations as computations on those numbers directly.
For example, consider a situation in which you are a bank and you store amounts of money as int32
. A very wealthy client of yours has about twenty million dollars. You save this information as "this client has two billion cents". A bit later, he earns another two million dollars, but when the money is sent to him, suddenly the system says that he is millions in of dollars in debt. There are two big issues with this scenario: 1. The client has lost money and 2. The total amount of money has decreased:
λ> 2000000000 + 200000000 :: Int32 2094967296
Underflow is a similar problem in which money disappears because an amount becomes too small:
λ> 0.5 ^ 1074 5.0e324 λ> 0.5 ^ 1075 0.0
Silently wrong results must be avoided in software that deals with money.
Representing nonsense values of money
The IEEE754 Floating point number spec defines NaN
, +Infinity
and Infinity
. Rational numbers also have these, in the form of 0 / 0
, 1 / 0
and 1 / 0
. These make no sense when interpreting them as monetary values. If your system ever computes one of these, there is little you can do to undo that.
Minimal quantisations
The smallest denomination of US dollar that you can hold is 0.01 USD
: a penny. Any amount of US dollars more granular than that does not exist in coins and cannot usually be transacted. (You can still potentially have more granular debts, as you will see below.)
Different currencies can also have different minimal quantisations. In Switzerland, where I am using my broker, the smallest coin of a Swiss Frank is 5 "rappen": 0.05 CHF
. The transaction granularity goes down to 0.01 CHF
in bank transactions but no lower than that either.
Representing amounts of money smaller than the minimal quantisation that you have chosen means you are dealing with amounts that make no sense as an amount of money. We must use a representation that cannot represent more granular amounts than the minimal quantisation of the currency at hand.
Arbitrary space and time
Given that fixedsized representations can result in computational errors, one might be tempted to use arbitrarilysized representations instead.
Sadly, such a representation only trades in the above issues for other ones. For example, consider the operation of halving an amount of money. After noteventhatmany halvings, the result should be pretty close to negligible. However, performing the halving operation with a representation that has arbitrary precision results in a representation that unboundedly increases in size.
For example, when using rational numbers, each halving operation requires at least one more bit to store the result:
Prelude Data.Ratio> (1 % 2 )^1000 1 % 10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
The important insight about this problem is that using arbitrary amounts of time or space makes a computation practically partial. In other words, it is no longer fair to assume that any given computation will complete on your machine, and that is dangerous.
Possible representations
This section explores different possible representations of amounts of money, and details the problems and tradeoffs with each.
Money as floatingpoint numbers
If you take away one thing from this blogpost, let it be this:
You must NOT store monetary values as IEEE 754 floating point numbers.
Calculations involving floating point numbers usually have some amount of error, so you can create or destroy money using calculations:
λ> 0.1 + 0.1 + 0.1 0.30000000000000004
Many floating point numbers do not make sense as a value of money. These values include
NaN
(0/0
),Infinity
(1/0
),Infinity
(1/0
).Calculations can silently overflow or underflow and sometimes create such values:
λ> 8.98846567431158e307 * 2 Infinity λ> 0.5 ^ 1075 0.0
Floating point numbers do not support minimal quantisations. You can represent values that are more granular:
λ> 0.01 / 2 5.0e3
Money as a fixedsize integral amount of minimal quantisations
Storing amounts of money as an integral number of minimal quantisations makes sense. The only problem you will have with using fixedsize integrals with it, is that you have to deal with overflow:
λ> (9223372036854775807 :: Int64) + 1 9223372036854775808
This problem is fixable using overflow checks.
Money as an arbitrarysize integral amount of minimal quantisations
Storing amounts of money as an arbitrarilysized integral number of minimal quantisations also makes sense because you don't have the overflow problem. Unfortunately this representation has the unfixable problem of unbounded resource requirements.
Prelude Data.Ratio> (1 % 2)^1000 1 % 10715086071862673209484250490600018105614048117055336074437503 883703510511249361224931983788156958581275946729175531468251871452 856923140435984577574698574803934567774824230985421074605062371141 877954182153046474983581941267398767559165543946077062914571196477 686542167660429831652624386837205668069376
Money as a fixedsize rational amount
You could store an amount of money as two numbers $a$ and $b$ such that they represent $a / b$.
If these numbers $a$ and $b$ are stored as finitelysized integers, then you run into the following problems:
Overflow and underflow:
Prelude Data.Int Data.Ratio> let r = 1 % 12 :: Ratio Int8 Prelude Data.Int Data.Ratio> r + r 3 % (14) > r * r 1 % (112)
Representing nonsense values of money
If $b$ are zero, $a/b$ does not represent a valid amount of money. All three of $1/0$, $0/0$, and $1/0$ are nonsensical as amounts of money.
Minimal quantisations
Rational values allow us to represent amounts of money that are more granular than the minimal quantisation. For example when the minimal quantisation is 0.01 USD
, and we have a value where $a=1$ and $b=1000$.
Money as an arbitrarysize rational amount
If instead you use arbitrarilysized numbers to represent $a$ and $b$, then computations using numbers like these can take unbounded amounts of time and space.
For example, consider the operation of halving an amount of money. This corresponds to doubling the $b$ in this representation. Performing this operation indefinitely involves an unbounded amount of time and memory, because every time the operation is performed, at least one more bit is needed to store the result.
Prelude Data.Ratio> (1 % 2 )^1000 1 % 10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
This representation also suffers from the same two problems as the previous option:
Minimal quantisations
Representing nonsense values of money
Overview
Given the above problems and possible representations, here is an overview of the tradeoffs
IEEE754 Floating point numbers such as
Float
andDouble
Fixedsize integral numbers of minimal quantisations such as
Int64
Arbitrarilysized integral numbers of minimal quantisations such as
Integer
Fixedsize rational numbers of minimal quantisations such as
Ratio Int64
Arbitrarilysized rational numbers of minimal quantisations such as
Ratio Integer
Double

Int64

Integer

Ratio Int64

Ratio Integer


Creating or destroying money through errors  💣  👌  👌  👌  👌 
Silent Overflow and Underflow  💣  💣  👌  💣  👌 
Representing nonsense values of money  💣  👌  👌  💣  💣 
Unbounded space and time usage  👌  👌  💣  👌  💣 
Minimal quantisations  💣  👌  👌  💣  💣 
In what follows, we will recommend option using 2: Int64
and dealing with the overflow and underflow problems in a library.
Solutions
Requirements
In order to begin talking about solutions, let us clarify what needs to be possible in the system that we produce:
Addition
We must be able to add up amounts of money.
This is useful to keep a running balance, for example.
The additions must happen in such a way that the representation of the sum equals the sum of the representations. Otherwise we might create or destroy money via addition.
For example, 0.1 + 0.1 + 0.1
must somehow equal 0.3
and not 0.30000000000000004
.
Integral Multiplication
We must be able to multiply amounts of money by an integer.
This is useful to find out how much multiple units of an item would cost, in the case of integral multiples.
You will notice that if you can solve the problem of addition, then you have also solved the problem of correct integral multiplication. However, ideally, an integral multiplication would take up the same amount of time as a single addition, not a linear multiple of it. We would like to have multiplication be efficient as well as correct.
For example, 3.33 x 3
must equal 9.99
and not 10
.
Distribution into an integer number of chunks
We must be able to distribute an amount into an integer number of chunks.
This is useful for dividing a payment into installments. Here it is important that the sum of the installments equals the amount that was divided. (Note that implementing integer division will always result in surprising (dangerous) behaviour, so we should use distribution instead.)
For example, 10 / 3
must be divided into three times 3.33
and an extra 0.01
somewhere: 3.34 + 3.33 + 3.33
.
Fractional Multiplication
We must be able to multiply amounts of money by a fraction.
This is useful for calculating interest rates, taxes and dividends, for example.
It should happen in such a way that the representation of the product equals the product of the representations. Failing that (because we'll see that this is impossible), it should at least not create or destroy money.
For example, 0.1
times 0.11 USD
, using a minimal quantisation of 0.01 USD
, should result in 0.01 USD
. The representational error which mathematically equals 0.001
should be dealt with somehow.
Accurate Accounting
We must be able to accurately account for every smallest unit of currency.
When earning or spending money, we must be able to account for every smallest unit of currency in play. No such units be created or destroyed, and no amounts smaller than the smallest unit may be accounted for.
Note that you can choose a different minimal quantisation of currency in your accounting system than is used for bank transactions. Even if banks use 0.01 CHF
as a minimal transaction value, you can still use 0.0001 CHF
as a minimal quantisation internally. The only real constraints are correctness: you do not create or destroy money and storage size. Dealing with more precise numbers requires more storage.
When you choose a different minimal quantisation of currency internally than the one you show to users, you have to somehow make that conversion. When you do, it is important that you don't add any mistakes back in.
Granular pricing
We must be able to make calculations with values smaller than the smallest unit of currency.
The utility of this requirement may not be obvious. However, when dealing with an item of large volume and cost such as oil, we want to be able to reason about (and compete on) the (average) price of a small amount of it. In such a case it may be necessary to reason about currency in units smaller than its minimal quantisation.
For example, if I want to rent out digital storage for 0.000023 USD
per kilobyte per month, that rate is like expressed in a more precise granularity then your system deals with. In that case we must still be able to rent out 2'000'000
kilobyte for a month for 46.00 USD
.
Serialisations
We must be able to losslessly send amounts of money between programs.
In a standard frontend/backend split, the backend must be able to send over information about amounts of money to the frontend and vice versa. This communication must be lossless, so that no miscommunications happen.
In other words, we need this law to hold:
decode(encode(amount)) == Success(amount)
Units
We must not be able to perform dimensionally incorrect calculations.
An amount of money is qualitatively different from a number because it has a unit. When multiplying two amounts of money together, the result is not an amount of money but an amount of money squared. That means that we must not be able to produce an amount of money by multiplying together two amounts of money.
Implementation
The rest of this post will details instructions for how to implement a library that can satisfy the above requirements.
Representation
Looking at the overview table of representations and their problems, we will use fixedsize integer numbers as an underlying representation, and check for overflows.
This reduces the representation issues to none, as long as the fixed size of the integers is big enough. A library author can choose to provide separate types for "only positive" and "positive or negative" amounts if they want.
We use Int64
or Word64
for just about any currency because these are almost always big enough. Indeed, maxBound :: Int64
is 9223372036854775807
. When choosing 0.0001 USD
as the minimal quantisation of USD, this allows us to represent a quadrillion dollars. This is more than the current M1 money supply of USD.
Addition, subtraction, and integer multiplication
Addition and subtraction of amounts of money is relatively simple. The only thing we have to watch out for is overflow. This means that these operations must be able to fail loudly. We can do this naively by converting to arbitrarily sized integers, performing the computation, and checking for overflow before converting back.
Multiplication can be implemented naively using iterated addition, but this would not be a constanttime operation. Instead, we implement multiplication in the same way using conversion, computation, checking and conversion back.
Integer distribution
When distributing an amount of money into an integer number of chunks, we also have the problem of remainders. The remainder has to be distributed somehow. For example, when distributing 10 into 3 chunks, we have 1
chunk of 4
and 2
chunks of 3
.
We could return a list of chunks, but then the result would not be a constantsize data structure. Instead we must return a special data structure that specifies the number of chunks and their sizes. This is a constantsize data structure because we can always distribute the amount into at most two differently sized chunks.
The procedure to compute the result works using integer division:
Divide the total amount by the number of chunks to get the size of the smaller chunks.
The remainder amount of minimal quantisations equals the number of larger chunks.
The number of smaller chunks is the number of chunks minus the number of larger chunks.
The size of the larger chunks is one minimal quantisation more than the size of the smaller chunks.
Fractional multiplication, accurate accounting and granular pricing
As long as we only perform addition and integer multiplication, the accurate accounting requirement is already fulfilled. It also turns out that if we can solve fractional multiplication in an accurate way, that we can get granular pricing for free.
The big issue with fractional multiplication is easily illustrated with currency conversions. For example, as of the time of writing, the conversion rate from EUR to CHF is 1 EUR = 1.072032 CHF
. At a first glance this really does not make sense because 1.072032 CHF
really does not mean anything if we choose a minimal quantisation greater than 0.0000001 CHF
. (Note that there will always be rates that don't make sense for your minimal quantisation, so you have to deal with this problem.) There exists 1.05 CHF
and 1.10 CHF
but there can never be 1.072032
CHF using this minimal quantisation. However, you never really convert exactly one EUR either. This is the important piece of info that will allow us to solve the problem.
You cannot simply multiply 1 EUR
by 1.072032 CHF/EUR
to get a sensible value in CHF
. Instead, a bit more infrastructure is needed. The way you should interpret such a ratio is a bit different from your intuition.
To exchange 10 000 EUR
to CHF
, we first make a hypothetical calculation of what would happen if currencies had no minimal quantisation:
10 000.00 EUR * 1.072032 CHF / EUR = 10 720.32 CHF
Next, we choose the closest meaningful value to the result. Recall that we chose a minimal quantisation of 0.05 CHF
, so the closest meaningful value is 10 720.30 CHF
. We represent this as the integer 214406
because 214406 * 0.05 CHF = 10 720.30 CHF
.
You now change the rate that you give so that this matches nicely, and represent the rate as an integral fraction:
214 406 % 10 000 00 = 107 203 % 5 000 00
We represent only the conversion rate as a fraction, not amounts of money. Note that you have to represent this as two numbers in memory, because there is no guarantee that the result would not have unrepresentable repeating digits. Note also that the original rate only differs from this rate by 0.000002
, and you can charge a higher rate if this is a problem for you. The 'error' also shrinks as transactions get bigger and the absolute (theoretical) error is never bigger than your chosen minimal quantisation so this is really no big deal in practice.
You can then safely use this rate to accurately convert 10 000.00 EUR
to 10 720.32 CHF
.
This means that the result of the fraction
computation must be a tuple of the resulting amount and the real ratio that was used. We then compute these as follows:
Compute the theoretical result as an arbitraryprecision fraction.
Compute the rounded results as a number of minimal quantisations. (You can round in whichever direction is most appropriate for your application.)
Compute the actual rate by dividing the rounded result by the input amount.
Return the rounded result together with the actual rate.
Showing amounts to users
When you chose amounts to users, we need to make sure that they make sense. It should not look like we made any errors.
By far the simplest way to deal with this problem is to show amounts to users at the same level of granularity as we are storing them. Otherwise you may show incorrectly rounded values to users. You can still give users the option to show amounts in a more conventional way if they do not really care about these apparent mistakes.
Serialisation
You are probably (rightfully) expecting that roundtripping numbers "should" be easy. Sadly, when using JSON numbers, they do not roundtrip when parsed by JavaScript's standard JSON.parse
function:
> JSON.parse("1234567891011121314") 1234567891011121400
(Note that this number is smaller than the maximum value of a Word64
.)
While JSON does support representing arbitrarily large integer numbers accurately, many implementations use floating point numbers to parse them. This means that we cannot use JSON numbers to represent serialised amounts of money and instead are forced to use a different representation.
The least surprising safe representation will be JSON String's that represent numbers that represent an integral number of minimal quantisations. When using JavaScript, this string must then be passed to a BigInt
constructor:
> BigInt(JSON.parse("\"1234567891011121314\"")) 1234567891011121314n
Other formats will likely support either precise integers or strings as well, so we can choose the appropriate serialisation based on what is available.
Units
We must use the tools that the programming language provides to ensure that dimensionally incorrect computations cannot occur.
For example, addition like Amount(5) + Amount(6)
should result in an Amount(11)
, but multiplication must not. Indeed, the following computation must fail, either at compiletime or at runtime (at least if it would result in an Amount
):
Amount(5) * Amount(6)
We must also use the strongest possible tools to ensure that these errors do not occur. In some languages like JavaScript and Python, that means using runtime checks because that's all there is. In Haskell we could simply not provide an instance of Num
for Amount
, but we must go further still by poisoning that instance entirely and turning it into a type error.
instance TypeError ('Text "Amounts of money must not be an instance of Num") => Num Amount
Library recommendations
In Haskell I can only recommend the reallysafemoney
package. The comparison table in the README
explains why. In short: every other library that I investigated uses a representation with a serious unfixable flaw and most have dangerous instances.
Conclusions
It is possible to deal with currency accurately, but it is not easy. Listen to experts on this matter, and test your implementation well. You can use propertybased testing, and validitybased testing in particular, to help you find problems with your implementation.
Addendum: Tests
This section aims to describe examples that any implementation must handle correctly. We use pseudo code here, so some interpretation is required.
Representation and conversions
Turning an integer amount of minimal quantisations (cents, for example), must be able to fail.
It must succeed for:
0
1 minimal quantisation
1 minimal quantisation
The maximal amount of minimal quantisations that your representation allows
On the positive end
On the negative end (that is usually not the same in absolute amount)
0.0
, but that should be normalised to 0 or disallowed by type
It must fail (either at runtime or by type) for:
Any fractional amount of minimal quantisations
NaN
+Infinity
Infinity
For any amount of money, turning it into an integer amount of minimal quantisations and back into an amount of money should succeed and result in the same amount of money.
toMoney(fromMoney(money)) == money
Note that the same does not hold for floating point amounts of minimal quantisations. Indeed, multiplying by the quantisation factor is a calculation that can have floatingpoint calculation errors as well.
The inverse property must not hold, see the list of prescribed failures above.
Turning a floating point or rational amount into an amount of money should fail whenever it introduces an error.
Addition
Addition of two amounts must be able to fail.
It must succeed for:
1 + 2
, and equal3
0 + money
for anymoney
(and equalmoney
).money + 0
for anymoney
(and equalmoney
).
It must fail for:
maxBound + 1
maxBound + maxBound
minBound + minBound
minBound + ( 1)
Adding two amounts in different currencies must always fail.
It must be associative if both sides succeed:
a + (b + c) == (a + b) + c
Note that either side might fail separately because of overflow.It must be commutative:
a + b == b + a
.
Subtraction
Subtraction of two amounts must be able to fail.
It must succeed for:
3  2
, and equal1
.0  money
for anymoney
(and equal money
).money  0
for anymoney
(and equalmoney
).
It must fail for:
minBound  1
maxBound  minBound
minBound  maxBound
maxBound  ( 1))
Summation
Summation must succeed for:
sum [1, 2, 3] == 6
Summation must fail for:
[maxBound, 1]
[maxBound, 1, 2]
(in order to maintain bounded complexities per element).
Integer multiplication
It must succeed for:
3 x 6
, and equal18
1 x money
, and equalmoney
0 x money
, and equal a zero amount
It must fail for:
2 x maxBound
3 x minBound
It must be distributive when both sides succeed:
a x (b + c) == (a x b) + (a x c)
.
Integer distribution
It distributes
3
into 3 as3
chunks of1
.It distributes
5
into 3 as2
chunks of2
and1
chunk of1
.It distributes
10
into 4 as2
chunks of3
and2
chunk of2
.It distributes any amount into chunks that sum up to the input amount successfully.
Fractional multiplication
100 minimal quantisations times 1/100 must equal 1 minimal quantisation.
101 minimal quantisations times 1/100 must equal 1 minimal quantisation with the rate changed to 1/101.
It must produce results that can be multiplied back to the input amount successfully.