Ezmoney
A library to safely manipulate monetary values.
Ezmoney (/ˈizi'mʌni/) helps you operate on monetary values, compare them, format them, and more. It features:
- Pure functions: Ezmoney only exports side-effect free functions. This makes the API simple and enables dead code elimination (i.e. tree-shaking).
- Safety first: Thanks to TypeScript, Ezmoney is fully type-safe. Additionally, all the standard functions validate their arguments for the properties that TypeScript cannot verify.
- Opt-in performance boost: All functions have an unsafe variant that forgoes the validation of the arguments. Use it when you are confident in the correctness of your code and need that extra bit of oomph.
Setup
Browser
To add Ezmoney to your page, include the following tag to your HTML.
<script src="https://unpkg.com/ezmoney">
It creates a global identifier Ezmoney
, which is an object that holds all the exported functions.
Node.js
To add Ezmoney to you dependencies, run the following command:
npm install ezmoney
Run yarn add ezmoney
instead if you are using yarn.
Then, you can use require('ezmoney')
in your code, or import { ... } from 'ezmoney'
if you are using TypeScript or ES6 modules.
Usage
Ezmoney exports a set of functions that operate on monetary values.
A monetary value is an object with three properties: amount
, currency
and precision
.
The properties amount
and precision
define together the decimal number that the monetary value represents: the amount is the coefficient part of the decimal number and the precision is the number of decimal places. As for the property currency
, it is a string that represents the currency in which the monetary value is expressed. You can use any string you want, but if you want internationalization you will need to use the official codes from ISO4217.
For example, to represent $3.14
, you would define the monetary value { amount: 314, currency: 'USD', precision: 2 }
.
Once you have defined some monetary values, you can start using one of the many functions exported by Ezmoney. For example, to add $3.14
and $420
, you would write the following:
Ezmoney.add(
{ amount: 314, currency: 'USD', precision: 2 },
{ amount: 420, currency: 'USD', precision: 0 },
);
Also note that amount
must be a safe integer and precision
must be an integer between 0 and 15 (included).
Creating monetary values
There are multiple ways to create monetary values. You can:
- build a literal object with
amount
, currency
and precision
(as we have been doing so far); - use the
create
function that takes the arguments amount
, currency
and precision
, in that order; - use the
fromNumber
function that takes the decimal value as first argument, then the currency
and the precision
; - use
fromString
to parse a string that contains the currency, followed by a space, followed by the decimal value, which digits up to the precision.
The following variables will therefore hold identical monetary values:
const withLiteralObject = { amount: 310, currency: 'USD', precision: 2 };
const withCreate = Ezmoney.create(310, 'USD', 2);
const withFromNumber = Ezmoney.fromNumber(3.1, 'USD', 2);
const withFromString = Ezmoney.fromString('USD 3.10');
Performing operations
Ezmoney exports functions to operate on monetary values. They are the following:
Unary | Binary | Others |
---|
negate | maximum | multiply |
absolute | minimum | integerDivide |
| add | divide |
| subtract | allocate |
Unary operations take a single monetary value as argument. Binary operations take two monetary values of the same currency as arguments. Other operations take a monetary value and one or multiple numbers as arguments.
Comparing monetary values
You can perform the usual binary comparisons with the following functions:
greaterThan
greaterThanOrEqual
equal
lessThanOrEqual
lessThan
They all return a boolean depending on whether the condition they describe is verified. They are implemented using the function compare
, which returns -1
, 0
or 1
depending on whether the first argument is less than, equal to or greater than the second argument.
These comparisons are only allowed on monetary values that have the same currency. To compare for equality on monetary values that may have different currencies, use either identical
or equivalent
. The former only returns true
when the arguments have exactly the same properties; the latter may return true
with objects that have different precisions if they still represent the same monetary value. Example:
Ezmoney.identical(
{ amount: 42, currency: 'USD', precision: 2 },
{ amount: 420, currency: 'USD', precision: 3 },
);
Ezmoney.equivalent(
{ amount: 42, currency: 'USD', precision: 2 },
{ amount: 420, currency: 'USD', precision: 3 },
);
Rounding
Functions that are expected to perform rounding take an optional rounding function as last argument. The default rounding function performs a half-to-even rounding (also called the Banker's rounding) but you can replace it by any of the exported rounding functions. They are:
roundDown
roundUp
roundTowardsZero
roundAwayFromZero
roundHalfUp
roundHalfDown
roundHalfTowardsZero
roundHalfAwayFromZero
roundHalfToEven
roundHalfToOdd
Note that none of the functions validate their arguments at runtime as they are not meant to be used directly.
You can also define your own rounding function but the ones defined in Ezmoney should cover all the common use cases and more.
Formatting
The format
function uses the native Intl
API to format a monetary value with the provided locale. Therefore, the result depends on your environment. First, make sure it supports Intl.NumberFormat
with explicit locale; the function isFormatSupported
will return true
if it the case. Then, check that the locales you want to use are available.
If your environment properly supports format
, you can use it as the following example illustrates:
Ezmoney.format({ amount: 314, currency: 'USD', precision: 2 }, 'en-US', {
currencyDisplay: 'symbol',
});
Ezmoney.format({ amount: 42, currency: 'EUR', precision: 0 }, 'fr-FR', {
currencyDisplay: 'name',
signDisplay: 'always',
});
The returned string always displays as many decimal places as the precision.
Checking that an object is a monetary value
The function isMonetaryValue
will return true
if the provided input satisfies the conditions to be a monetary value. It will check that the argument is an object with the properties amount
, currency
and precision
, that those properties have the right type and that they have valid values. You can use it to prevent the other functions from throwing unexpectedly.
If you are using TypeScript, the function isMonetaryValue
can be used as a type guard.
Using the unsafe variants
Preconditions that cannot be checked by TypeScript are validated at runtime. For example, compare
always ensures that the two monetary values provided have the same currency; if they don't, it will throw.
This incurs a small performance cost; if you want your code to be as fast as possible, you can trade a little bit of safety for more speed by using the unsafe functions.
Operations, comparisons and other similar functions each have an alternative implementation that displays the same behavior in normal circumstances but do not perform any runtime checks.
They are all prefixed by unsafe
and do not guarantee correctness if any of the provided arguments are invalid. For example:
Ezmoney.add(
{ amount: 314, currency: 'USD', precision: 2 },
{ amount: 420, currency: 'USD', precision: 0 },
);
Ezmoney.unsafeAdd(
{ amount: 314, currency: 'USD', precision: 2 },
{ amount: 420, currency: 'USD', precision: 0 },
);
Ezmoney.add(
{ amount: 314, currency: 'USD', precision: 2 },
{ amount: 420, currency: 'EUR', precision: 0 },
);
Ezmoney.unsafeAdd(
{ amount: 314, currency: 'USD', precision: 2 },
{ amount: 420, currency: 'EUR', precision: 0 },
);
Use the unsafe functions when you really need performance and are confident that the arguments will always be valid.
Contributing
There are no contributing guidelines yet. They will be communicated soon.
In the meantime, feel free to open an issue for bugs and features requests or submit a pull request if you would like to improve the project.
Why a library?
There is a very good article that explains why you would want to use a library to handle monetary values in JavaScript.
Basically, JavaScript follows the IEEE 754 standard made for floating-point arithmetic. Numbers all have a double precision (64 bit). This is dangerous because:
- non-integers cannot be precisely represented;
- integers outside of a certain range are not "safe", i.e. a single value could represent multiple possible numbers.
When you are dealing with money, accuracy is important. To avoid the pitfalls of floating-point arithmetic, you need to:
- always deal with integers, even for decimal values;
- make sure that the integers stay in the safe range.
This is exactly what Ezmoney provides (among other things).
Prior art
A non-exhaustive list of libraries that solve problems similar to Ezmoney:
Special shout out to Sarah Dayan, the author of both Dinero.js and the article linked above. Dinero.js was the main source of inspiration for Ezmoney. It has a nice object-oriented, chainable and immutable API; it may interest you if you prefer that style.
As with many other similar libraries, the fundamental design has been inspired by Martin Fowler's take on money as a value object.
The style of the API is inspired from date-fns: it exposes a set of simple, well-scoped pure functions to operate on date objects. Those properties makes the library easy to grok and pleasant to use.
License
Ezmoney is licensed under MIT.
Copyright (c) 2019 Spendesk