money
Quick examples
Money.of(2090.5, "EUR").toCurrency("NOK", 8.61).toString();
Money.of(0.1, "NOK").add(Money.of(0.2, "NOK")).toString();
Money.fromPriceAndQuantity(0.0005, 30, "NOK").toString();
Money.fromPrice(0.001, "NOK").multiply(60).resetDecimals().toString();
Money.of(10, "NOK").distributeBy([1, 1, 1]);
Money.of(5.5, "NOK").toLocaleString("no-NB");
Money.fromLocaleString("5,50", "NOK", "no-NB").toString();
Money.of(12.5, "NOK")
.getVat(25, true)
.toString();
Money.of(10, "NOK")
.getVat(25, false)
.toString();
Money.of("1234567891234567.25", "NOK").toNumber();
See full api at the bottom of this page.
Features
- Based on big.js, arbitrary base 10 decimal numbers. No weird results due to base 2 doubles.
- Basic math and comparisons
- Ensures correct precision for given currency
- Prevents mixed currencies in calculations
- Handle prices with arbitrary precision
- Print and parse from locale
- Print and parse from fractionless / minor unit / whole number of cents
- Rounding modes (from big.js). For example rounding half even (bankers rounding)
- Throws when encountering precision loss when converting to javascript numbers
- Money objects have tags and tag assertions. E.g. tag with includesVat=true to know that the amount has vat included.
- Everything is immutable
- Various utils
- Calculate VAT
- Distribute money by parts or weights (straight division doesn't work with money)
About money in javascript / why this library exists
Numbers in javascript are not suitable for calculations with money
Javascript uses double floating point numbers.
There are several problems with these when it comes to handling money:
- Base 2 cannot accurately represent all base 10 numbers (e.g. 0.1 + 0.2).
This makes some calculations inaccurate, no matter the precision we choose.
It should be mentioned that this does not mean that we cannot convert between the bases in a precise way,
though it then should be mentioned that toFixed() is not a precise conversion
- double precision isn't infinite precision.
This matters when we need large numbers, or numbers with many fractional digits (like prices)
- Default rounding in javascript is wrong for negative money amounts
It's easiest to show by example:
0.1 + 0.2;
2090.5 * 8.61;
(2090.5 * 8.61).toFixed(2);
(8.165).toFixed(2);
Math.round(-1.5) -
(1.5).toFixed(0);
Number(9999999999999999).toString();
Number(999999999999999.9).toString();
Number(99999999999999.99).toString();
Number(9999999999999.999).toString();
Number(999999999999.9999).toString();
Number(99999999999.99999).toString();
Number(9999999999.999999).toString();
Number(999999999.9999999).toString();
Number(99999999.99999999).toString();
Number(9999999.999999999).toString();
This library uses the excellent big.js library which does arbitrary decimal (base 10) arithmetic.
This automatically takes care of all the issues above.
Whole numbers of cents is not enough for everything
It is often said that one should use a whole number of the minor unit of the currency, e.g. a whole number of cents.
This is (probably, see below) completely fine for transport and storage, but it is not fine for precision calculations.
Here's why:
- You still have to divide, and division introduces decimals
- You still have to convert currencies, and the rates have decimals.
- You still have to deal with prices, and those can have an arbitrary number of decimals.
You would need bigints to deal with those since javascript only has 53 bits (15 decimal digits) available for integers.
- You probably still need to convert to and from decimal digits at some point, and .toFixed() is not accurate
- You MUST keep track of the currency and precision/scale. Any errors here and you'll be orders of magnitude off.
This can be larger headache than you might think if you need to cover prices with arbitrary precision.
The problem is also there for storage and transport in this case.
In this library we've chosen to use big.js to avoid all potential pitfalls with whole number calculations.
Transporting / storing money amounts as javascript numbers
Consider that a double has 53 bits for the significand. This is the part that has exact precision.
53 binary digits amounts to 15.95 decimal digits, or 15 to be safe.
So we have 15 digits to work with. How much can we fit in those?
The worlds GDP is about 8*10^13 USD, which requires 14 digits. In cents it would be 16 digits.
So in USD we're just about able to store the entire world's GDP in cents without loss of precision.
Note that the exponent in the double doesn't change number of precise digits we get,
so it doesn't matter if we store it as a whole number or a fractional number.
In any case, it's quite likely that javascript numbers are good enough for transport and storage for most use cases,
even as fractional numbers. However, there are some potential pitfalls:
- The JSON standard doesn't define a precision for numbers, and it's up to the parser to deal with.
It seems quite reasonable to assume at least a double precision though.
- Most currencies have between 0 and 4 decimals after the decimal point, but crypto currencies can have a lot more.
This might cause the precision requirement to go up.
- If you deal with prices, all bets are off. A price can have an arbitrary precision.
You'll have to know how large and precise your numbers will be before you choose how to store and transport them.
This library gives you a couple of options:
- as fractionless / whole number
- as a normal number
- as a string
As we've seen, the first two have potential problems (however unlikely),
but to mitigate the risk we make sure to throw an exception if you ever encounter these problems.
If you want to be completely safe, use strings.
Legal operations on money
Money is in a currency, and it usually has a major (e.g. Dollars) and minor unit (e.g. Cents).
- An amount of money cannot be smaller than the minor unit (e.g. half a cent).
- Prices can have arbitrary precision, but a price is not Money until it is rounded.
- A consequence of the precision rule is that money cannot be evenly divided into chunks (e.g. 10 USD into 3 chunks will be [3.34, 3.33, 3.33])
- Money math and comparisons cannot be performed across currencies. A currency conversion to a common currency must take place first.
This library ensures that one operates on correct currencies and the correct precision for the given currency.
For intermediary calculations and price it is possible to adjust the precision.
Performance
big.js is quite fast and lightweight, but nowhere near the performance of native numbers.
The following code takes around 0.06ms, and just the addition itself is around 0.02ms:
Money.of(Math.random(), "NOK").add(Money.of(Math.random(), "NOK")).toNumber();
So you can do about 260 of those within a 60 fps frame, or around 800 pure additions.
If we're talking larger jobs, you can do 300 million of the above in 5 minutes.
API
export type AdditionalOptions = {
decimals?: number;
roundingMode?: RoundingMode;
tags?: Partial<Tags>;
};
export declare class Money {
constructor(data: MoneyInputData);
static of(
amount: NumberInput,
currency: string,
options?: AdditionalOptions,
): Money;
static fromLocaleString(
str: string,
currency: string,
locale?: string,
options?: AdditionalOptions,
): Money;
static fromFractionlessAmount(
amount: number,
currency: string,
options?: AdditionalOptions,
): Money;
static fromPrice(
price: NumberInput,
currency: string,
options?: AdditionalOptions,
): Money;
static fromPriceAndQuantity(
price: NumberInput,
quantity: Factor,
currency: string,
options?: AdditionalOptions,
): Money;
static sum(
moneys: Money[],
currency?: string,
options?: AdditionalOptions,
): Money;
static max(moneys: Money[]): Money;
static min(moneys: Money[]): Money;
static compare(money1: Money, money2: Money): number;
merge: (data: Partial<MoneyInputData>) => Money;
getTags: () => Tags;
getTag: <Name extends keyof Tags, Value>(
tagName: Name,
defaultValue?: Value | undefined,
) => Value | undefined;
setTag: <Name extends keyof Tags>(tagName: Name, value: any) => Money;
assertTag: <Name extends keyof Tags>(
tagName: Name,
value: any,
cmp?: (actual: any, value: any) => boolean,
) => Money;
assertSameCurrency: (money: Money) => Money;
amount: () => Big;
currency: () => string;
toFractionlessAmount: () => number;
toNumber: () => number;
toString: () => string;
toLocaleString: (locale?: string | undefined) => string;
toJSON: () => number;
getDecimals: () => number;
setDecimals: (decimals: number) => Money;
resetDecimals: () => Money;
toCurrency: (
currency: string,
currencyRate?: Factor,
currencyUnit?: Factor,
) => Money;
round: (decimals: number, roundingMode?: RoundingMode | undefined) => Money;
multiply: (factor: Factor) => Money;
divide: (divisor: Factor) => Money;
add: (money: Money) => Money;
subtract: (money: Money) => Money;
abs: () => Money;
equals: (money: Money) => boolean;
greaterThan: (money: Money) => boolean;
greaterThanOrEqual: (money: Money) => boolean;
lessThan: (money: Money) => boolean;
lessThanOrEqual: (money: Money) => boolean;
isZero: () => boolean;
isPositive: () => boolean;
isNegative: () => boolean;
distribute: (nParts: number) => Money[];
distributeBy: (inputWeights: Factor[]) => Money[];
addVat: (vatPercentage: Factor) => Money;
removeVat: (vatPercentage: Factor) => Money;
getVat: (vatPercentage: Factor, includesVat?: boolean | undefined) => Money;
}
Creating a new release
- Enforce all commits to the master branch to be formatted according to the
Angular Commit Message Format
- When merged to master, it will automatically be released with
semantic-release