Pesa
A JS money lib whose precision goes up to 11 (and beyond).
Install / Use
/learn @frappe/PesaREADME
पेसा
</div>A money handling library for JavaScript that can handle USD to VEF conversions for Jeff without breaking a sweat!
const money = pesa(135, 'USD').add(25).to('INR', 75);
money.round(2);
// '12000.00'
Why should I use this, when I can just do all of this with plain old JavaScript numbers?!
Because JavaScript numbers full of fun foibles such as these:
0.1 + 0.2;
// 0.30000000000000004
9007199254740992 + 1;
// 9007199254740992
Using them for financial transactions will most likely lead to technical bankruptcy [1].
(check this talk by Bartek Szopka to understand why JS numbers are like this)
pesa circumvents this by conducting scaled integer operations using JS BigInt instead of Number. This allows for arithmetic involving arbitrarily large numbers with unnecessarily high precision.
Index
<details> <summary><code>show/hide</code></summary> </details>Documentation Index
<details> <summary><code>show/hide</code></summary>- Arithmetic Functions
- Comparison Functions
- Check Functions
- Other Calculation Functions
- Display
- Chainable Methods
- Non Chainable Methods
- Internals - Numeric Representation - Conversion Rates
Installation
For npm users:
npm install pesa
For yarn users:
yarn add pesa
Usage
pesa(200, 'USD').add(250, 'INR', 75).percent(50).round(3);
// '9475.000'
This section describes the usage in brief. For more details, check the Documentation section. For even more details, check the source code or raise an issue.
Initialization
To create an initialize a money object you can either use the constructor function pesa:
import { pesa } from 'pesa';
const money = pesa(200, 'USD');
// OR
const money = pesa(200, options);
or the constructor function maker getMoneyMaker, this can be used if you don't want to set the options everytime you call pesa:
import { getMoneyMaker } from 'pesa';
const pesa = getMoneyMaker('USD');
// OR
const pesa = getMoneyMaker(options);
const money = pesa(200);
Options and Value
Options are optional, but currency has to be set before any conversions can take place.
interface Options {
bankersRounding?: boolean; // Default: true, use bankers rounding instead of traditional rounding.
currency?: string; // Default: '', Three letter alphabetical code in uppercase ('INR').
precision?: number; // Default: 6, Integer between 0 and 20.
display?: number; // Default: 3, Number of digits .round defaults to.
wrapper?: Wrapper; // Default: (m) => m, Used to augment all returned money objects.
rate?: RateSetting | RateSetting[]; // Default: [], Conversion rates
}
interface RateSetting {
from: string;
to: string;
rate: string | number;
}
type Wrapper = (m: Money) => Money;
Value can be a string, number or a bigint. If value is not passed the value is set as 0.
type Value = string | number | bigint;
If bigint is passed then it doesn't undergo any conversion or scaling and is used to set the internal bigint.
pesa(235).internal;
// { bigint: 235000000n, precision: 6 }
pesa('235').internal;
// { bigint: 235000000n, precision: 6 }
pesa(235n).internal;
// { bigint: 235n, precision: 6 }
Wrapper is a function that can add additional properties to the returned object.
One use case is Vue3 where everything is deeply converted into a Proxy, this is incompatible with pesa because of it's private variables and immutability.
So to remedy this you can pass markRaw as the wrapper function.
This will prevent the proxification of pesa objects. Which in the case of pesa shouldn't be required anyway because the underlying value is never changed.
Currency and Conversions
A numeric value isn't money unless a currency is assigned to it.
Setting Currency
Currency can be assigned any time before a conversion is applied.
// During initialization
const money = pesa(200, 'USD');
// After initialization
const money = pesa(200).currency('USD');
Setting Rates
To allow for conversion between two currencies, a conversion rate has to be set. This can be set before the operation or during the operation.
// Rate set before the operation
pesa(200).currency('USD').rate('INR', 75).add(2000, 'INR');
// Rate set during the operation
pesa(200).currency('USD').add(2000, 'INR', 0.013);
Conversion
The result of an operation will always have the currency on the left (USD in the above example). To convert to a currency:
// Rate set during the operation
money.to('INR', 75);
// Rate set before the operation
money.to('INR');
This returns a new Money object.
Does it provide conversion rates?
pesa doesn't provide or fetch conversion rates. This would cause dependencies on exchange rate APIs and async behaviour. There are a lot of exchange rate apis such as Coinbase, VAT Comply, European Central Bank, and others.
Immutability
The underlying value or currency of a Money object doesn't change after an operation.
const a = pesa(200, 'USD');
const b = pesa(125, 'INR').rate('USD', 0.013);
const c = a.add(b);
// Statements below will evaluate to true
a.float === 200;
b.float === 125;
c.float === 201.625;
a.getCurrency() === 'USD';
b.getCurrency() === 'INR';
c.getCurrency() === 'USD';
Chaining
Due to the following two points:
-
All arithmetic operation (
add,sub,mulanddiv), create a newMoneyobject having the values that is the result of that operation. -
All setter methods (
currency,rate), set the value of an internal parameter and return the callingMoneyobject.
Methods can be chained and executed like so:
pesa(200)
.add(22)
.currency('USD')
.sub(33)
.rate('INR', 75)
.mul(2, 'INR')
.to('INR')
.round(2);
// '377.99'
Documentation
Calling the main function pesa returns an object of the Money class.
const money: Money = pesa(200, 'USD');
The rest of the documentation pertains to the methods and parameters of this class.
Arithmetic Functions
Operations that involve the value of two Money objects and return a new Money object as the result.
Function signature
[operationName](value: Input, currency?: string, rate?: number) : Money;
type Input = Money | number | string;
Example:
money = pesa(200, 'USD');
money.add(150).round();
// '350.000'
money.sub(150, 'INR', 0.013);
// '198.050'
Note: The rate argument here is from the currency given in the function to the calling objects currency. So in the above example rate of 0.013 is for converting 'INR' to 'USD'. The reason for this is to prevent precision loss due to reciprocal.
Arguments (arithmetic)
| name | description | example |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| value | This is compulsory. If input is a string then '_' or ',' can be used as digit separator
