├── .prettierignore ├── .gitignore ├── tests ├── types.ts ├── profile │ ├── timer.ts │ ├── profile.ts │ └── helpers.ts ├── preciseNumber │ ├── core.spec.ts │ ├── preciseNumberOperator.spec.ts │ ├── preciseNumberComparator.spec.ts │ └── preciseNumberMaker.spec.ts └── money │ └── money.spec.ts ├── run.sh ├── .github ├── logo.png ├── precision.png └── workflows │ └── everything.yml ├── src ├── consts.ts ├── utils.ts ├── preciseNumber.ts └── money.ts ├── tsconfig.build.json ├── profile.sh ├── tsconfig.json ├── LICENSE ├── index.ts ├── package.json ├── yarn.lock └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | types -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | types 4 | /**/iso4217.json -------------------------------------------------------------------------------- /tests/types.ts: -------------------------------------------------------------------------------- 1 | export type Input = bigint | number | string; 2 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env zsh 2 | 3 | node --require ts-node/register $1 -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/pesa/HEAD/.github/logo.png -------------------------------------------------------------------------------- /.github/precision.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frappe/pesa/HEAD/.github/precision.png -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | export const MIN_PREC = 0; 2 | export const MAX_PREC = 36; 3 | export const DEF_PREC = 6; 4 | export const DEF_DISP = 3; 5 | export const USE_BNKR = true; 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["tests/**/*"], 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": true, 8 | "outDir": "./dist" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /profile.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env zsh 2 | 3 | rm ./profiler-output.log 2> /dev/null 4 | echo "starting profile" 5 | # Will take around a minute 6 | node --require ts-node/register --prof ./tests/profile/profile.ts 7 | node --prof-process --preprocess -j ./isolate-*-v8.log > ./profiler-output.json 1> /dev/null 2> /dev/null 8 | # Feed JSON here: https://v8.dev/tools/head/profview 9 | rm ./isolate-*-v8.log 10 | echo "profiler-output.log generated" -------------------------------------------------------------------------------- /.github/workflows/everything.yml: -------------------------------------------------------------------------------- 1 | name: Everything 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | everything: 7 | runs-on: macos-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-versions: '16.4.0' 13 | - name: Instal dev-dependencies 14 | run: yarn --frozen-lockfile 15 | - name: Checking typing and formatting 16 | run: yarn run checkformat 17 | - name: Running build 18 | run: yarn run build 19 | - name: Running tests 20 | run: yarn run testci 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Checks 4 | "strict": true, 5 | "noUnusedLocals": true, 6 | "noImplicitReturns": true, 7 | "allowUnusedLabels": false, 8 | "noUnusedParameters": true, 9 | "noImplicitOverride": true, 10 | "allowUnreachableCode": false, 11 | "exactOptionalPropertyTypes": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noPropertyAccessFromIndexSignature": true, 14 | // Other 15 | "target": "es2020", 16 | "isolatedModules": true, 17 | "forceConsistentCasingInFileNames": true 18 | }, 19 | "include": ["index.ts", "src/**/*", "tests/**/*"] 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { DEF_PREC } from './src/consts'; 2 | import Money, { Options } from './src/money'; 3 | import PreciseNumber from './src/preciseNumber'; 4 | 5 | export type Input = bigint | number | string; 6 | export type Currency = string; 7 | export type Config = Options | Currency; 8 | 9 | export type PreciseNumberMaker = ( 10 | value: Input, 11 | innerPrecision?: number 12 | ) => PreciseNumber; 13 | export type MoneyMaker = (value: Input, innerOptions?: Config) => Money; 14 | 15 | export function p(value: Input = 0, precision: number = 6): PreciseNumber { 16 | return new PreciseNumber(value, precision); 17 | } 18 | 19 | export function pesa(value: Input = 0, options: Config = {}): Money { 20 | if (typeof options === 'string') { 21 | options = { currency: options }; 22 | } 23 | 24 | return new Money(value, options); 25 | } 26 | 27 | export function getPreciseNumberMaker( 28 | precision: number = DEF_PREC 29 | ): PreciseNumberMaker { 30 | return function (value: Input, innerPrecision?: number) { 31 | return p(value, innerPrecision ?? precision); 32 | }; 33 | } 34 | 35 | export function getMoneyMaker(options: Config = {}): MoneyMaker { 36 | return function (value: Input, innerOptions?: Config) { 37 | return pesa(value, innerOptions ?? options); 38 | }; 39 | } 40 | 41 | export { Money, PreciseNumber }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pesa", 3 | "version": "1.1.13", 4 | "description": "A JS money lib whose precision goes upto 11 (and beyond)", 5 | "main": "dist/pesa.js", 6 | "module": "dist/pesa.es.js", 7 | "browser": "dist/pesa.min.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "checkformat": "prettier --check . && tsc --noEmit", 14 | "test": "mocha --reporter nyan --require ts-node/register tests/**/*.spec.ts", 15 | "testci": "mocha --reporter spec --require ts-node/register tests/**/*.spec.ts", 16 | "build": "tsc --project ./tsconfig.build.json && node build.js", 17 | "pub": "rm -rf dist && yarn build && yarn publish", 18 | "format": "prettier --write ." 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/frappe/pesa.git" 23 | }, 24 | "keywords": [ 25 | "currency", 26 | "precision", 27 | "money", 28 | "accounting", 29 | "utilities", 30 | "jsLibForJeff" 31 | ], 32 | "author": "18alantom", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/frappe/pesa/issues" 36 | }, 37 | "homepage": "https://github.com/frappe/pesa#readme", 38 | "devDependencies": { 39 | "@types/mocha": "^9.0.0", 40 | "@types/node": "^16.11.7", 41 | "esbuild": "^0.13.13", 42 | "mocha": "^9.1.3", 43 | "prettier": "2.4.1", 44 | "ts-node": "^10.4.0", 45 | "typescript": "^4.4.4" 46 | }, 47 | "prettier": { 48 | "semi": true, 49 | "singleQuote": true, 50 | "trailingComma": "es5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/profile/timer.ts: -------------------------------------------------------------------------------- 1 | export class Timer { 2 | timers: Record; 3 | constructor() { 4 | this.timers = {}; 5 | } 6 | 7 | start(name: string) { 8 | this.timers[name] ??= { start: [], end: [] }; 9 | 10 | const start = process.hrtime.bigint(); 11 | this.timers[name].start.push(start); 12 | } 13 | 14 | end(name: string) { 15 | const end = process.hrtime.bigint(); 16 | this.timers[name]!.end!.push(end); 17 | } 18 | 19 | mean(): Record { 20 | const rec: Record = {}; 21 | for (const n of Object.keys(this.timers)) { 22 | rec[n] = this.#mean(n); 23 | } 24 | 25 | return rec; 26 | } 27 | 28 | min(): Record { 29 | return this.minOrMax('min'); 30 | } 31 | 32 | max(): Record { 33 | return this.minOrMax('max'); 34 | } 35 | 36 | minOrMax(type: 'min' | 'max'): Record { 37 | const rec: Record = {}; 38 | for (const n of Object.keys(this.timers)) { 39 | rec[n] = this.#minOrMax(n, type); 40 | } 41 | 42 | return rec; 43 | } 44 | 45 | #mean(name: string): bigint { 46 | let diffs = this.#diff(name); 47 | if (!diffs.length) { 48 | return 0n; 49 | } 50 | 51 | return diffs.reduce((a, b) => a + b) / BigInt(diffs.length); 52 | } 53 | 54 | #minOrMax(name: string, type: 'min' | 'max'): bigint { 55 | const index = type === 'min' ? 0 : -1; 56 | return this.#diff(name).at(index)!; 57 | } 58 | 59 | #diff(name: string) { 60 | const { start, end } = this.timers[name]; 61 | 62 | if (!start || !end) { 63 | throw Error('timers not set'); 64 | } 65 | 66 | if (start.length !== end.length) { 67 | throw Error(`${start.length - end.length} timers still running`); 68 | } 69 | 70 | return start.map((v, i) => end[i] - v).sort((a, b) => Number(a - b)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/profile/profile.ts: -------------------------------------------------------------------------------- 1 | import { randomInt } from 'crypto'; 2 | import { p, pesa } from '../../index'; 3 | import { loop, MAX, print } from './helpers'; 4 | import { Timer } from './timer'; 5 | 6 | function randomBigInt(max: number): bigint { 7 | return BigInt(randomInt(max)); 8 | } 9 | 10 | function doPNumber() { 11 | return p(randomInt(MAX)) 12 | .add(randomInt(MAX)) 13 | .mul(randomInt(MAX)) 14 | .div(randomInt(MAX) || 1) 15 | .sub(randomInt(MAX)).float; 16 | } 17 | 18 | function doPesa() { 19 | return pesa(randomInt(MAX)) 20 | .add(randomInt(MAX)) 21 | .mul(randomInt(MAX)) 22 | .div(randomInt(MAX) || 1) 23 | .sub(randomInt(MAX)).float; 24 | } 25 | 26 | function doNumber() { 27 | return ( 28 | ((randomInt(MAX) + randomInt(MAX)) * randomInt(MAX)) / 29 | (randomInt(MAX) || 1) - 30 | randomInt(MAX) 31 | ); 32 | } 33 | 34 | function doBigint() { 35 | return ( 36 | ((randomBigInt(MAX) + randomBigInt(MAX)) * randomBigInt(MAX)) / 37 | (randomBigInt(MAX) || 1n) - 38 | randomBigInt(MAX) 39 | ); 40 | } 41 | 42 | export const pf = {}; 43 | 44 | class Empty { 45 | #value: bigint; 46 | constructor(value: number) { 47 | this.#value = BigInt(value); 48 | } 49 | get value() { 50 | return this.#value; 51 | } 52 | } 53 | 54 | const toProfile = { 55 | initEmpty: () => new Empty(randomInt(MAX)), 56 | initPn: () => p(randomInt(MAX)), 57 | initPesa: () => pesa(randomInt(MAX)), 58 | doPNumber, 59 | doPesa, 60 | doNumber, 61 | doBigint, 62 | }; 63 | 64 | (function run() { 65 | const timers: Timer[] = []; 66 | 67 | loop(() => { 68 | const t = new Timer(); 69 | for (const key of Object.keys(toProfile)) { 70 | const fn = toProfile[key as keyof typeof toProfile]; 71 | 72 | loop(() => { 73 | t.start(key); 74 | fn(); 75 | t.end(key); 76 | }, MAX / 5); 77 | } 78 | 79 | timers.push(t); 80 | }, 3); 81 | 82 | print(timers); 83 | })(); 84 | -------------------------------------------------------------------------------- /tests/profile/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Timer } from './timer'; 2 | 3 | export const MAX = 100_000; 4 | type BigMap = Record; 5 | type Metric = 'mean' | 'min' | 'max'; 6 | const metrics: Metric[] = ['mean', 'min', 'max']; 7 | 8 | export function loop(fn: () => void, count: number = MAX) { 9 | for (let i = 0; i <= count; i++) { 10 | fn(); 11 | } 12 | } 13 | 14 | export function print(t: Timer | Timer[]) { 15 | if (Array.isArray(t)) { 16 | return printArr(t); 17 | } 18 | 19 | for (const key of metrics) { 20 | console.log(key); 21 | console.log(format(t[key as Metric]())); 22 | console.log(); 23 | } 24 | } 25 | 26 | function printArr(timers: Timer[], printOnlyMean: boolean = true) { 27 | const metricArrs: Record = { mean: [], min: [], max: [] }; 28 | 29 | for (const tim of timers) { 30 | for (const met of metrics) { 31 | metricArrs[met].push(tim[met]()); 32 | } 33 | } 34 | 35 | const metricGroups = Object.keys(metricArrs).reduce((acc, m) => { 36 | acc[m] = group(metricArrs[m as Metric]); 37 | return acc; 38 | }, {} as Record>); 39 | 40 | for (const met in metricGroups) { 41 | const group = metricGroups[met]; 42 | if (printOnlyMean && met !== 'mean') { 43 | continue; 44 | } 45 | 46 | console.log(met + ':'); 47 | for (const key in group) { 48 | console.log(` ${key}:`); 49 | for (const i in group[key]) { 50 | const value = group[key][i]; 51 | console.log( 52 | ` ${i.toString().padStart(2)}. ${value.toString().padStart(15)} ns` 53 | ); 54 | } 55 | console.log(); 56 | } 57 | } 58 | } 59 | 60 | function group(arr: BigMap[]) { 61 | return arr.reduce((acc, tim) => { 62 | for (const k of Object.keys(tim)) { 63 | acc[k] ??= []; 64 | acc[k].push(tim[k]); 65 | } 66 | 67 | return acc; 68 | }, {} as Record); 69 | } 70 | 71 | function format(obj: Record) { 72 | return Object.keys(obj) 73 | .map((k, i) => { 74 | const time = (Number(obj[k]).toString() + ' ns').padStart(15); 75 | return `${i.toString().padStart(3)}. ${k.padEnd(15)}: ${time}`; 76 | }) 77 | .join('\n'); 78 | } 79 | -------------------------------------------------------------------------------- /tests/preciseNumber/core.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import 'mocha'; 3 | import { 4 | getIsInputValid, 5 | scaler, 6 | ScalerInput, 7 | toDecimalString, 8 | } from '../../src/utils'; 9 | 10 | describe('Core functions', function () { 11 | describe('scaler', function () { 12 | const testThese = [ 13 | ['.2', 0, 0n], 14 | ['.2', 1, 2n], 15 | ['22.2', 2, 2220n], 16 | ['22.2', 0, 22n], 17 | ['55.555', 2, 5556n], 18 | ['55.555', 6, 55555000n], 19 | ['-55.555', 6, -55555000n], 20 | ['2.555', 2, 256n], 21 | ['-2.555', 2, -256n], 22 | ['1.9999999999999999999999999999', 2, 200n], 23 | ['-1.9999999999999999999999999999', 2, -200n], 24 | ['1.9999999999999999999999999999', 27, 2000000000000000000000000000n], 25 | ['-1.9999999999999999999999999999', 27, -2000000000000000000000000000n], 26 | ['1.9999999999999999999999999999', 28, 19999999999999999999999999999n], 27 | ['-1.9999999999999999999999999999', 28, -19999999999999999999999999999n], 28 | ['1.9999999999999999999999999999', 30, 1999999999999999999999999999900n], 29 | ['-1.999999999999999999999999999', 29, -199999999999999999999999999900n], 30 | [22, 0, 22n], 31 | [-22, 0, -22n], 32 | [22.555, 0, 23n], 33 | [-22.555, 0, -23n], 34 | [22.555, 2, 2256n], 35 | [-22.555, 2, -2256n], 36 | ]; 37 | 38 | for (let [input, precision, output] of testThese) { 39 | const scalerOutput = scaler(input as ScalerInput, precision as number); 40 | 41 | specify(`input: ${input}`, function () { 42 | assert.strictEqual(scalerOutput, output); 43 | }); 44 | } 45 | }); 46 | 47 | describe('getIsInputValid', function () { 48 | const testThese = [ 49 | [0.22, true], 50 | [22, true], 51 | ['22.222', true], 52 | ['.22', true], 53 | ['22.', true], 54 | ['.', false], 55 | ['22.22.22', false], 56 | ['22,22,22', false], 57 | ['22_22_22', false], 58 | ]; 59 | 60 | for (let [input, expectedIsValid] of testThese) { 61 | const isValid = getIsInputValid(input.toString()); 62 | 63 | specify(`input: ${input}`, function () { 64 | assert.equal(expectedIsValid, isValid); 65 | }); 66 | } 67 | }); 68 | 69 | describe('toDecimalString', function () { 70 | const testThese = [ 71 | [0n, 5, '0'], 72 | [1n, 1, '0.1'], 73 | [-1n, 1, '-0.1'], 74 | [100n, 3, '0.1'], 75 | [-100n, 3, '-0.1'], 76 | [22n, 0, '22'], 77 | [-22n, 0, '-22'], 78 | [22n, 1, '2.2'], 79 | [-22n, 1, '-2.2'], 80 | [22n, 2, '0.22'], 81 | [-22n, 2, '-0.22'], 82 | [22n, 3, '0.022'], 83 | [-22n, 3, '-0.022'], 84 | [222000n, 3, '222'], 85 | [-222000n, 3, '-222'], 86 | [222100n, 3, '222.1'], 87 | [-222100n, 3, '-222.1'], 88 | ]; 89 | 90 | for (let [value, precision, expectedDecimalString] of testThese) { 91 | const decimalString = toDecimalString( 92 | value as bigint, 93 | precision as number 94 | ); 95 | 96 | specify(`input: ${value}, ${precision}`, function () { 97 | assert.equal(expectedDecimalString, decimalString); 98 | }); 99 | } 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /tests/preciseNumber/preciseNumberOperator.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import 'mocha'; 3 | import { getPreciseNumberMaker } from '../../index'; 4 | import { Input } from '../types'; 5 | 6 | export type Operator = 'add' | 'sub' | 'mul' | 'div'; 7 | export interface OperatorTestSingle { 8 | a: Input; 9 | b: Input; 10 | value: number; 11 | integer: bigint; 12 | type: Operator; 13 | precision: number; 14 | } 15 | 16 | export const getVals = ( 17 | a: Input, 18 | b: Input, 19 | value: number, 20 | integer: bigint, 21 | type: Operator, 22 | precision: number 23 | ): OperatorTestSingle => ({ a, b, value, integer, type, precision }); 24 | 25 | describe('PreciseNumber, Operators', function () { 26 | context('Single values', function () { 27 | const testThese: OperatorTestSingle[] = [ 28 | getVals(0.1, 0.2, 0.3, 30n, 'add', 2), 29 | getVals(0.1, 0.2, -0.1, -10n, 'sub', 2), 30 | getVals(0.1, -0.2, -0.1, -10n, 'add', 2), 31 | getVals(55.55, 0.2, 55.35, 5535n, 'sub', 2), 32 | getVals(55.55, -0.2, 55.35, 5535n, 'add', 2), 33 | getVals(-55.55, 0.2, -55.75, -5575n, 'sub', 2), 34 | getVals(-55.55, -0.2, -55.75, -5575n, 'add', 2), 35 | getVals(0.555, 0.001, 0.56, 56n, 'add', 2), 36 | getVals(0.555, 0.001, 0.56, 56n, 'sub', 2), 37 | getVals(0.555, 0.001, 0.554, 554n, 'sub', 3), 38 | getVals(0.555, 0.001, 0.554, 5540n, 'sub', 4), 39 | getVals(0.555, -0.001, 0.56, 56n, 'add', 2), 40 | getVals(0, -0.001, 0, 0n, 'add', 2), 41 | getVals(0, -0.01, -0.01, -1n, 'add', 2), 42 | getVals(1, 1, 1, 100n, 'mul', 2), 43 | getVals(1.1, 1, 1.1, 110n, 'mul', 2), 44 | getVals(1.1, 0.55, 0.61, 61n, 'mul', 2), 45 | getVals(1.1, 0.54, 0.59, 59n, 'mul', 2), 46 | getVals(1.1, 0.54, 0.594, 594n, 'mul', 3), 47 | getVals(1.1, 0.55, 0.605, 605n, 'mul', 3), 48 | getVals(3.3, 3.3, 10.89, 1089n, 'mul', 2), 49 | getVals(3.3, 3.3, 10.89, 10890n, 'mul', 3), 50 | getVals(55, 2, 27.5, 2750n, 'div', 2), 51 | getVals(5.5, 2, 2.75, 275n, 'div', 2), 52 | getVals(Math.PI, Math.E, 9, 9n, 'mul', 0), 53 | getVals(Math.PI, Math.E, 8.4, 84n, 'mul', 1), 54 | getVals(Math.PI, Math.E, 8.54, 854n, 'mul', 2), 55 | getVals(Math.PI, Math.E, 8.54, 8540n, 'mul', 3), 56 | getVals(Math.PI, Math.E, 8.539734222673566, 8539734222673566n, 'mul', 15), 57 | ]; 58 | 59 | for (let { a, b, value, integer, type, precision } of testThese) { 60 | const p = getPreciseNumberMaker(precision); 61 | specify(`${type}: ${a}, ${b}`, () => { 62 | const final = p(a)[type](b); 63 | assert.strictEqual(final.integer, integer); 64 | assert.strictEqual(final.value, value); 65 | }); 66 | } 67 | }); 68 | 69 | context('Checking immutability', function () { 70 | const precision = 6; 71 | const p = getPreciseNumberMaker(precision); 72 | const number = p(100.0); 73 | const result = number.add(22).sub(33).mul(100).div(5); 74 | 75 | specify('checking initial', function () { 76 | assert.strictEqual(number.integer, 100000000n); 77 | assert.strictEqual(number.value, 100); 78 | assert.strictEqual(number.precision, precision); 79 | }); 80 | 81 | specify('checking result', function () { 82 | assert.strictEqual(result.integer, 1780000000n); 83 | assert.strictEqual(result.value, 1780); 84 | assert.strictEqual(result.precision, precision); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export type ScalerInput = string | number; 2 | 3 | export function scaler(value: ScalerInput, precision: number): bigint { 4 | if (typeof value === 'string') { 5 | value = replaceDelimiters(value); 6 | throwIfNotValid(value); 7 | value = stripZeros(value); 8 | } 9 | 10 | const stringRep = typeof value === 'number' ? value + '' : value; 11 | const floatRep = typeof value === 'string' ? Number(value) : value; 12 | 13 | const parts = stringRep.split('.'); 14 | const whole = parts[0]; 15 | const fractional = parts[1] ?? ''; 16 | const fractionalLength = fractional.length; 17 | 18 | let stringBigRep = whole + fractional; 19 | stringBigRep = stringBigRep === '-0' ? '0' : stringBigRep; 20 | let bigRep = BigInt(stringBigRep); 21 | 22 | if (precision > fractionalLength) { 23 | bigRep *= 10n ** BigInt(precision - fractionalLength); 24 | } else { 25 | bigRep /= 10n ** BigInt(fractionalLength - precision); 26 | if (Number(fractional[precision]) > 4) { 27 | bigRep += floatRep >= 0 ? 1n : -1n; 28 | } 29 | } 30 | 31 | return bigRep; 32 | } 33 | 34 | function replaceDelimiters(value: string): string { 35 | return value.replace(/[_,]/g, ''); 36 | } 37 | 38 | export function toDecimalString(value: bigint, precision: number): string { 39 | const isNegative = value < 0; 40 | let stringRep = value + ''; 41 | stringRep = isNegative ? stringRep.slice(1) : stringRep; 42 | 43 | const d = stringRep.length - precision; 44 | const sign = isNegative ? '-' : ''; 45 | 46 | if (d < 0) { 47 | return sign + stripZeros('0.' + '0'.repeat(Math.abs(d)) + stringRep); 48 | } else if (d === 0) { 49 | return sign + stripZeros('0.' + stringRep); 50 | } 51 | 52 | const whole = stringRep.slice(0, d) || '0'; 53 | const fractional = stringRep.slice(d); 54 | 55 | if (fractional.length) { 56 | return (sign + stripZeros(`${whole}.${fractional}`)) as string; 57 | } 58 | return sign + whole; 59 | } 60 | 61 | function stripZeros(value: string): string { 62 | if (value.includes('.') && value.endsWith('0')) { 63 | let [fractional, whole] = (value as string).split('.'); 64 | whole = whole.replace(/0*$/, ''); 65 | whole = whole.length > 0 ? `.${whole}` : whole; 66 | return fractional + whole; 67 | } 68 | 69 | return value; 70 | } 71 | 72 | export function getIsCurrencyCode(code: string): boolean { 73 | return !!code.match(/^[A-Z]{3}$/); 74 | } 75 | 76 | export function throwIfInvalidCurrencyCode(code: string) { 77 | if (!getIsCurrencyCode(code)) { 78 | throw Error(`invalid currency code '${code}'`); 79 | } 80 | } 81 | 82 | export function getConversionRateKey(from: string, to: string) { 83 | const keys = [from, to]; 84 | keys.forEach(throwIfInvalidCurrencyCode); 85 | return keys.join('-'); 86 | } 87 | 88 | export function matchPrecision( 89 | value: bigint, 90 | from: number, 91 | to: number 92 | ): bigint { 93 | if (from > to) { 94 | return value / 10n ** BigInt(from - to); 95 | } else if (from < to) { 96 | return value * 10n ** BigInt(to - from); 97 | } 98 | return value; 99 | } 100 | 101 | function throwIfNotValid(value: string) { 102 | if (getIsInputValid(value as string)) { 103 | return; 104 | } 105 | 106 | throw Error(`invalid input '${value}' of type '${typeof value}'`); 107 | } 108 | 109 | export function getIsInputValid(value: string): boolean { 110 | // regex for: ['22.22', '.22', '22.'] 111 | const hasMatch = value.match(/^-?\d*(?:(?:\.?\d)|(?:\d\.?))\d*$/); 112 | return Boolean(hasMatch); 113 | } 114 | 115 | export function throwRateNotProvided(from: string, to: string) { 116 | throw Error(`rate not provided for conversion from ${from} to ${to}`); 117 | } 118 | 119 | export function scalerNumber(value: ScalerInput, precision: number) { 120 | if (typeof value === 'string') { 121 | return scaler(value, precision); 122 | } 123 | 124 | // const sign = Math.sign(value); 125 | // const abs = Math.abs(value); 126 | 127 | const frac = (value + '').split('.')[1]; 128 | const fracLength = frac?.length; 129 | 130 | let fracBig = 0n; 131 | if (frac && fracLength < precision) { 132 | fracBig = BigInt(frac) * 10n ** BigInt(precision - fracLength); 133 | } 134 | 135 | if (frac && fracLength >= precision) { 136 | fracBig = BigInt(frac.substring(0, precision)); 137 | } 138 | 139 | const wholeBig = BigInt(Math.trunc(value)) * 10n ** BigInt(precision); 140 | const big = wholeBig + fracBig; 141 | 142 | if (fracBig % 10n > 4n) { 143 | return big + (wholeBig > 0n ? 1n : -1n); 144 | } 145 | 146 | return big; 147 | } 148 | -------------------------------------------------------------------------------- /tests/preciseNumber/preciseNumberComparator.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import 'mocha'; 3 | import { getPreciseNumberMaker } from '../../index'; 4 | import { Input } from '../types'; 5 | 6 | export type Comparator = 'eq' | 'gt' | 'lt' | 'gte' | 'lte'; 7 | export interface ComparatorTest { 8 | a: Input; 9 | b: Input; 10 | output: boolean; 11 | type: Comparator; 12 | } 13 | 14 | export const getVals = ( 15 | a: Input, 16 | b: Input, 17 | output: boolean, 18 | type: Comparator 19 | ): ComparatorTest => ({ 20 | a, 21 | b, 22 | output, 23 | type, 24 | }); 25 | 26 | describe('PreciseNumber, Comparators', function () { 27 | context('Single values', function () { 28 | const testThese: ComparatorTest[] = [ 29 | getVals(0.1, 0.2, false, 'eq'), 30 | getVals(0.1, 0.2, true, 'lt'), 31 | getVals(0.1, 0.2, false, 'gt'), 32 | getVals(0.1, 0.2, true, 'lte'), 33 | getVals(0.1, 0.2, false, 'gte'), 34 | getVals(0.2, 0.2, true, 'eq'), 35 | getVals(0.2, 0.2, false, 'lt'), 36 | getVals(0.2, 0.2, false, 'gt'), 37 | getVals(0.2, 0.2, true, 'lte'), 38 | getVals(0.2, 0.2, true, 'gte'), 39 | getVals(0.2, 0.1, false, 'eq'), 40 | getVals(0.2, 0.1, false, 'lt'), 41 | getVals(0.2, 0.1, true, 'gt'), 42 | getVals(0.2, 0.1, false, 'lte'), 43 | getVals(0.2, 0.1, true, 'gte'), 44 | ]; 45 | 46 | for (let { a, b, output, type } of testThese) { 47 | specify(`${type}: ${a}, ${b}`, () => { 48 | const p = getPreciseNumberMaker(1); 49 | const result = p(a)[type](b); 50 | assert.strictEqual(output, result); 51 | }); 52 | 53 | specify(`${type}: ${a}, ${b}`, () => { 54 | const p = getPreciseNumberMaker(6); 55 | const result = p(a)[type](b); 56 | assert.strictEqual(output, result); 57 | }); 58 | } 59 | }); 60 | 61 | context('Immutability', function () { 62 | context('one', function () { 63 | const p = getPreciseNumberMaker(6); 64 | const a = p(0.1); 65 | const b = a.add(0.2); 66 | 67 | specify('one.a', function () { 68 | assert.strictEqual(a.integer, 100000n); 69 | assert.strictEqual(a.value, 0.1); 70 | assert.strictEqual(b.integer, 300000n); 71 | assert.strictEqual(b.value, 0.3); 72 | }); 73 | 74 | specify('one.b', function () { 75 | assert.strictEqual(a.eq(b), false); 76 | assert.strictEqual(a.lt(b), true); 77 | assert.strictEqual(a.gt(b), false); 78 | assert.strictEqual(a.lte(b), true); 79 | assert.strictEqual(a.gte(b), false); 80 | }); 81 | 82 | specify('one.c', function () { 83 | assert.strictEqual(a.eq(0.1), true); 84 | assert.strictEqual(a.eq(p(0.1)), true); 85 | assert.strictEqual(b.eq(0.3), true); 86 | assert.strictEqual(b.eq(p(0.3)), true); 87 | }); 88 | }); 89 | 90 | context('two', function () { 91 | const p = getPreciseNumberMaker(6); 92 | const a = p(0.1).mul(0.1).div(2); 93 | const b = a.add(0.2); 94 | 95 | specify('one.a', function () { 96 | assert.strictEqual(a.integer, 5000n); 97 | assert.strictEqual(a.value, 0.005); 98 | assert.strictEqual(b.integer, 205000n); 99 | assert.strictEqual(b.value, 0.205); 100 | }); 101 | 102 | specify('one.b', function () { 103 | assert.strictEqual(a.eq(b), false); 104 | assert.strictEqual(a.lt(b), true); 105 | assert.strictEqual(a.gt(b), false); 106 | assert.strictEqual(a.lte(b), true); 107 | assert.strictEqual(a.gte(b), false); 108 | }); 109 | 110 | specify('one.c', function () { 111 | assert.strictEqual(a.eq(0.005), true); 112 | assert.strictEqual(a.eq(p(0.005)), true); 113 | assert.strictEqual(b.eq(0.205), true); 114 | assert.strictEqual(b.eq(p(0.205)), true); 115 | }); 116 | }); 117 | }); 118 | 119 | describe('checks', function () { 120 | const p = getPreciseNumberMaker(); 121 | specify('isZero', function () { 122 | assert.strictEqual(p(0).isZero(), true); 123 | assert.strictEqual(p(220).sub(220).isZero(), true); 124 | assert.strictEqual(p(220).mul(0).isZero(), true); 125 | assert.strictEqual(p(0).mul(20).isZero(), true); 126 | }); 127 | 128 | specify('isPositive', function () { 129 | assert.strictEqual(p(0).isPositive(), false); 130 | assert.strictEqual(p(1).isPositive(), true); 131 | assert.strictEqual(p(220).sub(219).isPositive(), true); 132 | assert.strictEqual(p(220).mul(0.000001).isPositive(), true); 133 | assert.strictEqual(p(0).mul(20).isPositive(), false); 134 | }); 135 | 136 | specify('isNegative', function () { 137 | assert.strictEqual(p(0).isNegative(), false); 138 | assert.strictEqual(p(-1).isNegative(), true); 139 | assert.strictEqual(p(220).sub(221).isNegative(), true); 140 | assert.strictEqual(p(220).mul(-0.000001).isNegative(), true); 141 | assert.strictEqual(p(-0).mul(20).isNegative(), false); 142 | assert.strictEqual(p(-0).mul(-20).isNegative(), false); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /tests/preciseNumber/preciseNumberMaker.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import 'mocha'; 3 | import { p } from '../../index'; 4 | import { DEF_PREC, MAX_PREC, MIN_PREC } from '../../src/consts'; 5 | import { Input } from '../types'; 6 | 7 | interface Test { 8 | input: Input; 9 | precision: number; 10 | value: number; 11 | integer: bigint; 12 | } 13 | 14 | interface InvalidTest { 15 | input: Input; 16 | precision: number; 17 | } 18 | 19 | const getVals = ( 20 | input: Input, 21 | precision: number, 22 | value: number, 23 | integer: bigint 24 | ): Test => ({ 25 | input, 26 | precision, 27 | value, 28 | integer, 29 | }); 30 | 31 | const getInvalidVals = (input: Input, precision: number): InvalidTest => ({ 32 | input, 33 | precision, 34 | }); 35 | 36 | describe('PreciseNumber, Maker', function () { 37 | context('valid inputs', function () { 38 | const testThese = [ 39 | getVals(-0, 10, 0, 0n), 40 | getVals(2.555, 0, 3, 3n), 41 | getVals(-2.555, 0, -3, -3n), 42 | getVals(2.555, 2, 2.56, 256n), 43 | getVals(-2.555, 2, -2.56, -256n), 44 | getVals(2.555, 11, 2.555, 255500000000n), 45 | getVals(-2.555, 11, -2.555, -255500000000n), 46 | getVals('.555', 0, 1, 1n), 47 | getVals('-.555', 0, -1, -1n), 48 | getVals('.455', 0, 0, 0n), 49 | getVals('-.455', 0, 0, 0n), 50 | getVals('0', 10, 0, 0n), 51 | getVals('-0', 10, 0, 0n), 52 | getVals('0.1', 10, 0.1, 1000000000n), 53 | getVals('2.555', 0, 3, 3n), 54 | getVals('-2.555', 0, -3, -3n), 55 | getVals('2.555', 2, 2.56, 256n), 56 | getVals('-2.555', 2, -2.56, -256n), 57 | getVals('2.555', 11, 2.555, 255500000000n), 58 | getVals('-2.555', 11, -2.555, -255500000000n), 59 | getVals('-0.000001', 20, -0.000001, -100000000000000n), 60 | getVals('200', 3, 200, 200000n), 61 | getVals('00200', 3, 200, 200000n), 62 | getVals('.00200', 3, 0.002, 2n), 63 | getVals(-0.002, 3, -0.002, -2n), 64 | getVals('-00.002', 3, -0.002, -2n), 65 | getVals( 66 | '200_000_000_000_000.18', 67 | 2, 68 | 200000000000000.16, 69 | 20000000000000018n 70 | ), // kilo Jeff 71 | getVals(2n, 3, 0.002, 2n), 72 | ]; 73 | 74 | for (let test of testThese) { 75 | const { input, precision, value, integer } = test; 76 | 77 | specify(`input: ${input}`, function () { 78 | const pn = p(input, precision); 79 | assert.strictEqual(pn.value, value); 80 | assert.strictEqual(pn.integer, integer); 81 | }); 82 | } 83 | }); 84 | 85 | context('invalid inputs', function () { 86 | const testThese = [ 87 | getInvalidVals(2.5, MIN_PREC - 1), 88 | getInvalidVals(2.5, MAX_PREC + 1), 89 | getInvalidVals('', DEF_PREC), 90 | getInvalidVals('.', DEF_PREC), 91 | getInvalidVals('2.555.5', DEF_PREC), 92 | getInvalidVals('.555.5', DEF_PREC), 93 | getInvalidVals('1|000|000.000', DEF_PREC), 94 | getInvalidVals('1-000-000.000', DEF_PREC), 95 | ]; 96 | for (let test of testThese) { 97 | const { input, precision } = test; 98 | 99 | specify(`input: ${input}`, function () { 100 | assert.throws(() => { 101 | p(input, precision); 102 | }); 103 | }); 104 | } 105 | }); 106 | }); 107 | 108 | describe('PreciseNumber, Other functions', function () { 109 | context('Round, traditional', function () { 110 | const testThese: [number | string, number, string, number][] = [ 111 | [0.5, 6, '1', 0], 112 | [1.5, 6, '2', 0], 113 | [2.5, 6, '3', 0], 114 | [3.5, 6, '4', 0], 115 | [-0.5, 6, '0', 0], 116 | [-1.5, 6, '-1', 0], 117 | [-2.5, 6, '-2', 0], 118 | [-3.5, 6, '-3', 0], 119 | [-1.51, 6, '-2', 0], 120 | [-1.5000001, 6, '-1', 0], 121 | [-1.51, 6, '-2', 0], 122 | [1234.5678, 4, '1235', -1], 123 | [1234.5678, 4, '1235', 0], 124 | [1234.5678, 4, '1234.6', 1], 125 | [1234.5678, 4, '1234.57', 2], 126 | [1234.5678, 4, '1234.568', 3], 127 | [1234.5678, 4, '1234.5678', 4], 128 | [1234.5678, 4, '1234.56780', 5], 129 | [1234.5678, 4, '1234.567800', 6], 130 | [-1234.5678, 4, '-1235', -1], 131 | [-1234.5678, 4, '-1235', 0], 132 | [-1234.5678, 4, '-1234.6', 1], 133 | [-1234.5678, 4, '-1234.57', 2], 134 | [-1234.5678, 4, '-1234.568', 3], 135 | [-1234.5678, 4, '-1234.5678', 4], 136 | [-1234.5678, 4, '-1234.56780', 5], 137 | [-1234.5678, 4, '-1234.567800', 6], 138 | ['200_000_000_000_000.18', 4, '200000000000000.18', 2], 139 | ['0.000000031032882086386885', 30, '0.0000000310328820863868850', 25], 140 | ]; 141 | 142 | for (let test of testThese) { 143 | const [input, precision, expectedOutput, to] = test; 144 | const output = p(input, precision).round(to, false); 145 | specify(`input: ${input}`, function () { 146 | assert.strictEqual(output, expectedOutput); 147 | }); 148 | } 149 | }); 150 | 151 | context('Round, bankers', function () { 152 | const testThese: [number | string, number, string, number][] = [ 153 | [0.5, 6, '0', 0], 154 | [1.5, 6, '2', 0], 155 | [2.5, 6, '2', 0], 156 | [3.5, 6, '4', 0], 157 | [-0.5, 6, '0', 0], 158 | [-1.5, 6, '-2', 0], 159 | [-2.5, 6, '-2', 0], 160 | [-3.5, 6, '-4', 0], 161 | [0.15, 6, '0.2', 1], 162 | [1.15, 6, '1.2', 1], 163 | [2.15, 6, '2.2', 1], 164 | [3.15, 6, '3.2', 1], 165 | [-0.15, 6, '-0.2', 1], 166 | [-1.15, 6, '-1.2', 1], 167 | [-2.15, 6, '-2.2', 1], 168 | [-3.15, 6, '-3.2', 1], 169 | [0.115, 6, '0.12', 2], 170 | [1.115, 6, '1.12', 2], 171 | [2.115, 6, '2.12', 2], 172 | [3.115, 6, '3.12', 2], 173 | [0.115, 6, '0.12', 2], 174 | [1.125, 6, '1.12', 2], 175 | [2.135, 6, '2.14', 2], 176 | [3.145, 6, '3.14', 2], 177 | ]; 178 | 179 | for (let test of testThese) { 180 | const [input, precision, expectedOutput, to] = test; 181 | const output = p(input, precision).round(to, true); 182 | specify(`input: ${input}`, function () { 183 | assert.strictEqual(output, expectedOutput); 184 | }); 185 | } 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /src/preciseNumber.ts: -------------------------------------------------------------------------------- 1 | import { DEF_PREC, MAX_PREC, MIN_PREC, USE_BNKR } from './consts'; 2 | import { matchPrecision, scaler, toDecimalString } from './utils'; 3 | 4 | const enum Operator { 5 | Add = '+', 6 | Sub = '-', 7 | Mul = '*', 8 | Div = '/', 9 | } 10 | 11 | const enum Comparator { 12 | Eq = '===', 13 | Gt = '>', 14 | Lt = '<', 15 | Gte = '>=', 16 | Lte = '<=', 17 | } 18 | 19 | type Input = PreciseNumber | bigint | number | string; 20 | 21 | export default class PreciseNumber { 22 | #neutralizer: bigint; 23 | #precision: number; 24 | #value: bigint; 25 | #bankersRounding: boolean; 26 | 27 | /* --------------------------------- 28 | * Constructor 29 | * ---------------------------------*/ 30 | 31 | constructor( 32 | value: Input = 0n, 33 | precision: number = DEF_PREC, 34 | bankersRounding: boolean = USE_BNKR 35 | ) { 36 | this.#precision = validateAndGetPrecision(precision); 37 | this.#bankersRounding = bankersRounding; 38 | this.#neutralizer = 10n ** BigInt(this.#precision); 39 | this.#value = scaleAndConvert(value, this.precision); 40 | } 41 | 42 | /* --------------------------------- 43 | * Utility methods 44 | * ---------------------------------*/ 45 | 46 | round(to: number, bankersRounding?: boolean): string { 47 | bankersRounding ??= this.#bankersRounding; 48 | throwIfInvalidInput(to); 49 | to = to >= 0 ? to : 0; 50 | 51 | const diff = to - this.#precision; 52 | const isNeg = this.#value < 0; 53 | let stringRep = this.#value.toString().slice(isNeg ? 1 : 0); 54 | if (stringRep.length < this.precision) { 55 | stringRep = '0'.repeat(this.precision - stringRep.length) + stringRep; 56 | } 57 | const dpoint = stringRep.length - this.precision; 58 | const whole = stringRep.slice(0, dpoint) || '0'; 59 | const fraction = stringRep.slice(dpoint); 60 | const trailingZeros = '0'.repeat(Math.max(0, diff)); 61 | const ultimateDigit = Number(fraction[to]); 62 | const isMid = 63 | ultimateDigit === 5 && 64 | [...fraction.slice(to + 1)].every((d) => d === '0'); 65 | let roundingDigit = 66 | (ultimateDigit || 0) <= 4 ? 0n : isNeg && isMid ? 0n : 1n; 67 | 68 | if (bankersRounding && isMid && trailingZeros.length === 0) { 69 | const penultimateDigit = 70 | Number(to - 1 >= 0 ? fraction[to - 1] : whole[dpoint - 1]) || 0; 71 | roundingDigit = penultimateDigit % 2 === 0 ? 0n : 1n; 72 | } 73 | 74 | let lowPrescisionRep = ( 75 | BigInt(whole + fraction.slice(0, to) + trailingZeros) + roundingDigit 76 | ).toString(); 77 | if (lowPrescisionRep.length < to) { 78 | lowPrescisionRep = 79 | '0'.repeat(to - lowPrescisionRep.length) + lowPrescisionRep; 80 | } 81 | const newDpoint = lowPrescisionRep.length - to; 82 | const newWhole = lowPrescisionRep.slice(0, newDpoint) || '0'; 83 | const newFractional = lowPrescisionRep.slice(newDpoint); 84 | const tail = '.' + newFractional + '0'.repeat(to - newFractional.length); 85 | const noSign = newWhole + (tail !== '.' ? tail : ''); 86 | return ( 87 | (!isNeg || [...noSign].filter((d) => d !== '.').every((d) => d === '0') 88 | ? '' 89 | : '-') + noSign 90 | ); 91 | } 92 | 93 | clip(to: number) { 94 | throwIfInvalidInput(to); 95 | return new PreciseNumber(this.round(to), this.#precision); 96 | } 97 | 98 | copy() { 99 | return new PreciseNumber(this.#value, this.#precision); 100 | } 101 | 102 | /* --------------------------------- 103 | * Getters and Setters 104 | * ---------------------------------*/ 105 | 106 | get value(): number { 107 | return Number(this.#value) / Math.pow(10, this.#precision); 108 | } 109 | 110 | get float(): number { 111 | return this.value; 112 | } 113 | 114 | get integer(): bigint { 115 | return this.#value; 116 | } 117 | 118 | get precision(): number { 119 | return this.#precision; 120 | } 121 | 122 | get store(): string { 123 | return this.round(this.precision); 124 | } 125 | 126 | set precision(precision: number) { 127 | precision = validateAndGetPrecision(precision); 128 | 129 | this.#value = matchPrecision(this.#value, this.precision, this.#precision); 130 | this.#precision = precision; 131 | this.#neutralizer = getNeutralizer(precision); 132 | } 133 | 134 | /* --------------------------------- 135 | * Operator Methods 136 | * ---------------------------------*/ 137 | 138 | add(...values: Input[]): PreciseNumber { 139 | return executeOperation( 140 | Operator.Add, 141 | [this, values].flat(), 142 | this.precision, 143 | this.#neutralizer 144 | ); 145 | } 146 | 147 | sub(...values: Input[]): PreciseNumber { 148 | return executeOperation( 149 | Operator.Sub, 150 | [this, values].flat(), 151 | this.precision, 152 | this.#neutralizer 153 | ); 154 | } 155 | 156 | mul(...values: Input[]): PreciseNumber { 157 | return executeOperation( 158 | Operator.Mul, 159 | [this, values].flat(), 160 | this.precision, 161 | this.#neutralizer 162 | ); 163 | } 164 | 165 | div(...values: Input[]): PreciseNumber { 166 | return executeOperation( 167 | Operator.Div, 168 | [this, values].flat(), 169 | this.precision, 170 | this.#neutralizer 171 | ); 172 | } 173 | 174 | /* --------------------------------- 175 | * Comparator Methods 176 | * ---------------------------------*/ 177 | 178 | eq(value: Input): boolean { 179 | return executeComparison(Comparator.Eq, this, value, this.precision); 180 | } 181 | 182 | gt(value: Input): boolean { 183 | return executeComparison(Comparator.Gt, this, value, this.precision); 184 | } 185 | 186 | lt(value: Input): boolean { 187 | return executeComparison(Comparator.Lt, this, value, this.precision); 188 | } 189 | 190 | gte(value: Input): boolean { 191 | return executeComparison(Comparator.Gte, this, value, this.precision); 192 | } 193 | 194 | lte(value: Input): boolean { 195 | return executeComparison(Comparator.Lte, this, value, this.precision); 196 | } 197 | 198 | /* --------------------------------- 199 | * Static Configuration 200 | * ---------------------------------*/ 201 | 202 | static #prec: number = DEF_PREC; 203 | 204 | static #neut: bigint = getNeutralizer(DEF_PREC); 205 | 206 | static get prec() { 207 | return this.#prec; 208 | } 209 | 210 | static set prec(value: number) { 211 | value = validateAndGetPrecision(value); 212 | this.#prec = value; 213 | this.#neut = getNeutralizer(value); 214 | } 215 | 216 | /* --------------------------------- 217 | * Static Operator Methods 218 | * ---------------------------------*/ 219 | 220 | static add(...values: Input[]): PreciseNumber { 221 | return executeOperation(Operator.Add, values, this.prec, this.#neut); 222 | } 223 | 224 | static sub(...values: Input[]): PreciseNumber { 225 | return executeOperation(Operator.Sub, values, this.prec, this.#neut); 226 | } 227 | 228 | static mul(...values: Input[]): PreciseNumber { 229 | return executeOperation(Operator.Sub, values, this.prec, this.#neut); 230 | } 231 | 232 | static div(...values: Input[]): PreciseNumber { 233 | return executeOperation(Operator.Sub, values, this.prec, this.#neut); 234 | } 235 | 236 | /* --------------------------------- 237 | * Static Comparator Methods 238 | * ---------------------------------*/ 239 | 240 | static eq(valueA: Input, valueB: Input): boolean { 241 | return executeComparison(Comparator.Eq, valueA, valueB, this.prec); 242 | } 243 | 244 | static gt(valueA: Input, valueB: Input): boolean { 245 | return executeComparison(Comparator.Gt, valueA, valueB, this.prec); 246 | } 247 | 248 | static lt(valueA: Input, valueB: Input): boolean { 249 | return executeComparison(Comparator.Lt, valueA, valueB, this.prec); 250 | } 251 | 252 | static gte(valueA: Input, valueB: Input): boolean { 253 | return executeComparison(Comparator.Gte, valueA, valueB, this.prec); 254 | } 255 | 256 | static lte(valueA: Input, valueB: Input): boolean { 257 | return executeComparison(Comparator.Lte, valueA, valueB, this.prec); 258 | } 259 | 260 | /* --------------------------------- 261 | * Checks 262 | * ---------------------------------*/ 263 | 264 | isPositive(): boolean { 265 | return this.#value > 0n; 266 | } 267 | 268 | isNegative(): boolean { 269 | return this.#value < 0n; 270 | } 271 | 272 | isZero(): boolean { 273 | return this.#value === 0n; 274 | } 275 | 276 | /* --------------------------------- 277 | * Special methods 278 | * ---------------------------------*/ 279 | 280 | toString(): string { 281 | return toDecimalString(this.#value, this.#precision); 282 | } 283 | 284 | toJSON(): string { 285 | return toDecimalString(this.#value, this.#precision); 286 | } 287 | 288 | valueOf(): bigint { 289 | return this.#value; 290 | } 291 | } 292 | 293 | function validateAndGetPrecision(precision: number) { 294 | precision = Math.round(precision); 295 | if (precision > MAX_PREC || precision < MIN_PREC) { 296 | throw Error(`precision should be between ${MIN_PREC} and ${MAX_PREC}`); 297 | } 298 | 299 | return precision; 300 | } 301 | 302 | function throwIfInvalidInput(...values: (Input | bigint)[]) { 303 | values.forEach((value) => { 304 | if (value === 0 || value === 0n) { 305 | return; 306 | } 307 | 308 | if ( 309 | Number.isNaN(value) || 310 | value === Infinity || 311 | value === -Infinity || 312 | !value 313 | ) { 314 | throw Error(`invalid value ${value} found`); 315 | } 316 | }); 317 | } 318 | 319 | function scaleAndConvert(value: Input, precision: number): bigint { 320 | if (typeof value === 'bigint') { 321 | return value; 322 | } 323 | 324 | if (value instanceof PreciseNumber) { 325 | return matchPrecision(value.integer, value.precision, precision); 326 | } 327 | 328 | return scaler(value, precision); 329 | } 330 | 331 | function neutralizedMul( 332 | product: bigint, 333 | precision: number, 334 | neutralizer: bigint 335 | ) { 336 | const final = product / neutralizer; 337 | const temp = product.toString(); 338 | const roundingNum = 339 | Number(temp.charAt(temp.length - precision) || '0') > 4 ? 1n : 0n; 340 | 341 | return final + roundingNum; 342 | } 343 | 344 | function getNeutralizer(precision: number) { 345 | return 10n ** BigInt(precision); 346 | } 347 | 348 | function executeOperation( 349 | operator: Operator, 350 | values: Input[], 351 | precision: number, 352 | neutralizer: bigint 353 | ): PreciseNumber { 354 | throwIfInvalidInput(...values); 355 | 356 | const prAmounts: bigint[] = values.map((val) => 357 | scaleAndConvert(val, precision) 358 | ); 359 | 360 | const finalAmount: bigint = prAmounts.reduce((a, b) => { 361 | switch (operator) { 362 | case Operator.Add: 363 | return a + b; 364 | case Operator.Sub: 365 | return a - b; 366 | case Operator.Div: 367 | return (a * (neutralizer ?? getNeutralizer(precision))) / b; 368 | case Operator.Mul: 369 | return neutralizedMul(a * b, precision, neutralizer); 370 | default: 371 | return 0n; 372 | } 373 | }); 374 | 375 | return new PreciseNumber(finalAmount, precision); 376 | } 377 | 378 | function executeComparison( 379 | comparator: Comparator, 380 | valueA: Input, 381 | valueB: Input, 382 | precision: number 383 | ): boolean { 384 | throwIfInvalidInput(valueA, valueB); 385 | 386 | const prAmountA = scaleAndConvert(valueA, precision); 387 | const prAmountB = scaleAndConvert(valueB, precision); 388 | 389 | switch (comparator) { 390 | case '===': 391 | return prAmountA === prAmountB; 392 | case '>': 393 | return prAmountA > prAmountB; 394 | case '<': 395 | return prAmountA < prAmountB; 396 | case '>=': 397 | return prAmountA >= prAmountB; 398 | case '<=': 399 | return prAmountA <= prAmountB; 400 | default: 401 | return false; 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /tests/money/money.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import 'mocha'; 3 | import { getMoneyMaker, pesa } from '../../index'; 4 | import Money from '../../src/money'; 5 | 6 | describe('Money', function () { 7 | describe('currency', function () { 8 | specify('string input', function () { 9 | assert.strictEqual(pesa(200, 'USD').getCurrency(), 'USD'); 10 | assert.strictEqual(pesa(200).currency('USD').getCurrency(), 'USD'); 11 | assert.throws(function () { 12 | pesa(200, 'usd'); 13 | }); 14 | assert.throws(function () { 15 | pesa(200).currency('usd'); 16 | }); 17 | }); 18 | }); 19 | 20 | describe('rate', function () { 21 | specify('rate, string input', function () { 22 | const rate = 75; 23 | const money = pesa(200, 'USD').rate('INR', rate); 24 | assert.strictEqual(money.hasConversionRate('INR'), true); 25 | assert.strictEqual(money.to('INR').hasConversionRate('USD'), true); 26 | assert.strictEqual(money.getConversionRate('USD', 'INR'), rate); 27 | assert.strictEqual(money.getConversionRate('INR', 'USD'), 1 / rate); 28 | assert.strictEqual(money.to('INR').float, 15000); 29 | assert.strictEqual(money.to('INR').to('USD').float, 199.995); 30 | assert.strictEqual(money.to('INR').to('USD').round(2), '200.00'); 31 | }); 32 | 33 | specify('rate, RateSetting input', function () { 34 | const rate = 75; 35 | const money = pesa(200, 'USD').rate({ from: 'USD', to: 'INR', rate }); 36 | assert.strictEqual(money.hasConversionRate('INR'), true); 37 | assert.strictEqual(money.to('INR').hasConversionRate('USD'), true); 38 | assert.strictEqual(money.getConversionRate('USD', 'INR'), rate); 39 | assert.strictEqual(money.getConversionRate('INR', 'USD'), 1 / rate); 40 | assert.strictEqual(money.to('INR').float, 15000); 41 | assert.strictEqual(money.to('INR').to('USD').float, 199.995); 42 | assert.strictEqual(money.to('INR').to('USD').round(2), '200.00'); 43 | }); 44 | 45 | specify('rate, RateSetting input', function () { 46 | const rates = [75, 0.013]; 47 | const money = pesa(200, 'USD').rate([ 48 | { from: 'USD', to: 'INR', rate: rates[0] }, 49 | { from: 'INR', to: 'USD', rate: rates[1] }, 50 | ]); 51 | assert.strictEqual(money.hasConversionRate('INR'), true); 52 | assert.strictEqual(money.to('INR').hasConversionRate('USD'), true); 53 | assert.strictEqual(money.getConversionRate('USD', 'INR'), rates[0]); 54 | assert.strictEqual(money.getConversionRate('INR', 'USD'), rates[1]); 55 | assert.strictEqual(money.to('INR').float, 15000); 56 | assert.strictEqual(money.to('INR').to('USD').float, 195); 57 | }); 58 | }); 59 | 60 | describe('copy', function () { 61 | const money = pesa(200, 'USD').rate('INR', 75); 62 | const moneyCopy = money.copy(); 63 | 64 | moneyCopy.rate('EUR', 0.89); 65 | specify('conversion rate check', function () { 66 | assert.strictEqual(moneyCopy.getConversionRate('USD', 'EUR'), 0.89); 67 | assert.strictEqual(money.hasConversionRate('EUR'), false); 68 | assert.strictEqual(moneyCopy.hasConversionRate('EUR'), true); 69 | assert.throws(() => money.getConversionRate('USD', 'EUR')); 70 | }); 71 | 72 | specify('immutability', function () { 73 | assert.strictEqual(moneyCopy.add(10).float, 210); 74 | assert.strictEqual(money.add(10).float, 210); 75 | assert.strictEqual(money.float, 200); 76 | assert.strictEqual(moneyCopy.float, 200); 77 | }); 78 | }); 79 | 80 | describe('clip', function () { 81 | const money = pesa(100, 'USD').to('INR', 75).to('USD'); 82 | specify('internal.bigint check', function () { 83 | assert.strictEqual(money.internal.bigint, 99997500n); 84 | assert.strictEqual(money.clip(1).internal.bigint, 100000000n); 85 | assert.strictEqual(money.clip(2).internal.bigint, 100000000n); 86 | assert.strictEqual(money.clip(3).internal.bigint, 99998000n); 87 | assert.strictEqual(money.clip(4).internal.bigint, 99997500n); 88 | }); 89 | }); 90 | 91 | describe('arithmetic', function () { 92 | // these have been tested under preciseNumber. 93 | // adding it here for coverage 94 | describe('add', function () { 95 | const money = pesa(200, 'USD'); 96 | specify('no conversion', function () { 97 | assert.strictEqual(money.add(200).float, 400); 98 | }); 99 | 100 | specify('conversion', function () { 101 | assert.strictEqual(money.add(200, 'INR', 0.013).float, 202.6); 102 | }); 103 | }); 104 | 105 | describe('sub', function () { 106 | const money = pesa(200, 'USD'); 107 | specify('no conversion', function () { 108 | assert.strictEqual(money.sub(200).float, 0); 109 | }); 110 | 111 | specify('conversion', function () { 112 | assert.strictEqual(money.sub(200, 'INR', 0.013).float, 197.4); 113 | }); 114 | }); 115 | 116 | describe('mul', function () { 117 | const money = pesa(200, 'USD'); 118 | specify('no conversion', function () { 119 | assert.strictEqual(money.mul(200).float, 40000); 120 | }); 121 | 122 | specify('conversion', function () { 123 | assert.strictEqual(money.mul(200, 'INR', 0.013).float, 520); 124 | }); 125 | }); 126 | 127 | describe('div', function () { 128 | const money = pesa(200, 'USD'); 129 | specify('no conversion', function () { 130 | assert.strictEqual(money.div(200).float, 1); 131 | }); 132 | 133 | specify('conversion', function () { 134 | assert.strictEqual(money.div(200, 'INR', 0.013).float, 76.923076); 135 | }); 136 | }); 137 | }); 138 | 139 | describe('comparison', function () { 140 | describe('eq', function () { 141 | const money = pesa(150, 'INR'); 142 | specify('no conversion', function () { 143 | assert.strictEqual(money.eq(150), true); 144 | assert.strictEqual(money.eq(151), false); 145 | }); 146 | 147 | specify('conversion', function () { 148 | assert.strictEqual(money.eq(2, 'USD', 75), true); 149 | assert.strictEqual(money.eq(2, 'USD', 74.99), false); 150 | }); 151 | }); 152 | 153 | describe('gt', function () { 154 | const money = pesa(150, 'INR'); 155 | specify('no conversion', function () { 156 | assert.strictEqual(money.gt(149.99), true); 157 | assert.strictEqual(money.gt(150), false); 158 | assert.strictEqual(money.gt(151), false); 159 | }); 160 | 161 | specify('conversion', function () { 162 | assert.strictEqual(money.gt(1.99, 'USD', 75), true); 163 | assert.strictEqual(money.gt(2, 'USD', 75), false); 164 | assert.strictEqual(money.gt(2.01, 'USD', 75), false); 165 | }); 166 | }); 167 | 168 | describe('lt', function () { 169 | const money = pesa(150, 'INR'); 170 | specify('no conversion', function () { 171 | assert.strictEqual(money.lt(150.01), true); 172 | assert.strictEqual(money.lt(150), false); 173 | assert.strictEqual(money.lt(149.99), false); 174 | }); 175 | 176 | specify('conversion', function () { 177 | assert.strictEqual(money.lt(2.01, 'USD', 75), true); 178 | assert.strictEqual(money.lt(2, 'USD', 75), false); 179 | assert.strictEqual(money.lt(1.99, 'USD', 75), false); 180 | }); 181 | }); 182 | 183 | describe('gte', function () { 184 | const money = pesa(150, 'INR'); 185 | specify('no conversion', function () { 186 | assert.strictEqual(money.gte(149.99), true); 187 | assert.strictEqual(money.gte(150), true); 188 | assert.strictEqual(money.gte(151), false); 189 | }); 190 | 191 | specify('conversion', function () { 192 | assert.strictEqual(money.gte(1.99, 'USD', 75), true); 193 | assert.strictEqual(money.gte(2, 'USD', 75), true); 194 | assert.strictEqual(money.gte(2.01, 'USD', 75), false); 195 | }); 196 | }); 197 | 198 | describe('lte', function () { 199 | const money = pesa(150, 'INR'); 200 | specify('no conversion', function () { 201 | assert.strictEqual(money.lte(150.01), true); 202 | assert.strictEqual(money.lte(150), true); 203 | assert.strictEqual(money.lte(149.99), false); 204 | }); 205 | 206 | specify('conversion', function () { 207 | assert.strictEqual(money.lte(2.01, 'USD', 75), true); 208 | assert.strictEqual(money.lte(2, 'USD', 75), true); 209 | assert.strictEqual(money.lte(1.99, 'USD', 75), false); 210 | }); 211 | }); 212 | }); 213 | 214 | describe('checks', function () { 215 | specify('isPositive', function () { 216 | assert.strictEqual(pesa().isPositive(), false); 217 | assert.strictEqual(pesa(1).isPositive(), true); 218 | assert.strictEqual(pesa(-1).isPositive(), false); 219 | }); 220 | 221 | specify('isNegative', function () { 222 | assert.strictEqual(pesa().isNegative(), false); 223 | assert.strictEqual(pesa(1).isNegative(), false); 224 | assert.strictEqual(pesa(-1).isNegative(), true); 225 | }); 226 | 227 | specify('isPositive', function () { 228 | assert.strictEqual(pesa().isZero(), true); 229 | assert.strictEqual(pesa(1).isZero(), false); 230 | assert.strictEqual(pesa(-1).isZero(), false); 231 | }); 232 | }); 233 | 234 | describe('other calculations', function () { 235 | describe('percent', function () { 236 | const money = pesa(200, 'USD'); 237 | specify('-', function () { 238 | assert.strictEqual(money.percent(200).float, 400); 239 | assert.strictEqual(money.percent(100).float, 200); 240 | assert.strictEqual(money.percent(50).float, 100); 241 | assert.strictEqual(money.percent(30).float, 60); 242 | assert.strictEqual(money.percent(12.5).float, 25); 243 | assert.strictEqual(money.percent(1.125).float, 2.25); 244 | }); 245 | }); 246 | 247 | describe('abs', function () { 248 | specify('-', function () { 249 | assert.strictEqual(pesa(0).abs().float, 0); 250 | assert.strictEqual(pesa(-0).abs().float, 0); 251 | assert.strictEqual(pesa(0.00001).abs().float, 0.00001); 252 | assert.strictEqual(pesa(-0.00001).abs().float, 0.00001); 253 | assert.strictEqual(pesa(200).abs().float, 200); 254 | assert.strictEqual(pesa(-200).abs().eq(200), true); 255 | assert.strictEqual(pesa(-200).abs().eq(-200), false); 256 | }); 257 | }); 258 | 259 | describe('split', function () { 260 | const sum = (list: number[]) => list.reduce((a, b) => a + b); 261 | const money = pesa(200, 'USD'); 262 | 263 | describe('percent list input', function () { 264 | specify('even splits', function () { 265 | const splits = money.split([60, 40]).map((m) => m.float); 266 | assert.strictEqual(splits[0], 120); 267 | assert.strictEqual(splits[1], 80); 268 | assert.strictEqual(sum(splits), money.float); 269 | }); 270 | 271 | [0, 1, 2, 4, 6].forEach((d) => { 272 | specify(`uneven splits 0, d: ${d}`, function () { 273 | const splits = money.split([33, 33, 34], d).map((m) => m.float); 274 | assert.strictEqual(sum(splits), money.float); 275 | }); 276 | 277 | specify(`uneven splits 1, d: ${d}`, function () { 278 | const splits = money.split([49.99, 50.01], d).map((m) => m.float); 279 | assert.strictEqual(sum(splits), money.float); 280 | }); 281 | }); 282 | }); 283 | 284 | describe('number input', function () { 285 | [2, 3, 5, 6, 8, 10].forEach((n) => { 286 | specify(`n: ${n}`, function () { 287 | const splits = money.split(n).map((m) => m.float); 288 | assert.strictEqual(sum(splits), money.float); 289 | }); 290 | }); 291 | }); 292 | }); 293 | }); 294 | 295 | describe('constructor', function () { 296 | specify('wrapper', function () { 297 | const wrapper = (m: Money) => { 298 | // @ts-ignore 299 | m.__v_skip = true; 300 | return m; 301 | }; 302 | 303 | const money = pesa(200, { wrapper }); 304 | assert.strictEqual(money.float, 200); 305 | assert.strictEqual(money.hasOwnProperty('__v_skip'), true); 306 | 307 | let otherMoney = money.add(300); 308 | assert.strictEqual(otherMoney.float, 500); 309 | assert.strictEqual(otherMoney.hasOwnProperty('__v_skip'), true); 310 | }); 311 | }); 312 | }); 313 | 314 | describe('getMoneyMaker', function () { 315 | describe('constructor', function () { 316 | specify('wrapper', function () { 317 | const wrapper = (m: Money) => { 318 | // @ts-ignore 319 | m.__v_skip = true; 320 | return m; 321 | }; 322 | 323 | const moneyMaker = getMoneyMaker({ wrapper }); 324 | 325 | const money = moneyMaker(200); 326 | assert.strictEqual(money.float, 200); 327 | assert.strictEqual(money.hasOwnProperty('__v_skip'), true); 328 | 329 | let otherMoney = money.add(300); 330 | assert.strictEqual(otherMoney.float, 500); 331 | assert.strictEqual(otherMoney.hasOwnProperty('__v_skip'), true); 332 | }); 333 | }); 334 | }); 335 | -------------------------------------------------------------------------------- /src/money.ts: -------------------------------------------------------------------------------- 1 | import { DEF_DISP, DEF_PREC, USE_BNKR } from './consts'; 2 | import PreciseNumber from './preciseNumber'; 3 | import { 4 | getConversionRateKey, 5 | throwIfInvalidCurrencyCode, 6 | throwRateNotProvided, 7 | } from './utils'; 8 | 9 | type Input = PreciseNumber | bigint | number | string; 10 | type ArithmeticInput = Money | number | string; 11 | type Rate = string | number; 12 | type ConversionRateMap = Map; 13 | type Wrapper = (m: Money) => Money; 14 | 15 | interface RateSetting { 16 | from: string; 17 | to: string; 18 | rate: Rate; 19 | } 20 | 21 | export interface Options { 22 | bankersRounding?: boolean; 23 | precision?: number; 24 | currency?: string; 25 | display?: number; 26 | rates?: RateSetting | RateSetting[]; 27 | wrapper?: Wrapper; 28 | } 29 | 30 | export default class Money { 31 | display: number; 32 | #currency: string; 33 | #wrapper: Wrapper; 34 | #preciseNumber: PreciseNumber; 35 | #conversionRates: ConversionRateMap; 36 | 37 | constructor(amount: Input, options: Options = {}) { 38 | this.#preciseNumber = new PreciseNumber( 39 | amount, 40 | options.precision ?? DEF_PREC, 41 | options.bankersRounding ?? USE_BNKR 42 | ); 43 | 44 | this.#currency = ''; 45 | this.#conversionRates = new Map(); 46 | this.#wrapper = options.wrapper ?? ((m: Money) => m); 47 | this.display = options.display ?? DEF_DISP; 48 | 49 | const { currency, rates } = options; 50 | if (currency) { 51 | this.currency(currency); 52 | } 53 | if (rates) { 54 | this.rate(rates); 55 | } 56 | 57 | return this.#wrapper(this); 58 | } 59 | 60 | /* --------------------------------- 61 | * Getters and setters 62 | * ---------------------------------*/ 63 | 64 | get float() { 65 | return this.#preciseNumber.value; 66 | } 67 | 68 | get options(): Options { 69 | const rates: RateSetting[] = Array.from(this.#conversionRates.keys()).map( 70 | (k) => { 71 | const [from, to] = k.split('-'); 72 | const rate = this.#conversionRates.get(k) ?? -1; 73 | return { from, to, rate }; 74 | } 75 | ); 76 | 77 | return { 78 | currency: this.#currency, 79 | precision: this.#preciseNumber.precision, 80 | display: this.display, 81 | rates, 82 | }; 83 | } 84 | 85 | get preciseNumber(): PreciseNumber { 86 | return this.#preciseNumber; 87 | } 88 | 89 | get internal() { 90 | const bigint = this.#preciseNumber.integer; 91 | const precision = this.#preciseNumber.precision; 92 | return { bigint, precision }; 93 | } 94 | 95 | get conversionRates() { 96 | return new Map(this.#conversionRates); 97 | } 98 | 99 | get store(): string { 100 | return this.#preciseNumber.store; 101 | } 102 | 103 | /* --------------------------------- 104 | * Internal functions 105 | * ---------------------------------*/ 106 | 107 | _setConversionRates(rates: ConversionRateMap) { 108 | if (this.#conversionRates.size === 0) { 109 | this.#conversionRates = new Map(rates); 110 | } 111 | } 112 | 113 | #throwCurrencyNotSetIfNotSet() { 114 | if (!this.#currency) { 115 | throw Error('currency has not been set for conversion'); 116 | } 117 | } 118 | 119 | #copySelf(value: PreciseNumber | string, currency: string = ''): Money { 120 | const options = { 121 | currency: currency || this.#currency, 122 | precision: this.#preciseNumber.precision, 123 | display: this.display, 124 | wrapper: this.#wrapper, 125 | }; 126 | 127 | const result = new Money(value, options); 128 | result._setConversionRates(this.#conversionRates); 129 | return result; 130 | } 131 | 132 | #convertInput(value: ArithmeticInput, currency?: string, rate?: number) { 133 | let rhs; 134 | const valueIsMoney = value instanceof Money; 135 | if (valueIsMoney) { 136 | rhs = value.preciseNumber; 137 | currency = value.getCurrency() || currency; 138 | } else { 139 | rhs = new PreciseNumber(value, this.#preciseNumber.precision); 140 | } 141 | 142 | if (currency && currency !== this.#currency) { 143 | let finalRate; 144 | if (rate) { 145 | finalRate = rate; 146 | } 147 | 148 | if (!finalRate && valueIsMoney) { 149 | try { 150 | finalRate = value.getConversionRate(currency, this.#currency); 151 | } catch {} 152 | } 153 | 154 | if (!finalRate) { 155 | try { 156 | finalRate = this.getConversionRate(currency, this.#currency); 157 | } catch {} 158 | } 159 | 160 | if (!finalRate) { 161 | throwRateNotProvided(currency, this.#currency); 162 | } 163 | 164 | rhs = rhs.mul(finalRate ?? 1); // will never be one cause error if undefined 165 | } 166 | 167 | let lhs = this.#preciseNumber; 168 | return { lhs, rhs }; 169 | } 170 | 171 | /* --------------------------------- 172 | * User facing functions (chainable) 173 | * ---------------------------------*/ 174 | 175 | currency(value: string) { 176 | if (!this.#currency) { 177 | throwIfInvalidCurrencyCode(value); 178 | this.#currency = value; 179 | } 180 | return this; 181 | } 182 | 183 | rate(input: string | RateSetting | RateSetting[], rate?: Rate) { 184 | if (typeof input === 'string') { 185 | this.#throwCurrencyNotSetIfNotSet(); 186 | } 187 | 188 | if (typeof input === 'string' && typeof rate === 'undefined') { 189 | throwRateNotProvided(this.#currency, input); 190 | } 191 | 192 | let settings: RateSetting[]; 193 | if (input instanceof Array) { 194 | settings = input; 195 | } else if (typeof input === 'string') { 196 | settings = [ 197 | { 198 | from: this.#currency, 199 | to: input, 200 | rate: rate ?? 1, // It will never be '1' there's a guard clause. 201 | }, 202 | ]; 203 | } else if (input instanceof Object) { 204 | settings = [input]; 205 | } else { 206 | throw Error(`invalid input to rate: ${input}`); 207 | } 208 | 209 | for (let setting of settings) { 210 | const { from, to, rate } = setting; 211 | const key = getConversionRateKey(from, to); 212 | this.#conversionRates.set(key, rate); 213 | } 214 | 215 | return this; 216 | } 217 | 218 | /* --------------------------------- 219 | * User facing functions (chainable, im-mutate) 220 | * ---------------------------------*/ 221 | 222 | to(to: string, rate?: Rate): Money { 223 | this.#throwCurrencyNotSetIfNotSet(); 224 | if ( 225 | typeof rate === 'number' || 226 | (typeof rate === 'string' && !this.hasConversionRate(to)) 227 | ) { 228 | this.rate(to, rate); 229 | } else { 230 | rate = this.getConversionRate(this.#currency, to); 231 | } 232 | const preciseNumber = this.#preciseNumber.mul(rate); 233 | return this.#copySelf(preciseNumber, to); 234 | } 235 | 236 | /* --------------------------------- 237 | * User facing functions (chainable, operations) 238 | * ---------------------------------*/ 239 | 240 | add(value: ArithmeticInput, currency?: string, rate?: number): Money { 241 | const { lhs, rhs } = this.#convertInput(value, currency, rate); 242 | const outPreciseNumber = lhs.add(rhs); 243 | return this.#copySelf(outPreciseNumber); 244 | } 245 | 246 | sub(value: ArithmeticInput, currency?: string, rate?: number): Money { 247 | const { lhs, rhs } = this.#convertInput(value, currency, rate); 248 | const outPreciseNumber = lhs.sub(rhs); 249 | return this.#copySelf(outPreciseNumber); 250 | } 251 | 252 | mul(value: ArithmeticInput, currency?: string, rate?: number): Money { 253 | const { lhs, rhs } = this.#convertInput(value, currency, rate); 254 | const outPreciseNumber = lhs.mul(rhs); 255 | return this.#copySelf(outPreciseNumber); 256 | } 257 | 258 | div(value: ArithmeticInput, currency?: string, rate?: number): Money { 259 | const { lhs, rhs } = this.#convertInput(value, currency, rate); 260 | const outPreciseNumber = lhs.div(rhs); 261 | return this.#copySelf(outPreciseNumber); 262 | } 263 | 264 | /* --------------------------------- 265 | * User facing functions (chainable, other calcs) 266 | * ---------------------------------*/ 267 | 268 | percent(value: number): Money { 269 | return this.#copySelf(this.#preciseNumber.mul(value / 100)); 270 | } 271 | 272 | split(values: number | number[], round?: number): Money[] { 273 | round ??= this.display; 274 | let percents: number[] = []; 275 | if (typeof values === 'number') { 276 | const n = values; 277 | percents = Array(n - 1) 278 | .fill(100 / n) 279 | .map( 280 | (v) => 281 | new PreciseNumber(v, this.#preciseNumber.precision).clip( 282 | round ?? DEF_DISP 283 | ).float 284 | ); 285 | percents.push(100 - percents.reduce((a, b) => a + b)); 286 | } else if (values instanceof Array) { 287 | percents = values; 288 | } 289 | 290 | const isFull = 291 | percents.map((v) => new PreciseNumber(v)).reduce((a, b) => a.add(b)) 292 | .float === 100; 293 | const final = isFull ? percents.length - 1 : percents.length; 294 | 295 | const splits = percents.slice(0, final).map((v) => { 296 | const rounded = this.#preciseNumber.mul(v / 100).round(round ?? DEF_DISP); 297 | return this.#copySelf(rounded); 298 | }); 299 | 300 | if (isFull) { 301 | const sum = splits.reduce((a, b) => a.add(b)).round(round); 302 | const finalMoney = this.#copySelf(this.round(round)).sub(sum); 303 | splits.push(finalMoney); 304 | } 305 | 306 | return splits; 307 | } 308 | 309 | abs(): Money { 310 | if (this.lt(0)) { 311 | return this.mul(-1); 312 | } 313 | return this.copy(); 314 | } 315 | 316 | neg(): Money { 317 | return this.mul(-1); 318 | } 319 | 320 | clip(to?: number): Money { 321 | to ??= this.display; 322 | return this.#copySelf(this.#preciseNumber.clip(to), this.#currency); 323 | } 324 | 325 | copy(): Money { 326 | return this.#copySelf(this.#preciseNumber.copy(), this.#currency); 327 | } 328 | 329 | /* --------------------------------- 330 | * User facing functions (non-chainable, comparisons) 331 | * ---------------------------------*/ 332 | 333 | eq(value: ArithmeticInput, currency?: string, rate?: number): boolean { 334 | const { lhs, rhs } = this.#convertInput(value, currency, rate); 335 | return lhs.eq(rhs); 336 | } 337 | 338 | neq(value: ArithmeticInput, currency?: string, rate?: number): boolean { 339 | const { lhs, rhs } = this.#convertInput(value, currency, rate); 340 | return !lhs.eq(rhs); 341 | } 342 | 343 | gt(value: ArithmeticInput, currency?: string, rate?: number): boolean { 344 | const { lhs, rhs } = this.#convertInput(value, currency, rate); 345 | return lhs.gt(rhs); 346 | } 347 | 348 | lt(value: ArithmeticInput, currency?: string, rate?: number): boolean { 349 | const { lhs, rhs } = this.#convertInput(value, currency, rate); 350 | return lhs.lt(rhs); 351 | } 352 | 353 | gte(value: ArithmeticInput, currency?: string, rate?: number): boolean { 354 | const { lhs, rhs } = this.#convertInput(value, currency, rate); 355 | return lhs.gte(rhs); 356 | } 357 | 358 | lte(value: ArithmeticInput, currency?: string, rate?: number): boolean { 359 | const { lhs, rhs } = this.#convertInput(value, currency, rate); 360 | return lhs.lte(rhs); 361 | } 362 | 363 | /* --------------------------------- 364 | * User facing functions (non-chainable, checks) 365 | * ---------------------------------*/ 366 | 367 | isPositive(): boolean { 368 | return this.#preciseNumber.integer > 0n; 369 | } 370 | 371 | isNegative(): boolean { 372 | return this.#preciseNumber.integer < 0n; 373 | } 374 | 375 | isZero(): boolean { 376 | return this.#preciseNumber.integer === 0n; 377 | } 378 | 379 | /* --------------------------------- 380 | * User facing functions (non chainable) 381 | * ---------------------------------*/ 382 | 383 | getCurrency() { 384 | return this.#currency; 385 | } 386 | 387 | getConversionRate(from: string, to: string): Rate { 388 | let key = getConversionRateKey(from, to); 389 | let value = this.#conversionRates.get(key); 390 | 391 | if (!value) { 392 | key = getConversionRateKey(to, from); 393 | value = this.#conversionRates.get(key); 394 | 395 | if (value && typeof value === 'string') { 396 | value = 1 / parseFloat(value); 397 | } else if (value && typeof value === 'number') { 398 | value = 1 / value; 399 | } 400 | } 401 | 402 | if (!value) { 403 | throw Error(`please set the conversion rate for ${from} to ${to}`); 404 | } 405 | 406 | return value; 407 | } 408 | 409 | hasConversionRate(to: string): boolean { 410 | let key = getConversionRateKey(this.getCurrency(), to); 411 | let keyInverse = getConversionRateKey(to, this.getCurrency()); 412 | return ( 413 | this.#conversionRates.has(key) || this.#conversionRates.has(keyInverse) 414 | ); 415 | } 416 | 417 | /* --------------------------------- 418 | * User facing functions (display) 419 | * ---------------------------------*/ 420 | 421 | round(to?: number): string { 422 | to ??= this.display; 423 | return this.#preciseNumber.round(to); 424 | } 425 | 426 | toString() { 427 | return this.#preciseNumber.toString(); 428 | } 429 | 430 | toJSON() { 431 | return this.#preciseNumber.toJSON(); 432 | } 433 | 434 | valueOf(): bigint { 435 | return this.#preciseNumber.valueOf(); 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@cspotcode/source-map-consumer@0.8.0": 6 | version "0.8.0" 7 | resolved "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz" 8 | integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== 9 | 10 | "@cspotcode/source-map-support@0.7.0": 11 | version "0.7.0" 12 | resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz" 13 | integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== 14 | dependencies: 15 | "@cspotcode/source-map-consumer" "0.8.0" 16 | 17 | "@tsconfig/node10@^1.0.7": 18 | version "1.0.8" 19 | resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz" 20 | integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== 21 | 22 | "@tsconfig/node12@^1.0.7": 23 | version "1.0.9" 24 | resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz" 25 | integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== 26 | 27 | "@tsconfig/node14@^1.0.0": 28 | version "1.0.1" 29 | resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz" 30 | integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== 31 | 32 | "@tsconfig/node16@^1.0.2": 33 | version "1.0.2" 34 | resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz" 35 | integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== 36 | 37 | "@types/mocha@^9.0.0": 38 | version "9.0.0" 39 | resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.0.0.tgz#3205bcd15ada9bc681ac20bef64e9e6df88fd297" 40 | integrity sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA== 41 | 42 | "@types/node@^16.11.7": 43 | version "16.11.7" 44 | resolved "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz" 45 | integrity sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw== 46 | 47 | "@ungap/promise-all-settled@1.1.2": 48 | version "1.1.2" 49 | resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" 50 | integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== 51 | 52 | acorn-walk@^8.1.1: 53 | version "8.2.0" 54 | resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" 55 | integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== 56 | 57 | acorn@^8.4.1: 58 | version "8.5.0" 59 | resolved "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz" 60 | integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== 61 | 62 | ansi-colors@4.1.1: 63 | version "4.1.1" 64 | resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz" 65 | integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== 66 | 67 | ansi-regex@^5.0.1: 68 | version "5.0.1" 69 | resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" 70 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 71 | 72 | ansi-styles@^4.0.0, ansi-styles@^4.1.0: 73 | version "4.3.0" 74 | resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" 75 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 76 | dependencies: 77 | color-convert "^2.0.1" 78 | 79 | anymatch@~3.1.2: 80 | version "3.1.2" 81 | resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" 82 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== 83 | dependencies: 84 | normalize-path "^3.0.0" 85 | picomatch "^2.0.4" 86 | 87 | arg@^4.1.0: 88 | version "4.1.3" 89 | resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" 90 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 91 | 92 | argparse@^2.0.1: 93 | version "2.0.1" 94 | resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" 95 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 96 | 97 | balanced-match@^1.0.0: 98 | version "1.0.2" 99 | resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" 100 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 101 | 102 | binary-extensions@^2.0.0: 103 | version "2.2.0" 104 | resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" 105 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 106 | 107 | brace-expansion@^1.1.7: 108 | version "1.1.11" 109 | resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" 110 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 111 | dependencies: 112 | balanced-match "^1.0.0" 113 | concat-map "0.0.1" 114 | 115 | braces@~3.0.2: 116 | version "3.0.2" 117 | resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" 118 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 119 | dependencies: 120 | fill-range "^7.0.1" 121 | 122 | browser-stdout@1.3.1: 123 | version "1.3.1" 124 | resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" 125 | integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== 126 | 127 | camelcase@^6.0.0: 128 | version "6.2.0" 129 | resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz" 130 | integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== 131 | 132 | chalk@^4.1.0: 133 | version "4.1.2" 134 | resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" 135 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 136 | dependencies: 137 | ansi-styles "^4.1.0" 138 | supports-color "^7.1.0" 139 | 140 | chokidar@3.5.2: 141 | version "3.5.2" 142 | resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz" 143 | integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== 144 | dependencies: 145 | anymatch "~3.1.2" 146 | braces "~3.0.2" 147 | glob-parent "~5.1.2" 148 | is-binary-path "~2.1.0" 149 | is-glob "~4.0.1" 150 | normalize-path "~3.0.0" 151 | readdirp "~3.6.0" 152 | optionalDependencies: 153 | fsevents "~2.3.2" 154 | 155 | cliui@^7.0.2: 156 | version "7.0.4" 157 | resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" 158 | integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== 159 | dependencies: 160 | string-width "^4.2.0" 161 | strip-ansi "^6.0.0" 162 | wrap-ansi "^7.0.0" 163 | 164 | color-convert@^2.0.1: 165 | version "2.0.1" 166 | resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" 167 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 168 | dependencies: 169 | color-name "~1.1.4" 170 | 171 | color-name@~1.1.4: 172 | version "1.1.4" 173 | resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" 174 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 175 | 176 | concat-map@0.0.1: 177 | version "0.0.1" 178 | resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" 179 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 180 | 181 | create-require@^1.1.0: 182 | version "1.1.1" 183 | resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" 184 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 185 | 186 | debug@4.3.2: 187 | version "4.3.2" 188 | resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz" 189 | integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== 190 | dependencies: 191 | ms "2.1.2" 192 | 193 | decamelize@^4.0.0: 194 | version "4.0.0" 195 | resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" 196 | integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== 197 | 198 | diff@5.0.0: 199 | version "5.0.0" 200 | resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" 201 | integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== 202 | 203 | diff@^4.0.1: 204 | version "4.0.2" 205 | resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" 206 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 207 | 208 | emoji-regex@^8.0.0: 209 | version "8.0.0" 210 | resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" 211 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 212 | 213 | esbuild-android-arm64@0.13.13: 214 | version "0.13.13" 215 | resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.13.tgz#da07b5fb2daf7d83dcd725f7cf58a6758e6e702a" 216 | integrity sha512-T02aneWWguJrF082jZworjU6vm8f4UQ+IH2K3HREtlqoY9voiJUwHLRL6khRlsNLzVglqgqb7a3HfGx7hAADCQ== 217 | 218 | esbuild-darwin-64@0.13.13: 219 | version "0.13.13" 220 | resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.13.tgz" 221 | integrity sha512-wkaiGAsN/09X9kDlkxFfbbIgR78SNjMOfUhoel3CqKBDsi9uZhw7HBNHNxTzYUK8X8LAKFpbODgcRB3b/I8gHA== 222 | 223 | esbuild-darwin-arm64@0.13.13: 224 | version "0.13.13" 225 | resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.13.tgz#8c320eafbb3ba2c70d8062128c5b71503e342471" 226 | integrity sha512-b02/nNKGSV85Gw9pUCI5B48AYjk0vFggDeom0S6QMP/cEDtjSh1WVfoIFNAaLA0MHWfue8KBwoGVsN7rBshs4g== 227 | 228 | esbuild-freebsd-64@0.13.13: 229 | version "0.13.13" 230 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.13.tgz#ce0ca5b8c4c274cfebc9326f9b316834bd9dd151" 231 | integrity sha512-ALgXYNYDzk9YPVk80A+G4vz2D22Gv4j4y25exDBGgqTcwrVQP8rf/rjwUjHoh9apP76oLbUZTmUmvCMuTI1V9A== 232 | 233 | esbuild-freebsd-arm64@0.13.13: 234 | version "0.13.13" 235 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.13.tgz#463da17562fdcfdf03b3b94b28497d8d8dcc8f62" 236 | integrity sha512-uFvkCpsZ1yqWQuonw5T1WZ4j59xP/PCvtu6I4pbLejhNo4nwjW6YalqnBvBSORq5/Ifo9S/wsIlVHzkzEwdtlw== 237 | 238 | esbuild-linux-32@0.13.13: 239 | version "0.13.13" 240 | resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.13.tgz#2035793160da2c4be48a929e5bafb14a31789acc" 241 | integrity sha512-yxR9BBwEPs9acVEwTrEE2JJNHYVuPQC9YGjRfbNqtyfK/vVBQYuw8JaeRFAvFs3pVJdQD0C2BNP4q9d62SCP4w== 242 | 243 | esbuild-linux-64@0.13.13: 244 | version "0.13.13" 245 | resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.13.tgz#fbe4802a8168c6d339d0749f977b099449b56f22" 246 | integrity sha512-kzhjlrlJ+6ESRB/n12WTGll94+y+HFeyoWsOrLo/Si0s0f+Vip4b8vlnG0GSiS6JTsWYAtGHReGczFOaETlKIw== 247 | 248 | esbuild-linux-arm64@0.13.13: 249 | version "0.13.13" 250 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.13.tgz#f08d98df28d436ed4aad1529615822bb74d4d978" 251 | integrity sha512-KMrEfnVbmmJxT3vfTnPv/AiXpBFbbyExH13BsUGy1HZRPFMi5Gev5gk8kJIZCQSRfNR17aqq8sO5Crm2KpZkng== 252 | 253 | esbuild-linux-arm@0.13.13: 254 | version "0.13.13" 255 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.13.tgz#6f968c3a98b64e30c80b212384192d0cfcb32e7f" 256 | integrity sha512-hXub4pcEds+U1TfvLp1maJ+GHRw7oizvzbGRdUvVDwtITtjq8qpHV5Q5hWNNn6Q+b3b2UxF03JcgnpzCw96nUQ== 257 | 258 | esbuild-linux-mips64le@0.13.13: 259 | version "0.13.13" 260 | resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.13.tgz#690c78dc4725efe7d06a1431287966fbf7774c7f" 261 | integrity sha512-cJT9O1LYljqnnqlHaS0hdG73t7hHzF3zcN0BPsjvBq+5Ad47VJun+/IG4inPhk8ta0aEDK6LdP+F9299xa483w== 262 | 263 | esbuild-linux-ppc64le@0.13.13: 264 | version "0.13.13" 265 | resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.13.tgz#7ec9048502de46754567e734aae7aebd2df6df02" 266 | integrity sha512-+rghW8st6/7O6QJqAjVK3eXzKkZqYAw6LgHv7yTMiJ6ASnNvghSeOcIvXFep3W2oaJc35SgSPf21Ugh0o777qQ== 267 | 268 | esbuild-netbsd-64@0.13.13: 269 | version "0.13.13" 270 | resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.13.tgz#439bdaefffa03a8fa84324f5d83d636f548a2de3" 271 | integrity sha512-A/B7rwmzPdzF8c3mht5TukbnNwY5qMJqes09ou0RSzA5/jm7Jwl/8z853ofujTFOLhkNHUf002EAgokzSgEMpQ== 272 | 273 | esbuild-openbsd-64@0.13.13: 274 | version "0.13.13" 275 | resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.13.tgz#c9958e5291a00a3090c1ec482d6bcdf2d5b5d107" 276 | integrity sha512-szwtuRA4rXKT3BbwoGpsff6G7nGxdKgUbW9LQo6nm0TVCCjDNDC/LXxT994duIW8Tyq04xZzzZSW7x7ttDiw1w== 277 | 278 | esbuild-sunos-64@0.13.13: 279 | version "0.13.13" 280 | resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.13.tgz#ac9ead8287379cd2f6d00bd38c5997fda9c1179e" 281 | integrity sha512-ihyds9O48tVOYF48iaHYUK/boU5zRaLOXFS+OOL3ceD39AyHo46HVmsJLc7A2ez0AxNZCxuhu+P9OxfPfycTYQ== 282 | 283 | esbuild-windows-32@0.13.13: 284 | version "0.13.13" 285 | resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.13.tgz#a3820fc86631ca594cb7b348514b5cc3f058cfd6" 286 | integrity sha512-h2RTYwpG4ldGVJlbmORObmilzL8EECy8BFiF8trWE1ZPHLpECE9//J3Bi+W3eDUuv/TqUbiNpGrq4t/odbayUw== 287 | 288 | esbuild-windows-64@0.13.13: 289 | version "0.13.13" 290 | resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.13.tgz#1da748441f228d75dff474ddb7d584b81887323c" 291 | integrity sha512-oMrgjP4CjONvDHe7IZXHrMk3wX5Lof/IwFEIbwbhgbXGBaN2dke9PkViTiXC3zGJSGpMvATXVplEhlInJ0drHA== 292 | 293 | esbuild-windows-arm64@0.13.13: 294 | version "0.13.13" 295 | resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.13.tgz#06dfa52a6b178a5932a9a6e2fdb240c09e6da30c" 296 | integrity sha512-6fsDfTuTvltYB5k+QPah/x7LrI2+OLAJLE3bWLDiZI6E8wXMQU+wLqtEO/U/RvJgVY1loPs5eMpUBpVajczh1A== 297 | 298 | esbuild@^0.13.13: 299 | version "0.13.13" 300 | resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.13.13.tgz" 301 | integrity sha512-Z17A/R6D0b4s3MousytQ/5i7mTCbaF+Ua/yPfoe71vdTv4KBvVAvQ/6ytMngM2DwGJosl8WxaD75NOQl2QF26Q== 302 | optionalDependencies: 303 | esbuild-android-arm64 "0.13.13" 304 | esbuild-darwin-64 "0.13.13" 305 | esbuild-darwin-arm64 "0.13.13" 306 | esbuild-freebsd-64 "0.13.13" 307 | esbuild-freebsd-arm64 "0.13.13" 308 | esbuild-linux-32 "0.13.13" 309 | esbuild-linux-64 "0.13.13" 310 | esbuild-linux-arm "0.13.13" 311 | esbuild-linux-arm64 "0.13.13" 312 | esbuild-linux-mips64le "0.13.13" 313 | esbuild-linux-ppc64le "0.13.13" 314 | esbuild-netbsd-64 "0.13.13" 315 | esbuild-openbsd-64 "0.13.13" 316 | esbuild-sunos-64 "0.13.13" 317 | esbuild-windows-32 "0.13.13" 318 | esbuild-windows-64 "0.13.13" 319 | esbuild-windows-arm64 "0.13.13" 320 | 321 | escalade@^3.1.1: 322 | version "3.1.1" 323 | resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" 324 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== 325 | 326 | escape-string-regexp@4.0.0: 327 | version "4.0.0" 328 | resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" 329 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 330 | 331 | fill-range@^7.0.1: 332 | version "7.0.1" 333 | resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" 334 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 335 | dependencies: 336 | to-regex-range "^5.0.1" 337 | 338 | find-up@5.0.0: 339 | version "5.0.0" 340 | resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" 341 | integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== 342 | dependencies: 343 | locate-path "^6.0.0" 344 | path-exists "^4.0.0" 345 | 346 | flat@^5.0.2: 347 | version "5.0.2" 348 | resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" 349 | integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== 350 | 351 | fs.realpath@^1.0.0: 352 | version "1.0.0" 353 | resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" 354 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 355 | 356 | fsevents@~2.3.2: 357 | version "2.3.2" 358 | resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" 359 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 360 | 361 | get-caller-file@^2.0.5: 362 | version "2.0.5" 363 | resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" 364 | integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 365 | 366 | glob-parent@~5.1.2: 367 | version "5.1.2" 368 | resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" 369 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 370 | dependencies: 371 | is-glob "^4.0.1" 372 | 373 | glob@7.1.7: 374 | version "7.1.7" 375 | resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" 376 | integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== 377 | dependencies: 378 | fs.realpath "^1.0.0" 379 | inflight "^1.0.4" 380 | inherits "2" 381 | minimatch "^3.0.4" 382 | once "^1.3.0" 383 | path-is-absolute "^1.0.0" 384 | 385 | growl@1.10.5: 386 | version "1.10.5" 387 | resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz" 388 | integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== 389 | 390 | has-flag@^4.0.0: 391 | version "4.0.0" 392 | resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" 393 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 394 | 395 | he@1.2.0: 396 | version "1.2.0" 397 | resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" 398 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 399 | 400 | inflight@^1.0.4: 401 | version "1.0.6" 402 | resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" 403 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 404 | dependencies: 405 | once "^1.3.0" 406 | wrappy "1" 407 | 408 | inherits@2: 409 | version "2.0.4" 410 | resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" 411 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 412 | 413 | is-binary-path@~2.1.0: 414 | version "2.1.0" 415 | resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" 416 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 417 | dependencies: 418 | binary-extensions "^2.0.0" 419 | 420 | is-extglob@^2.1.1: 421 | version "2.1.1" 422 | resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" 423 | integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 424 | 425 | is-fullwidth-code-point@^3.0.0: 426 | version "3.0.0" 427 | resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" 428 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 429 | 430 | is-glob@^4.0.1, is-glob@~4.0.1: 431 | version "4.0.3" 432 | resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" 433 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 434 | dependencies: 435 | is-extglob "^2.1.1" 436 | 437 | is-number@^7.0.0: 438 | version "7.0.0" 439 | resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" 440 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 441 | 442 | is-plain-obj@^2.1.0: 443 | version "2.1.0" 444 | resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" 445 | integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== 446 | 447 | is-unicode-supported@^0.1.0: 448 | version "0.1.0" 449 | resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" 450 | integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== 451 | 452 | isexe@^2.0.0: 453 | version "2.0.0" 454 | resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" 455 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 456 | 457 | js-yaml@4.1.0: 458 | version "4.1.0" 459 | resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" 460 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 461 | dependencies: 462 | argparse "^2.0.1" 463 | 464 | locate-path@^6.0.0: 465 | version "6.0.0" 466 | resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" 467 | integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== 468 | dependencies: 469 | p-locate "^5.0.0" 470 | 471 | log-symbols@4.1.0: 472 | version "4.1.0" 473 | resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" 474 | integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== 475 | dependencies: 476 | chalk "^4.1.0" 477 | is-unicode-supported "^0.1.0" 478 | 479 | make-error@^1.1.1: 480 | version "1.3.6" 481 | resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" 482 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 483 | 484 | minimatch@3.0.4, minimatch@^3.0.4: 485 | version "3.0.4" 486 | resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" 487 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 488 | dependencies: 489 | brace-expansion "^1.1.7" 490 | 491 | mocha@^9.1.3: 492 | version "9.1.3" 493 | resolved "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz" 494 | integrity sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw== 495 | dependencies: 496 | "@ungap/promise-all-settled" "1.1.2" 497 | ansi-colors "4.1.1" 498 | browser-stdout "1.3.1" 499 | chokidar "3.5.2" 500 | debug "4.3.2" 501 | diff "5.0.0" 502 | escape-string-regexp "4.0.0" 503 | find-up "5.0.0" 504 | glob "7.1.7" 505 | growl "1.10.5" 506 | he "1.2.0" 507 | js-yaml "4.1.0" 508 | log-symbols "4.1.0" 509 | minimatch "3.0.4" 510 | ms "2.1.3" 511 | nanoid "3.1.25" 512 | serialize-javascript "6.0.0" 513 | strip-json-comments "3.1.1" 514 | supports-color "8.1.1" 515 | which "2.0.2" 516 | workerpool "6.1.5" 517 | yargs "16.2.0" 518 | yargs-parser "20.2.4" 519 | yargs-unparser "2.0.0" 520 | 521 | ms@2.1.2: 522 | version "2.1.2" 523 | resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" 524 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 525 | 526 | ms@2.1.3: 527 | version "2.1.3" 528 | resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" 529 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 530 | 531 | nanoid@3.1.25: 532 | version "3.1.25" 533 | resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz" 534 | integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== 535 | 536 | normalize-path@^3.0.0, normalize-path@~3.0.0: 537 | version "3.0.0" 538 | resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" 539 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 540 | 541 | once@^1.3.0: 542 | version "1.4.0" 543 | resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" 544 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 545 | dependencies: 546 | wrappy "1" 547 | 548 | p-limit@^3.0.2: 549 | version "3.1.0" 550 | resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" 551 | integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== 552 | dependencies: 553 | yocto-queue "^0.1.0" 554 | 555 | p-locate@^5.0.0: 556 | version "5.0.0" 557 | resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" 558 | integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== 559 | dependencies: 560 | p-limit "^3.0.2" 561 | 562 | path-exists@^4.0.0: 563 | version "4.0.0" 564 | resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" 565 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 566 | 567 | path-is-absolute@^1.0.0: 568 | version "1.0.1" 569 | resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" 570 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 571 | 572 | picomatch@^2.0.4, picomatch@^2.2.1: 573 | version "2.3.0" 574 | resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz" 575 | integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== 576 | 577 | prettier@2.4.1: 578 | version "2.4.1" 579 | resolved "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz" 580 | integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA== 581 | 582 | randombytes@^2.1.0: 583 | version "2.1.0" 584 | resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" 585 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== 586 | dependencies: 587 | safe-buffer "^5.1.0" 588 | 589 | readdirp@~3.6.0: 590 | version "3.6.0" 591 | resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" 592 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 593 | dependencies: 594 | picomatch "^2.2.1" 595 | 596 | require-directory@^2.1.1: 597 | version "2.1.1" 598 | resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" 599 | integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= 600 | 601 | safe-buffer@^5.1.0: 602 | version "5.2.1" 603 | resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" 604 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 605 | 606 | serialize-javascript@6.0.0: 607 | version "6.0.0" 608 | resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz" 609 | integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== 610 | dependencies: 611 | randombytes "^2.1.0" 612 | 613 | string-width@^4.1.0, string-width@^4.2.0: 614 | version "4.2.3" 615 | resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" 616 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== 617 | dependencies: 618 | emoji-regex "^8.0.0" 619 | is-fullwidth-code-point "^3.0.0" 620 | strip-ansi "^6.0.1" 621 | 622 | strip-ansi@^6.0.0, strip-ansi@^6.0.1: 623 | version "6.0.1" 624 | resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" 625 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 626 | dependencies: 627 | ansi-regex "^5.0.1" 628 | 629 | strip-json-comments@3.1.1: 630 | version "3.1.1" 631 | resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" 632 | integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 633 | 634 | supports-color@8.1.1: 635 | version "8.1.1" 636 | resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" 637 | integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== 638 | dependencies: 639 | has-flag "^4.0.0" 640 | 641 | supports-color@^7.1.0: 642 | version "7.2.0" 643 | resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" 644 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 645 | dependencies: 646 | has-flag "^4.0.0" 647 | 648 | to-regex-range@^5.0.1: 649 | version "5.0.1" 650 | resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" 651 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 652 | dependencies: 653 | is-number "^7.0.0" 654 | 655 | ts-node@^10.4.0: 656 | version "10.4.0" 657 | resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz" 658 | integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== 659 | dependencies: 660 | "@cspotcode/source-map-support" "0.7.0" 661 | "@tsconfig/node10" "^1.0.7" 662 | "@tsconfig/node12" "^1.0.7" 663 | "@tsconfig/node14" "^1.0.0" 664 | "@tsconfig/node16" "^1.0.2" 665 | acorn "^8.4.1" 666 | acorn-walk "^8.1.1" 667 | arg "^4.1.0" 668 | create-require "^1.1.0" 669 | diff "^4.0.1" 670 | make-error "^1.1.1" 671 | yn "3.1.1" 672 | 673 | typescript@^4.4.4: 674 | version "4.4.4" 675 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" 676 | integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== 677 | 678 | which@2.0.2: 679 | version "2.0.2" 680 | resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" 681 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 682 | dependencies: 683 | isexe "^2.0.0" 684 | 685 | workerpool@6.1.5: 686 | version "6.1.5" 687 | resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz" 688 | integrity sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw== 689 | 690 | wrap-ansi@^7.0.0: 691 | version "7.0.0" 692 | resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" 693 | integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 694 | dependencies: 695 | ansi-styles "^4.0.0" 696 | string-width "^4.1.0" 697 | strip-ansi "^6.0.0" 698 | 699 | wrappy@1: 700 | version "1.0.2" 701 | resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" 702 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 703 | 704 | y18n@^5.0.5: 705 | version "5.0.8" 706 | resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" 707 | integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== 708 | 709 | yargs-parser@20.2.4: 710 | version "20.2.4" 711 | resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" 712 | integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== 713 | 714 | yargs-parser@^20.2.2: 715 | version "20.2.9" 716 | resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" 717 | integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== 718 | 719 | yargs-unparser@2.0.0: 720 | version "2.0.0" 721 | resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz" 722 | integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== 723 | dependencies: 724 | camelcase "^6.0.0" 725 | decamelize "^4.0.0" 726 | flat "^5.0.2" 727 | is-plain-obj "^2.1.0" 728 | 729 | yargs@16.2.0: 730 | version "16.2.0" 731 | resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" 732 | integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== 733 | dependencies: 734 | cliui "^7.0.2" 735 | escalade "^3.1.1" 736 | get-caller-file "^2.0.5" 737 | require-directory "^2.1.1" 738 | string-width "^4.2.0" 739 | y18n "^5.0.5" 740 | yargs-parser "^20.2.2" 741 | 742 | yn@3.1.1: 743 | version "3.1.1" 744 | resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" 745 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 746 | 747 | yocto-queue@^0.1.0: 748 | version "0.1.0" 749 | resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" 750 | integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 751 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | pesa logo 4 | 5 | # पेसा 6 | 7 |
8 | 9 | A money handling library for JavaScript that can handle USD to VEF conversions for Jeff without breaking a sweat! 10 | 11 | --- 12 | 13 | ```javascript 14 | const money = pesa(135, 'USD').add(25).to('INR', 75); 15 | 16 | money.round(2); 17 | // '12000.00' 18 | ``` 19 | 20 | > Why should I use this, when I can just do all of this with plain old JavaScript numbers?! 21 | 22 | Because JavaScript numbers full of fun foibles such as these: 23 | 24 | ```javascript 25 | 0.1 + 0.2; 26 | // 0.30000000000000004 27 | 28 | 9007199254740992 + 1; 29 | // 9007199254740992 30 | ``` 31 | 32 | Using them for financial transactions will most likely lead to technical bankruptcy [[1](#credits)]. 33 | 34 | _(check this talk by [Bartek Szopka](https://www.youtube.com/watch?v=MqHDDtVYJRI) to understand why JS numbers are like this)_ 35 | 36 | **`pesa`** circumvents this by conducting scaled integer operations using JS [`BigInt`](https://github.com/tc39/proposal-bigint) instead of `Number`. This allows for arithmetic involving arbitrarily large numbers with unnecessarily high precision. 37 | 38 | ## Index 39 | 40 |
41 | show/hide 42 | 43 | 1. [Installation](#installation) 44 | 2. [Usage](#usage) 45 | 1. [Initialization](#initialization) 46 | 2. [Currency and Conversions](#currency-and-conversions) 47 | 3. [Immutability](#immutability) 48 | 4. [Chaining](#chaining) 49 | 3. [Documentation](#documentation) 50 | 1. [Arithmetic Functions](#arithmetic-functions) 51 | 2. [Comparison Functions](#comparison-functions) 52 | 3. [Check Functions](#check-functions) 53 | 4. [Other Calculation Functions](#other-calculations-functions) 54 | 5. [Display](#display) 55 | 6. [Chainable Methods](#chainable-methods) 56 | 7. [Non Chainable Methods](#non-chainable-methods) 57 | 8. [Internals](#internals) 58 | 4. [Additional Notes](#additional-notes) 59 | 1. [Storing The Value](#storing-the-value) 60 | 2. [Non-Money Stuff](#non-money-stuff) 61 | 3. [Support](#Support) 62 | 4. [Precision vs Display](#precision-vs-display) 63 | 5. [Why High Precision](#why-high-precision) 64 | 6. [Implicit Conversions](#implicit-conversions) 65 | 7. [Rounding](#rounding) 66 | 8. [Use Cases](#use-cases) 67 | 5. [Alternatives](#alternatives) 68 | 69 |
70 | 71 | ## Documentation Index 72 | 73 |
74 | show/hide 75 | 76 | 1. [Arithmetic Functions](#arithmetic-functions) 77 | - [Arguments](#arguments-arithmetic) 78 | - [Return](#return-arithmetic) 79 | - [Operations](#operations-arithmetic) 80 | 2. [Comparison Functions](#comparison-functions) 81 | - [Arguments](#arguments-comparison) 82 | - [Return](#return-comparison) 83 | - [Operations](#operations-comparison) 84 | 3. [Check Functions](#check-functions) 85 | - [Operations](#operations-check) 86 | 4. [Other Calculation Functions](#other-calculations-functions) 87 | - [`percent`](#percent) 88 | - [`split`](#split) 89 | - [`abs`](#abs) 90 | 5. [Display](#display) 91 | - [`float`](#float) 92 | - [`round`](#round) 93 | - [`store`](#store) 94 | 6. [Chainable Methods](#chainable-methods) 95 | - [`currency`](#currency) 96 | - [`rate`](#rate) 97 | - [`clip`](#clip) 98 | - [`copy`](#copy) 99 | 7. [Non Chainable Methods](#non-chainable-methods) 100 | - [`getCurrency`](#getcurrency) 101 | - [`getConversionRate`](#getconversionrate) 102 | - [`hasConversionRate`](#hasconversionrate) 103 | 8. [Internals](#internals) - [Numeric Representation](#numeric-representation) - [Conversion Rates](#conversion-rates) 104 |
105 | 106 | ## Installation 107 | 108 | For `npm` users: 109 | 110 | ```bash 111 | npm install pesa 112 | ``` 113 | 114 | For `yarn` users: 115 | 116 | ```bash 117 | yarn add pesa 118 | ``` 119 | 120 | [Index](#index) 121 | 122 | ## Usage 123 | 124 | ```javascript 125 | pesa(200, 'USD').add(250, 'INR', 75).percent(50).round(3); 126 | // '9475.000' 127 | ``` 128 | 129 | This section describes the usage in brief. For more details, check the [Documentation](#documentation) section. For even more details, check the source code or raise an issue. 130 | 131 | [Index](#index) 132 | 133 | ### Initialization 134 | 135 | To create an initialize a money object you can either use the constructor function `pesa`: 136 | 137 | ```javascript 138 | import { pesa } from 'pesa'; 139 | 140 | const money = pesa(200, 'USD'); 141 | // OR 142 | const money = pesa(200, options); 143 | ``` 144 | 145 | or the constructor function maker `getMoneyMaker`, this can be used if you don't want to set the options everytime you call `pesa`: 146 | 147 | ```javascript 148 | import { getMoneyMaker } from 'pesa'; 149 | 150 | const pesa = getMoneyMaker('USD'); 151 | // OR 152 | const pesa = getMoneyMaker(options); 153 | 154 | const money = pesa(200); 155 | ``` 156 | 157 | #### Options and Value 158 | 159 | **Options** are optional, but currency has to be set before any conversions can take place. 160 | 161 | ```typescript 162 | interface Options { 163 | bankersRounding?: boolean; // Default: true, use bankers rounding instead of traditional rounding. 164 | currency?: string; // Default: '', Three letter alphabetical code in uppercase ('INR'). 165 | precision?: number; // Default: 6, Integer between 0 and 20. 166 | display?: number; // Default: 3, Number of digits .round defaults to. 167 | wrapper?: Wrapper; // Default: (m) => m, Used to augment all returned money objects. 168 | rate?: RateSetting | RateSetting[]; // Default: [], Conversion rates 169 | } 170 | 171 | interface RateSetting { 172 | from: string; 173 | to: string; 174 | rate: string | number; 175 | } 176 | 177 | type Wrapper = (m: Money) => Money; 178 | ``` 179 | 180 | **Value** can be a `string`, `number` or a `bigint`. If value is not passed the value is set as 0. 181 | 182 | ```typescript 183 | type Value = string | number | bigint; 184 | ``` 185 | 186 | If `bigint` is passed then it doesn't undergo any conversion or scaling and is used to set the internal `bigint`. 187 | 188 | ```javascript 189 | pesa(235).internal; 190 | // { bigint: 235000000n, precision: 6 } 191 | 192 | pesa('235').internal; 193 | // { bigint: 235000000n, precision: 6 } 194 | 195 | pesa(235n).internal; 196 | // { bigint: 235n, precision: 6 } 197 | ``` 198 | 199 | **Wrapper** is a function that can add additional properties to the returned object. 200 | 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. 201 | 202 | So to remedy this you can pass [`markRaw`](https://vuejs.org/api/reactivity-advanced.html#markraw) as the wrapper function. 203 | 204 | 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. 205 | 206 | ### Currency and Conversions 207 | 208 | A numeric value isn't money unless a currency is assigned to it. 209 | 210 | #### Setting Currency 211 | 212 | Currency can be assigned any time before a conversion is applied. 213 | 214 | ```javascript 215 | // During initialization 216 | const money = pesa(200, 'USD'); 217 | 218 | // After initialization 219 | const money = pesa(200).currency('USD'); 220 | ``` 221 | 222 | #### Setting Rates 223 | 224 | 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. 225 | 226 | ```javascript 227 | // Rate set before the operation 228 | pesa(200).currency('USD').rate('INR', 75).add(2000, 'INR'); 229 | 230 | // Rate set during the operation 231 | pesa(200).currency('USD').add(2000, 'INR', 0.013); 232 | ``` 233 | 234 | #### Conversion 235 | 236 | The result of an operation will always have the currency on the left (USD in the above example). To convert to a currency: 237 | 238 | ```javascript 239 | // Rate set during the operation 240 | money.to('INR', 75); 241 | 242 | // Rate set before the operation 243 | money.to('INR'); 244 | ``` 245 | 246 | This returns a new `Money` object. 247 | 248 | > Does it provide conversion rates? 249 | 250 | **`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](https://api.coinbase.com/v2/exchange-rates), [VAT Comply](https://vatcomply.com/), [European Central Bank](https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml?b743fd808a34daf65f9ac9d63a9538f9), and [others](https://github.com/public-apis/public-apis#currency-exchange). 251 | 252 | ### Immutability 253 | 254 | The underlying value or currency of a `Money` object doesn't change after an operation. 255 | 256 | ```javascript 257 | const a = pesa(200, 'USD'); 258 | const b = pesa(125, 'INR').rate('USD', 0.013); 259 | const c = a.add(b); 260 | 261 | // Statements below will evaluate to true 262 | a.float === 200; 263 | b.float === 125; 264 | c.float === 201.625; 265 | 266 | a.getCurrency() === 'USD'; 267 | b.getCurrency() === 'INR'; 268 | c.getCurrency() === 'USD'; 269 | ``` 270 | 271 | ### Chaining 272 | 273 | Due to the following two points: 274 | 275 | 1. All arithmetic operation (`add`, `sub`, `mul` and `div`), create a new `Money` object having the values that is the result of that operation. 276 | 277 | 2. All setter methods (`currency`, `rate`), set the value of an internal parameter and return the calling `Money` object. 278 | 279 | Methods can be chained and executed like so: 280 | 281 | ```javascript 282 | pesa(200) 283 | .add(22) 284 | .currency('USD') 285 | .sub(33) 286 | .rate('INR', 75) 287 | .mul(2, 'INR') 288 | .to('INR') 289 | .round(2); 290 | // '377.99' 291 | ``` 292 | 293 | [Index](#index) 294 | 295 | ## Documentation 296 | 297 | Calling the main function `pesa` returns an object of the `Money` class. 298 | 299 | ```typescript 300 | const money: Money = pesa(200, 'USD'); 301 | ``` 302 | 303 | The rest of the documentation pertains to the methods and parameters of this class. 304 | 305 | ### Arithmetic Functions 306 | 307 | Operations that involve the value of two `Money` objects and return a new `Money` object as the result. 308 | 309 | Function signature 310 | 311 | ```typescript 312 | [operationName](value: Input, currency?: string, rate?: number) : Money; 313 | 314 | type Input = Money | number | string; 315 | ``` 316 | 317 | Example: 318 | 319 | ```javascript 320 | money = pesa(200, 'USD'); 321 | 322 | money.add(150).round(); 323 | // '350.000' 324 | 325 | money.sub(150, 'INR', 0.013); 326 | // '198.050' 327 | ``` 328 | 329 | _**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._ 330 | 331 | #### Arguments (arithmetic) 332 | 333 | | name | description | example | 334 | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | 335 | | `value` | This is compulsory. If input is a string then `'_'` or `','` can be used as digit separators, but decimal points should always be `'.'`. Scientific notation isn't supported. | `'200_000.00'` | 336 | | `currency` | If value is a different currency from the calling object's currency then currency should be passed. Any arbitrary combination of 3 letters in uppercase can be a `currency`. | `'VEF'` | 337 | | `rate` | Required only if the calling object or `value` doesn't have the rate set. `rate` is the conversion rate between the calling object's `currency` and `value`'s `currency` | `450_000` | 338 | 339 | #### Return (arithmetic) 340 | 341 | All arightmetic operations return a new `Money` object that inherits the calling object's options, i.e. `currency`, `precision` and `display`. 342 | 343 | #### Operations (arithmetic) 344 | 345 | | name | description | example | 346 | | ----- | --------------------------- | ------------------ | 347 | | `add` | addition, i.e. a + b | `pesa(33).add(36)` | 348 | | `sub` | subtraction, i.e. a - b | `pesa(90).sub(21)` | 349 | | `mul` | multiplication, i.e. a \* b | `pesa(23).mul(3)` | 350 | | `div` | division, i.e. a / b | `pesa(138).div(2)` | 351 | 352 | ### Comparison Functions 353 | 354 | Operations that compare two values and return a boolean. 355 | 356 | Function Signature: 357 | 358 | ```typescript 359 | [operationName](value: Input, currency?: string, rate?: number) : boolean; 360 | 361 | type Input = Money | number | string; 362 | ``` 363 | 364 | Example: 365 | 366 | ```javascript 367 | money = pesa(150, 'INR'); 368 | 369 | money.eq(2, 'USD', 75); 370 | // true 371 | 372 | money.lte(200); 373 | // true 374 | ``` 375 | 376 | _**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 75 is for converting `'USD'` to `'INR'`. The reason for this is to prevent precision loss due to reciprocal._ 377 | 378 | #### Arguments (comparison) 379 | 380 | See the **Arguments** table under the **Arithmetic** section. 381 | 382 | #### Return 383 | 384 | `Boolean` indicating the result of the comparison. 385 | 386 | #### Operations (comparison) 387 | 388 | | name | description | example | 389 | | ----- | ---------------------------------------------------------------------------------------- | ------------------ | 390 | | `eq` | checks if the two amounts are the same, i.e. `===` | `pesa(20).eq(20)` | 391 | | `neq` | checks if the two amounts are not the same, i.e. `!==` | `pesa(20).neq(19)` | 392 | | `gt` | checks if calling object amount is greater than passed amount, i.e. `>` | `pesa(20).gt(19)` | 393 | | `lt` | checks if calling object amount is less than passed amount, i.e. `<` | `pesa(20).lt(21)` | 394 | | `gte` | checks if calling object amount is greater than or equal to the passed amount, i.e. `>=` | `pesa(20).gte(19)` | 395 | | `lte` | checks if calling object amount is less than or equal to passed amount, i.e. `<` | `pesa(20).lte(21)` | 396 | 397 | ### Check Functions 398 | 399 | Functions that return a `boolean` after evaluating the internal state. 400 | 401 | Function signature: 402 | 403 | ```typescript 404 | [checkName](): boolean; 405 | ``` 406 | 407 | Example: 408 | 409 | ```typescript 410 | pesa(200, 'USD').isPositive(); 411 | // true 412 | 413 | pesa(0, 'USD').isZero(); 414 | // true 415 | 416 | pesa(-200, 'USD').isNegative(); 417 | // true 418 | ``` 419 | 420 | #### Operations (check) 421 | 422 | | name | description | 423 | | ------------ | ------------------------------------------------------ | 424 | | `isPositive` | Returns true if underlying value is greater than zero. | 425 | | `isZero` | Returns true if underlying value is zero. | 426 | | `isNegative` | Returns true if underlying value is less than zero. | 427 | 428 | ### Other Calculation Functions 429 | 430 | #### `percent` 431 | 432 | Function that returns another `Money` object having a percent of the calling objects value. 433 | 434 | Function signature 435 | 436 | ```typescript 437 | percent(value: number): Money; 438 | ``` 439 | 440 | Example 441 | 442 | ```javascript 443 | pesa(200, 'USD').percent(50).round(2); 444 | // '100.00' 445 | ``` 446 | 447 | #### `split` 448 | 449 | Function that splits the underlying value into given list of percentages or `n` equal parts and returns an array of `Money` objects. 450 | 451 | The sum of `values` can exceed `100`. Argument `round` is used to decide at what precision the sum of all returned will equate to the calling objects value. If `round` is not passed then the calling object's `display` value is used. 452 | 453 | Function signature: 454 | 455 | ```typescript 456 | split(values: number | number[], round?: number): Money[]; 457 | ``` 458 | 459 | Example: 460 | 461 | ```javascript 462 | pesa(200.99) 463 | .split([33, 66, 1]) 464 | .map((m) => m.float); 465 | // [66.327, 132.653, 2.01] 466 | 467 | pesa(200.99) 468 | .split([33, 66, 1], 2) 469 | .map((m) => m.float); 470 | // [66.33, 132.65, 2.01] 471 | 472 | pesa(100) 473 | .split(3) 474 | .map((m) => m.float); 475 | // [33.333, 33.333, 33.334] 476 | ``` 477 | 478 | #### `abs` 479 | 480 | Returns a `Money` object having the the absolute value of the calling money object. 481 | 482 | Function signature: 483 | 484 | ```typescript 485 | abs(): Money; 486 | ``` 487 | 488 | Example: 489 | 490 | ```javascript 491 | pesa(-2).abs().eq(2); 492 | // true 493 | 494 | pesa(2).abs().eq(2); 495 | // true 496 | ``` 497 | 498 | #### `neg` 499 | 500 | Returns a `Money` object having the the negated value of the calling money object. 501 | 502 | Function signature: 503 | 504 | ```typescript 505 | neg(): Money; 506 | ``` 507 | 508 | Example: 509 | 510 | ```javascript 511 | pesa(-2).neg().round(); 512 | // '2.000' 513 | 514 | pesa(2).neg().round(); 515 | // '-2.000' 516 | ``` 517 | 518 | ### Display 519 | 520 | Functions and parameters used to display the `Money` object's value. 521 | 522 | > Does this support formatting? 523 | 524 | Nope, but you can use the [ECMAScript Internationalization API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) to handle formatting for you. The `NumberFormat` constructor can be configured to your needs then you can pass `Money#float` to it's format method. 525 | 526 | ```javascript 527 | const numberFormat = new Intl.NumberFormat('en-US', { 528 | style: 'currency', 529 | currency: 'USD', 530 | }); 531 | const money = pesa(2000, 'USD'); 532 | 533 | numberFormat.format(money.float); 534 | // '$2,000.00' 535 | ``` 536 | 537 | #### `float` 538 | 539 | Returns the JS Number form of the underlying value. 540 | 541 | Function signature: 542 | 543 | ```typescript 544 | float: number; 545 | ``` 546 | 547 | Example: 548 | 549 | ```javascript 550 | pesa(200).float; 551 | // 200 552 | ``` 553 | 554 | #### `round` 555 | 556 | Rounds the underlying value and returns it, this function is not susceptible to JS number foibles. If `to` is not passed it uses the `display` amount. 557 | 558 | Function Signature: 559 | 560 | ```typescript 561 | round(to?: number): string 562 | ``` 563 | 564 | Example: 565 | 566 | ```javascript 567 | pesa(200).round(2); 568 | // '200.00' 569 | ``` 570 | 571 | #### `store` 572 | 573 | Property that displays a precision intact string representation of the value. 574 | 575 | ```javascript 576 | pesa(200, { precision: 7 }).store; 577 | // '200.0000000' 578 | ``` 579 | 580 | ### Chainable Methods 581 | 582 | These methods are used to set some value and return the `Money` object itself so that other functions can be called on it. 583 | 584 | #### `currency` 585 | 586 | This is used to set the currency after initialization. Currency can be set only once so if currency has been set, this function will not change it. 587 | 588 | Function signature: 589 | 590 | ```typescript 591 | currency(value: string): Money; 592 | ``` 593 | 594 | Example: 595 | 596 | ```javascript 597 | pesa(200).currency('INR'); 598 | ``` 599 | 600 | #### `rate` 601 | 602 | This is used to set a single or multiple conversion rates. If input is a string then it is assumed that the conversion rate is from the calling objects `currency` to the string `input` passed as the first parameter with a value of `rate`. You can be more explicit by passing an object. 603 | 604 | Function signature: 605 | 606 | ```typescript 607 | rate(input: string | RateSetting | RateSetting[], rate?: Rate): 608 | 609 | interface RateSetting { 610 | from: string; 611 | to: string; 612 | rate: string | number; 613 | } 614 | ``` 615 | 616 | Example: 617 | 618 | ```javascript 619 | // string input 620 | pesa(200, 'INR').rate('USD', 75); 621 | 622 | // RateSetting input 623 | pesa(200, 'INR').rate({ from: 'INR', to: 'USD', rate: 75 }); 624 | 625 | // RateSetting[] input 626 | pesa(200, 'INR').rate([ 627 | { from: 'INR', to: 'USD', rate: 75 }, 628 | { from: 'USD', to: 'INR', rate: 0.013 }, 629 | ]); 630 | ``` 631 | 632 | #### `clip` 633 | 634 | Used to receive a copy of the calling object with the internal representation rounded off to the given place. 635 | 636 | Function signature: 637 | 638 | ```typescript 639 | clip(to: string): Money; 640 | ``` 641 | 642 | Example: 643 | 644 | ```javascript 645 | const money = pesa(7500, 'INR').to('USD', 1 / 75); 646 | 647 | money.round(); 648 | // '99.998' 649 | 650 | const clipped = money.clip(2); 651 | clipped.round(); 652 | // '100.000' 653 | 654 | clipped.internal; 655 | // { bigint: 100000000n, precision: 6 } 656 | ``` 657 | 658 | #### `copy` 659 | 660 | Returns a copy of it self. 661 | 662 | Function signature: 663 | 664 | ```typescript 665 | copy(): Money; 666 | ``` 667 | 668 | Example; 669 | 670 | ```javascript 671 | pesa(200).copy().round(); 672 | // '200.000' 673 | ``` 674 | 675 | ### Non Chainable Methods 676 | 677 | These methods are used to retrieve some value from the `Money` object. 678 | 679 | #### `getCurrency` 680 | 681 | This will return the set currency of the money object. If nothing is set then `''` is returned. 682 | 683 | Function signature; 684 | 685 | ```typescript 686 | getCurrency(): string 687 | ``` 688 | 689 | Example: 690 | 691 | ```javascript 692 | pesa(200, 'INR').getCurrency(); 693 | // 'INR' 694 | ``` 695 | 696 | #### `getConversionRate` 697 | 698 | This will return the stored conversion rate for the given arguments. If no conversion rate is found, it will throw an error. If conversion rate is stored for _from A to B_, fetching the conversion rate for _from B to A_ will return the reciprocal unless it is explicitly set. 699 | 700 | Function signature: 701 | 702 | ```typescript 703 | getConversionRate(from: string, to:string): string | number 704 | ``` 705 | 706 | Example 707 | 708 | ```javascript 709 | const money = pesa(200, 'INR').rate('USD', 75); 710 | 711 | money.getConversionRate('INR', 'USD'); 712 | // 75 713 | 714 | money.getConversionRate('USD', 'INR'); 715 | // 0.013333333333333334 716 | ``` 717 | 718 | #### `hasConversionRate` 719 | 720 | Will return true if either conversion rates _from A to B_ or _from B to A_ is found. 721 | 722 | Function signature: 723 | 724 | ```typescript 725 | hasConversionRate(to: string): boolean; 726 | ``` 727 | 728 | Example: 729 | 730 | ```javascript 731 | pesa(200, 'USD').rate('INR', 75).hasConversionRate('INR'); 732 | // true 733 | ``` 734 | 735 | ### Internals 736 | 737 | These values can be used for debugging. 738 | 739 | _Note: altering the returned values won't change the values stored in the `Money` object, these are copies._ 740 | 741 | #### Numeric Representation 742 | 743 | To view the internal numeric representation, the **`.internal`** attribute can be used. 744 | 745 | ```javascript 746 | pesa(201).internal; 747 | // { bigint: 201000000n, precision: 6 } 748 | ``` 749 | 750 | #### Conversion Rates 751 | 752 | To view the stored conversion rates, the **`.conversionRate`** attribute can be used. 753 | 754 | ```javascript 755 | pesa(200, 'USD').rate('INR', 75).conversionRate; 756 | // Map(1) { 'USD-INR' => 75 } 757 | ``` 758 | 759 | The returned object is that of the javascript `Map` class, which has the following key format `${fromCurrency}-${toCurrency}`. The value may be a `string` or `number`. 760 | 761 | [Index](#index) 762 | 763 | ## Additional Notes 764 | 765 | Additional notes pertaining to this lib. 766 | 767 | ### Storing The Value 768 | 769 | Since a `Money` constitutes of 2 values for the number: 1. the `precision`, and 2. the `value`. Storing this would require two cells, but this would be incredible stupid cause fractional numbers have already solved this with the decimal point. 770 | 771 | We can use the `store` property to get a string representation where the mantissa length gives the precision irrespective of the significant digits. 772 | 773 | ```javascript 774 | pesa(0, { precision: 4 }).store; 775 | // '0.0000' 776 | ``` 777 | 778 | You still have to deal with the `currency` though. Also if your db doesn't have a decimal type (I'm looking at you SQLite), then you'll have to store this as a string and cast it before operations. 779 | 780 | ### Non-Money Stuff 781 | 782 | Because of these two points: 783 | 784 | 1. A number is considered as money only when a currency is attached to it. 785 | 2. **`pesa`** allows deferring currency until conversion is required. 786 | 787 | **`pesa`** can be used for non monetary operations for when high precision is required or you want to circumvent the foibles of JS `number`. 788 | 789 | ```javascript 790 | pesa(0.1).add(0.2).eq(0.3); 791 | // true 792 | 793 | pesa(9007199254740992).add(1).round(0); 794 | // '9007199254740993' 795 | ``` 796 | 797 | ### Support 798 | 799 | Since **`pesa`** is built for high precision and large numbers that can exceed `Number.MAX_SAFE_INTEGER` it is dependent on `BigInt` so if your environment doesn't support `BigInt` you will have to rely on some other library. 800 | 801 | Check this chart for more info: [`BigInt` Browser compatibility](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#browser_compatibility) 802 | 803 | ### Precision vs Display 804 | 805 | A short note on the difference between the two in the context of **`pesa`**. 806 | 807 | precision 808 | 809 | Essentially: 810 | 811 | - **Precision** refers to how many digits are stored after the decimal point. 812 | - **Display** refers to how many digits are shown after the decimal point when `.round` is called. 813 | 814 | Ideally this number would be the same as the minor unit value of a currency. For example if currency is USD the minor is 2, the same for INR. The problem arises when the amount is to be multiplied with another amount having a fractional component such as in the case of conversion. 815 | 816 | ```javascript 817 | pesa(333, 'INR').to('USD', 0.013).float; 818 | // 4.329 819 | ``` 820 | 821 | If in the above example `precision` were set to 2 it would mess up the calculations because 0.013 gets rounded to 0.01 since input `precision` is matched to internal precision. 822 | 823 | ```javascript 824 | pesa(333, { currency: 'INR', precision: 2 }).to('USD', 0.013).float; 825 | // 3.33 826 | ``` 827 | 828 | So the number you want to adjust is `display` which doesn't mess up the internal representation. 829 | 830 | ```javascript 831 | const money = pesa(333, { currency: 'INR', display: 2 }).to('USD', 0.013); 832 | 833 | money.round(); 834 | // '4.33' 835 | 836 | money.float; 837 | // 4.329 838 | ``` 839 | 840 | ### Why High Precision 841 | 842 | The reason why you would want to conduct your operations in high precision is because: say you make 10,000 INR to USD conversions at 0.013 then that's a difference 10 USD that is added in due to rounding: 843 | 844 | ```javascript 845 | const oneConversion = pesa(333).mul(0.013).round(2); 846 | // '4.33' 847 | 848 | pesa(oneConversion).mul(10000).round(2); 849 | // '43300.00' 850 | ``` 851 | 852 | as opposed to: 853 | 854 | ```javascript 855 | pesa(333).mul(0.013).mul(10000).round(2); 856 | // '43290.00' 857 | ``` 858 | 859 | i.e. someone is losing money that they shouldn't be. 860 | 861 | Now this can't be solved entirely for the same reason that 1/3 in base 10 is 0.3 with the 3 recurring ad infinitum, but in base 3 it would be 0.1. But this can be mitigated by using high precision. Which means that someone will always loose money they shouldn't be but we can control the extent to which they do. 862 | 863 | ### Implicit Conversions 864 | 865 | In **`pesa`** if you provide the conversion _from A to B_, the conversion rate _from B to A_ is implied, i.e. the reciprocal of the former. 866 | 867 | This means that chained conversions will cause value loss: 868 | 869 | ```javascript 870 | pesa(100, 'USD').round(); 871 | // '100.000' 872 | 873 | pesa(100, 'USD').to('INR', 75).round(); 874 | // '7500.000' 875 | 876 | pesa(100, 'USD').to('INR', 75).to('USD').round(); 877 | // '99.998' 878 | ``` 879 | 880 | There are two ways to alleviate this until there's a proper solution: 881 | 882 | #### 1. Increase `precision` or decrease `display`: 883 | 884 | ```javascript 885 | pesa(100, { currency: 'USD', precision: 7 }).to('INR', 75).to('USD').round(); 886 | // '100.000' 887 | 888 | pesa(100, { currency: 'USD', display: 2 }).to('INR', 75).to('USD').round(); 889 | // '100.00' 890 | ``` 891 | 892 | in both the above cases the internal representation would represent a fractional value. 893 | 894 | #### 2. Use `clip` to maintain internally rounded values: 895 | 896 | ```javascript 897 | const clipped = pesa(100, 'USD').to('INR', 75).to('USD').clip(2); 898 | 899 | clipped.internal; 900 | // { bigint: 100000000n, precision: 6 } 901 | 902 | clipped.round(); 903 | // '100.000' 904 | ``` 905 | 906 | ### Rounding 907 | 908 | Rounding at mid-points is confusing af. 909 | 910 | ```javascript 911 | 2.5; // Mid-point 912 | 913 | 2.49999; // Not mid-point 914 | 2.50001; // Not mid-point 915 | ``` 916 | 917 | The **traditional** rounding method always _rounds up_ from the mid point. 918 | 919 | ```javascript 920 | pesa(2.5, { bankersRounding: false }).round(0); 921 | // '3' 922 | ``` 923 | 924 | This is uneven because of right side bias on the number line, so to mitigate this bias we have **bankers** rounding which _rounds to the closest even number_. `pesa` uses bankers rounding by default: 925 | 926 | ```javascript 927 | pesa(0.5).round(0); 928 | // '0' 929 | 930 | pesa(1.5).round(0); 931 | // '2' 932 | 933 | pesa(2.5).round(0); 934 | // '2' 935 | 936 | pesa(3.5).round(0); 937 | // '4' 938 | ``` 939 | 940 | Things get even more confusing when considering negative numbers. But if you remember that **traditional** rounding rounds _up_, i.e. towards positive infinity or to the right of the number line, not away from zero: 941 | 942 | ```javascript 943 | pesa(-2.5, { bankersRounding: false }).round(0); 944 | // '-2' 945 | ``` 946 | 947 | and **bankers** rounding rounds to the _closest even number_ which is always at a distance of 0.5 (if rounding to 0): 948 | 949 | ```javascript 950 | pesa(-2.5).round(0); 951 | // '-2' 952 | 953 | pesa(-3.5).round(0); 954 | // '-4' 955 | ``` 956 | 957 | you will be less confused. 958 | 959 | Finally, remember that bankers rounding kicks in _only at the mid point_, this depends on the precision: 960 | 961 | ```javascript 962 | pesa(2.51, { precision: 2 }).round(0); 963 | // '3' 964 | 965 | pesa(2.51, { precision: 1 }).round(0); 966 | // '2' 967 | ``` 968 | 969 | due to precision loss, a non mid-point can become a mid-point and bankers algo will be used for rounding. 970 | 971 | ### Use Cases 972 | 973 | Hyper-realistic use cases designed to showcase the capabilities of **`pesa`**. 974 | 975 | _Disclaimer: all characters, places and events in this `README.md` are entirely fictional. Any resemblances to real characters, places, and events are purely coincidental._ 976 | 977 | #### Mega Jeff in Venezuela 978 | 979 | Imagine Jeff was a million times as powerful, we can call him `megaJeff`, his bank being a whopping 200 quadrillion USD: 980 | 981 | ```javascript 982 | const megaJeff = pesa('200_000_000_000_000_000.18', 'USD'); 983 | ``` 984 | 985 | (_`megaJeff` is too powerful for JS numbers to handle accurately and hence we must rely on string for input._) 986 | 987 | and decided to emigrate to Venezuela at the height of it's hyper-inflation, requiring him to convert his USD to VEF: 988 | 989 | ```javascript 990 | const megaJeffInVenezuela = megaJeff.to('VEF', 451_853.23); 991 | ``` 992 | 993 | this is a conversion that JS numbers can't handle without resorting to dirty tricks such as _E notation_. 994 | 995 | ```javascript 996 | 200_000_000_000_000_000.18 * 451_853.23; 997 | // 9.037064599999999e+22 998 | ``` 999 | 1000 | Which is why you need **`pesa`**. 1001 | 1002 | ```javascript 1003 | megaJeffInVenezuela.round(4); 1004 | // '90370646000000000081333.5814' 1005 | ``` 1006 | 1007 | #### Alan the Seed Seller 1008 | 1009 | Alan sells seeds. He lives in Venezuela. Alan doesn't trust his dirty government, they messed up his currency. So Alan conducts his dealings only in BTC. Compared to VEF, BTC is less volatile. Seeds are precious, Alan is parsimonious. Alan likes to record the flow of each seed: 1010 | 1011 | ```javascript 1012 | const numberOfSeeds = 101_234_318; 1013 | 1014 | const options = { currency: 'BTC', precision: 30 }; 1015 | const costPerSeed = pesa('0.000000031032882086386885', options); // ~3 satoshis 1016 | ``` 1017 | 1018 | Say he wants to calculate the total value of his seeds: 1019 | 1020 | ```javascript 1021 | const totalvalue = costPerSeed.mul(numberOfSeeds); 1022 | 1023 | totalValue.round(24); 1024 | // '3.141592653589793387119430' 1025 | ``` 1026 | 1027 | if he were to rely on clumsy old JS number: 1028 | 1029 | ```javascript 1030 | 0.000000031032882086386885 * 101_234_318; 1031 | // 3.1415926535897936 1032 | ``` 1033 | 1034 | he would end up loosing almost 30% of his very significant digits. Any one who deals in BTC knows that it is very bad to loose one's digits. 1035 | 1036 | Say he wanted to find out the value of his seeds in USD, which at the time of writing has the conversion rate of 60951.60 from BTC. 1037 | 1038 | ```javascript 1039 | totalValue.to('USD', 59379.3).round(30); 1040 | // '186545.572655304418471780769799000000' 1041 | ``` 1042 | 1043 | were he to rely on JS number, he would end up with a number that—having lost more than 40% of it extremely significant digits—is completely detached from reality: 1044 | 1045 | ```javascript 1046 | 0.000000031032882086386885 * 101_234_318 * 59379.3; 1047 | // 186545.57265530445 1048 | ``` 1049 | 1050 | Keep your significant digits, use **`pesa`**. 1051 | 1052 | _Note: hyperinflation is not a joke, if you or your country is experiencing hyperinflation please seek help._ 1053 | 1054 | [Index](#index) 1055 | 1056 | ## Alternatives 1057 | 1058 | A few good alternatives to **`pesa`** that solve a similar problem. 1059 | 1060 | - [currency.js](https://github.com/scurker/currency.js/) 1061 | - [dinero.js](https://github.com/dinerojs/dinero.js/) 1062 | 1063 | > Why a create another Money lib if these already exists?! 1064 | 1065 | They either didn't use `bigint` (_[megaJeff](#mega-jeff-in-venezuela) sad_) or had too verbose an API. 1066 | 1067 | ## Credits 1068 | 1069 | 1. These are [Ankush's](http://github.com/ankush) wise words of wisdom. These words instilled fear of the JS `Number` in me. Thank you Ankush. 1070 | --------------------------------------------------------------------------------