Giter Club home page Giter Club logo

dnum's Introduction

dnum: small library for big decimal numbers

npm version bundle size License

dnum provides a small set of utilities designed for the manipulation of large numbers. It provides useful features for everyday apps, such as formatting and math functions. Numbers are represented as a pair composed of a value (BigInt) and a decimal precision. This structure allows to maintain the number precision while offering a great flexibility.

type Dnum = [value: bigint, decimals: number];

Usage

import * as dn from "dnum";

let a = dn.from(2, 18); // the number 2 followed by 18 decimals
let a = [2000000000000000000n, 18]; // equivalent to the previous line

let b = dn.from("870983127.93887"); // dn.from() can parse strings, numbers, bigint and more

let c = dn.multiply(a, b); // returns [1741966255877740000000000000n, 18]

console.log(
  dn.format(a), // "2"
  dn.format(b, 2), // "870,983,127.94"
  dn.format(c, 2), // "1,741,966,255.88"
  dn.format(b, { compact: true }), // "1.7B"
);

Install

npm install --save dnum
pnpm add dnum
yarn add dnum

TL;DR

dnum might be a good option for your project if:

  • Your numbers are represented as value + decimals pairs.
  • You need to format large numbers for UI purposes.
  • You want to keep your big numbers library small.
  • You want a simple, straightforward data structure.

Example

dnum can be used to perform math operations on currency values. Letโ€™s consider a scenario where you have the price of a specific token known as TKN, expressed in ETH, received as a string to prevent potential precision issues:

let tknPriceInEth = "17.30624293209842";

And you received the price of 1 ETH in USD from a different source, as a JavaScript number:

let ethPriceInUsd = 1002.37;

Finally, your app has a specific quantity of TKN to be displayed, represented as a BigInt with an implied 18 decimals precision:

let tknQuantity = 1401385000000000000000n; // 1401.385 (18 decimals precision)

You want to display the USD value of tknQuantity. This would normally require to:

  • Parse the numbers correctly (without using parseInt() / parseFloat() to avoid precision loss).
  • Convert everything into BigInt values with an identical decimals precision.
  • Multiply the numbers.
  • Convert the resulting BigInt into a string and format it for display purposes, without Intl.NumberFormat since it would cause precision loss.

dnum can do all of this for you:

let tknPriceInEth = "17.30624293209842";
let ethPriceInUsd = 1002.37;
let tknQuantity = 1401385000000000000000n; // 1401.385 (18 decimals precision)

// dnum function parameters accept various ways to represent decimal numbers.
let tknPriceInUsd = dnum.multiply(tknPriceInEth, ethPriceInUsd);

let tknQuantityInUsd = dnum.multiply(
  // Here we only attach the 18 decimals precision with the bigint value,
  // which corresponds to the Dnum type: [value: bigint, decimals: number].
  // You can pass this structure anywhere dnum expects a value, and this is
  // also what most dnum functions return.
  [tknQuantity, 18],
  tknPriceInUsd,
);

// We can now format the obtained result, rounding its decimals to 2 digits:
dnum.format(tknQuantityInUsd, 2); // $24,310,188.17

You can play with this example on CodeSandbox.

API

Types

type Dnum = [value: bigint, decimals: number];
type Numberish = string | number | bigint | Dnum;

format(value, options)

Formats the number for display purposes.

Name Description Type
value The value to format. Dnum
options.digits Number of digits to display. Setting options to a number acts as an alias for this option (see example below). Defaults to the number of decimals in the Dnum passed to value. number
options.compact Compact formatting (e.g. โ€œ1,000โ€ becomes โ€œ1Kโ€). boolean
options.trailingZeros Add trailing zeros if any, following the number of digits. boolean
options.locale The locale used to format the number. string
options.decimalsRounding Method used to round to digits decimals (defaults to "ROUND_HALF"). "ROUND_HALF" | "ROUND_UP" | "ROUND_DOWN"
options.signDisplay When to display the sign for the number. Follows the same rules as Intl.NumberFormat. Defaults to "auto". "auto" | "always" | "exceptZero" | "negative" | "never"
returns Formatted string. string

Example

let amount = [123456789000000000000000n, 18];

// If no digits are provided, the digits correspond to the decimals
dnum.format(amount); // 123,456.789

// options.digits
dnum.format(amount, { digits: 2 }); // 123,456.79
dnum.format(amount, 2); // 123,456.79 (alias for { digits: 2 })

// options.compact
dnum.format(amount, { compact: true }); // 123K

// options.trailingZeros
dnum.format(amount, { digits: 6, trailingZeros: true }); // 123,456.789000

from(valueToParse, decimals)

Parse a value and convert it into a Dnum. The passed value can be a string, a number, a bigint, or even a Dnum โˆ’ which can be useful to change its decimals.

Name Description Type
valueToParse Value to convert into a Dnum Numberish
decimals (optional) Number of decimals (defaults to true for auto) number | true
returns Converted value Dnum

Example

// Parses a number expressed as a string or number
let amount = dnum.from("17.30624", 18);

// amount equals [17306240000000000000n, 18]

add(value1, value2, decimals)

Adds two values together, regardless of their decimals. decimals correspond to the decimals desired in the result.

Name Description Type
value1 First value to add Numberish
value2 Second value to add Numberish
decimals (optional) Result decimals (defaults to value1 decimals) number
returns Result Dnum

subtract(value1, value2, decimals)

Subtracts the second value from the first one, regardless of their decimals. decimals correspond to the decimals desired in the result.

Name Description Type
value1 Value from which value2 is subtracted Numberish
value2 Value to subtract from value1 Numberish
decimals (optional) Result decimals (defaults to value1 decimals) number
returns Result Dnum

Alias: sub()

multiply(value1, value2, decimals)

Multiply two values together, regardless of their decimals. decimals correspond to the decimals desired in the result.

Name Description Type
value1 First value to multiply Numberish
value2 Second value to multiply Numberish
decimals (optional) Result decimals (defaults to value1 decimals) number
returns Result Dnum

Alias: mul()

Example

let ethPriceUsd = [100000n, 2]; // 1000 USD
let tokenPriceEth = [570000000000000000, 18]; // 0.57 ETH

let tokenPriceUsd = dnum.multiply(tokenPriceEth, ethPriceUsd, 2); // 570 USD

// tokenPriceUsd equals [57000, 2]

divide(value1, value2, decimals)

Divide a value by another one, regardless of their decimals. decimals correspond to the decimals desired in the result.

Name Description Type
value1 Dividend Numberish
value2 Divisor Numberish
decimals (optional) Result decimals (defaults to value1 decimals) number
returns Result value Dnum

Alias: div()

Example

let ethPriceUsd = [100000n, 2]; // 1000 USD
let tokenPriceUsd = [57000, 2]; // 570 USD

let tokenPriceEth = dnum.divide(tokenPriceUsd, ethPriceUsd, 18); // 0.57 ETH

// tokenPriceEth equals [570000000000000000, 18]

remainder(value1, value2, decimals)

Equivalent to the % operator: calculate the remainder left over when one operand is divided by a second operand.

Name Description Type
value1 Dividend Numberish
value2 Divisor Numberish
decimals (optional) Result decimals (defaults to value1 decimals) number
returns Result value Dnum

Alias: rem()

abs(value, decimals)

Equivalent to the Math.abs() function: it returns the absolute value of the Dnum number.

Name Description Type
value Value to remove the sign from Numberish
decimals (optional) Result decimals (defaults to value decimals) number
returns Result value Dnum

Example

let value = [-100000n, 2];

dnum.abs(value); // [100000n, 2]

round(value, decimals)

Equivalent to the Math.round() function: it returns the value of a number rounded to the nearest integer.

Name Description Type
value Value to round to the nearest integer Numberish
decimals (optional) Result decimals (defaults to value decimals) number
returns Result value Dnum

Example

let value = [-123456n, 2]; // 1234.56

dnum.round(value); // [123500n, 2] or 1235.00

floor(value, decimals)

Equivalent to the Math.floor() function: it rounds down and returns the largest integer less than or equal to the number.

Name Description Type
value Value to round down Numberish
decimals (optional) Result decimals (defaults to value decimals) number
returns Result value Dnum

ceil(value, decimals)

Equivalent to the Math.ceil() function: it rounds rounds up and returns the smaller integer greater than or equal to the number.

Name Description Type
value Value to round up Numberish
decimals (optional) Result decimals (defaults to value decimals) number
returns Result value Dnum

greaterThan(value1, value2, decimals)

Equivalent to the > operator: it returns true if the first value is greater than the second value and false otherwise, regardless of their respective decimals.

Name Description Type
value1 First value Numberish
value2 Second value Numberish
decimals (optional) Result decimals (defaults to value1 decimals) number
returns Result value Dnum

Alias: gt()

Example

let value1 = [10000100n, 4];
let value2 = [100000n, 2];

dnum.greaterThan(value1, value2); // true
dnum.greaterThan(value1, value1); // false
dnum.greaterThan(value2, value1); // false

lessThan(value1, value2, decimals)

Equivalent to the < operator: it returns true if the first value is less than the second value and false otherwise, regardless of their respective decimals.

Name Description Type
value1 First value Numberish
value2 Second value Numberish
decimals (optional) Result decimals (defaults to value1 decimals) number
returns Result value Dnum

Alias: lt()

Example

let value1 = [100000n, 2];
let value2 = [10000100n, 4];

dnum.lessThan(value1, value2); // true
dnum.lessThan(value1, value1); // false
dnum.lessThan(value2, value1); // false

equal(value1, value2, decimals)

Equivalent to the == operator: it returns true if the first value is equal to the second value and false otherwise, regardless of their respective decimals.

Name Description Type
value1 First value Numberish
value2 Second value Numberish
decimals (optional) Result decimals (defaults to value1 decimals) number
returns Result value Dnum

Alias: eq()

Example

let value1 = [100000n, 2];
let value2 = [10000000n, 4];

dnum.lessThan(value1, value2); // true

compare(value1, value2)

Returns 1 if value1 > value2, -1 if value1 < value2, 0 if value1 == value2. It makes it easy to combine Dnum values with sorting functions such as Array#sort().

Name Description Type
value1 First value Numberish
value2 Second value Numberish
returns Result value 1 | -1 | 0

Alias: cmp()

Example

let sorted = [
  1,
  8n,
  [700n, 2],
  3.1,
  2n,
  5,
].sort(compare);

console.log(sorted); // [1, 2n, 3.1, 5, [700n, 2], 8n];

toNumber(value, optionsOrDigits)

Converts the Dnum data structure into a number. This might result in a loss of precision depending on how large the number is.

Name Description Type
value The number to convert into a number Dnum
options.digits Number of digits to keep after the decimal point. Setting options to a number acts as an alias for this option (see example below). Defaults to the number of decimals in the Dnum passed to value. number
options.decimalsRounding Method used to round to digits decimals (defaults to "ROUND_HALF"). "ROUND_HALF" | "ROUND_UP" | "ROUND_DOWN"
returns Result value number
let value = [123456789000000000000000n, 18];

toNumber(value); // 123456.789
toNumber(value, { digits: 1 }); // 123456.8
toNumber(value, 1); // 123456.8 (alias for { digits: 1 })

toString(value, optionsOrDigits)

Converts the Dnum data structure into a string, without any formatting. This might result in a loss of precision depending on how large the number is.

Name Description Type
value The number to convert into a string Dnum
options.digits Number of digits to keep after the decimal point. Setting options to a number acts as an alias for this option (see example below). Defaults to the number of decimals in the Dnum passed to value. string
options.decimalsRounding Method used to round to digits decimals (defaults to "ROUND_HALF"). "ROUND_HALF" | "ROUND_UP" | "ROUND_DOWN"
returns Result value string
let value = [123456789000000000000000n, 18];

toString(value); // "123456.789"
toString(value, { digits: 1 }); // "123456.8"
toString(value, 1); // "123456.8" (alias for { digits: 1 })

Note that if you want to format the number for display purposes, you should probably use format() instead. If you need to convert the number into a JSON-compatible string without any precision loss, use toJSON() instead.

toJSON(value)

Converts the Dnum data structure into a JSON-compatible string. This function is provided because JSON.stringify() doesnโ€™t work with BigInt data types.

Name Description Type
value The number to convert into a JSON Dnum
returns Result value string
let json = toJSON([123456789000000000000n, 18]);

// json == "[\"123456789000000000000\", 18]";

fromJSON(value)

Converts the string resulting from toJSON() back into a Dnum.

Name Description Type
value The string value to convert back into a Dnum string
returns Result value Dnum
let dnum = fromJSON("[\"123456789000000000000\", 18]");

// dnum == [123456789000000000000n, 18]

setDecimals(value, decimals, options)

Return a new Dnum with a different amount of decimals. The value will reflect this change so that the represented number stays the same.

Name Description Type
value The number from which decimals will be changed Dnum
decimals New number of decimals number
options.round In case of reduction, whether to round the remaining decimals (defaults to true). boolean
returns Result value Dnum

Note: from(value, decimals) can also be used instead.

Tree shaking

To make use of tree shaking, named exports are also provided:

import { format, from } from "dnum";

FAQ

Should dnum be used instead of BigInt or libraries such as BN.js or decimal.js?

dnum is not a full replacement for libraries such as decimal.js or BigInt. Instead, dnum focuses on a small (~1kb) set of utilities focused around the simple Dnum data structure, allowing to manipulate numbers represented in various decimal precisions in a safe manner.

Why is it called dnum?

dnum stands for Decimal Numbers.

Who made the logo and banner? ๐Ÿ˜

The gorgeous visual identity of dnum has been created by Paty Davila.

Acknowledgements

dnum's People

Contributors

bpierre avatar dependabot[bot] avatar savikko avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

dnum's Issues

Round to floor when div?

format([49998805n, 9], { digits: 4 } );
// return 0.05
// expected 0.0499

UX issue with normal rounding case:
The user use a little bit of gas token to do a transaction, but the displayed amount of balance still same because formatting is using normal round.

Change divideAndRound to floor div implementation will fix this.
What do you think?

toNumber typing requires the decimals parameter

First of all, thanks for working on this library, it's incredibly useful and well designed.

The docs seem to state that the number of digits is optional for the toNumber utility:

Defaults to the number of decimals in the Dnum passed to value.

However, TS will complain if/when only the Dnum is passed. This doesn't happen with the options for other utilities (e.g., format).

dnum.from Incorrect number from String() converting to scientific notation

in the from method
String(value) converts expoents greater than 21 or less than -6 to scientific notation, which makes it not pass the regex in the next line

I was trying something like multiply(price, 10 ** -8, 18)
It's not a big issue since divide(price, 10 ** Math.abs(expo), 18) should do the same

a possible solution could be something like this, I'm not sure of the implications or if this should be handled in the lib, if you want I can look further and open a pr

  value = String(value);
  
  if (value.includes('e')) value = Number(value).toFixed(20)
  // or value = typeof value === 'number' ? value.toFixed(20) : String(value)

  if (!value.match(NUM_RE)) {
    throw new Error(`Incorrect number: ${value}`);
  }

It took some time today to figure what I was doing wrong here haha

Number.toString

Scientific notation is used if the radix is 10 and the number's magnitude (ignoring sign) is greater than or equal to 10^21 or less than 10-6

Add option to disable commifying in `dn.format` function

Thedn.format function currently always commifies numbers by default. It would be beneficial to have an option to turn this behavior off. This would provide greater flexibility in formatting number strings and improve the interoperability between dn.from and dn.format.

import * as dn from "dnum";

let b = dn.from("870983127.93887"); // dn.from() can parse strings, numbers, bigint and more

console.log(
  dn.format(b, 2), // Current behavior: "870,983,127.94"
  dn.format(b, { digits: 2, commify: false }), // Desired behavior: "870983127.94"
);

No trailing zero when there is no fraction?

const a = dn.from("5", 2);
dn.format(a, { digits: 2, trailingZeros: true })
// Expected 5.00
// Result 5

I was expecting number toFixed kinda behavior.
Shouldn't it also show trailing zero for consistency?
or is it intended?

const a = dn.from("5", 4);
const b = dn.from("5.1", 4);
const c = dn.from("5.12", 4);
const d = dn.from("5.123", 4);

console.log(dn.format(a, { digits: 4, trailingZeros: true })); // 5
console.log(dn.format(b, { digits: 4, trailingZeros: true })); // 5.1000
console.log(dn.format(c, { digits: 4, trailingZeros: true })); // 5.1200
console.log(dn.format(d, { digits: 4, trailingZeros: true })); // 5.1230

Problem with Jest

I cannot make my unit tests work.

I get this error:

Jest encountered an unexpected token

Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

If I remove the lines of code that exploit dnum the tests work perfectly.

This is my jest.config.js:

module.exports = {
    preset: 'ts-jest',
    testEnvironment: 'node',
    roots: ["<rootDir>/src"],
    testMatch: ['**/*.test.(ts|tsx)'],
};

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.