A high-precision fixed-point math library for JavaScript and TypeScript, built around native bigint for maximum performance and precision.
All benchmarks use 18-decimal precision and are run with Node.js v24 on compiled JS (no transpiler overhead). BigInt baselines are guarded against V8 constant-folding and dead-code elimination to ensure fair comparison.
| Operation | FixedPoint | dnum | BigNumber.js | plain BigInt |
|---|---|---|---|---|
| Addition | 48,821,402 | 23,050,494 | 13,220,627 | 62,609,254 |
| Subtraction | 46,123,366 | 22,932,617 | 14,295,216 | 57,004,234 |
| Multiplication | 18,024,463 | 3,614,501 | 2,510,335 | 30,465,962 |
| Division | 7,965,347 | 2,618,008 | 679,099 | 27,769,196 |
| Negation | 70,856,959 | — | 28,683,419 | 86,655,736 |
| Absolute value | 63,252,107 | 34,136,219 | 27,761,700 | 70,155,342 |
| Square root | 138,051 | — | 221,647 | — |
| Operation | FixedPoint | dnum | BigNumber.js | plain BigInt |
|---|---|---|---|---|
| Equal (eq) | 97,780,074 | 35,815,419 | 29,563,571 | 102,139,026 |
| Greater than (gt) | 88,555,713 | 34,580,855 | 29,927,076 | 100,810,362 |
| Less than (lt) | 87,459,835 | 34,995,875 | 29,698,561 | 101,387,607 |
| Greater or equal (gte) | 87,588,687 | — | 29,974,806 | 101,268,592 |
| Operation | FixedPoint | dnum | BigNumber.js | plain BigInt |
|---|---|---|---|---|
| Round (half-up) | 12,388,563 | — | 7,858,430 | — |
| Floor | 13,089,864 | 2,516,361 | 8,070,241 | — |
| Ceil | 12,246,498 | 2,525,178 | 7,956,707 | — |
| Precision change (18→6) | 26,231,015 | 5,331,133 | 6,521,731 | 10,979,108 |
| Operation | FixedPoint | dnum | BigNumber.js | plain BigInt |
|---|---|---|---|---|
| Creation from string | 3,861,335 | 1,916,232 | 4,073,521 | 18,526,058 |
| To decimal string | 7,819,748 | 1,720,008 | 6,614,876 | 20,246,125 |
| Predicates (isZero etc.) | 83,906,467 | — | 106,068,155 | 84,713,415 |
| Static min + max | 14,044,146 | — | 3,034,006 | 20,232,420 |
| Operation | FixedPoint | dnum | BigNumber.js | plain BigInt |
|---|---|---|---|---|
| Large multiply | 17,328,043 | 3,325,616 | 878,583 | 21,076,595 |
| Large division | 8,254,755 | 2,672,421 | 508,865 | 26,058,682 |
| Mixed precision add (6+18) | 14,115,163 | — | 13,907,332 | 10,597,417 |
| Chained: (a+b)*a-b | 11,593,415 | 2,772,107 | 2,019,168 | 19,405,599 |
All values are operations per second (higher is better). Bold marks the fastest library result (excluding plain BigInt, which serves as the theoretical ceiling).
Run benchmarks yourself:
npm run bench- Arbitrary precision fixed-point arithmetic
- Built on JavaScript's native
biginttype - Comprehensive math operations
- No external dependencies
- TypeScript support
# Using npm
npm install @hastom/fixed-point
# Using yarn
yarn add @hastom/fixed-point
# Using pnpm
pnpm add @hastom/fixed-pointimport { fpFromDecimal, fpFromInt } from '@hastom/fixed-point';
// Create fixed-point numbers with precision of 18 decimal places
const a = fpFromDecimal('123.456789', 18);
const b = fpFromDecimal('0.000000000000000001', 18); // Smallest possible value with precision of 18
const c = fpFromInt(100000000000000000000n, 18, 18); // Already scaled value with source and destination precision of 18
// Perform arithmetic operations
const sum = a.add(c);
console.log(sum.toDecimalString()); // '223.456789000000000000'
// Comparison
if (a.gt(b)) {
console.log('a is greater than b');
}
// Convert to decimal string
console.log(a.toDecimalString()); // '123.456789000000000000'Creates a fixed-point number from a decimal value.
Parameters:
src: The decimal value to convert (as a number, string, or bigint)dstPrecision: The precision (number of decimal places) for the resulting FixedPoint
import { fpFromDecimal } from '@hastom/fixed-point';
// Create a fixed-point number with 18 decimal places
const num1 = fpFromDecimal('123.456789', 18);
// Create a fixed-point number with 10 decimal places
const num2 = fpFromDecimal('3.1415926535', 10);
// Can also use a number as input (but be aware of JS floating point limitations)
const num3 = fpFromDecimal(123.456, 18);
// Or use a bigint (which will be scaled by the precision)
const num4 = fpFromDecimal(123n, 18); // Equivalent to 123.000000000000000000Creates a fixed-point number from an already scaled integer value.
Parameters:
src: The integer value (in an already scaled form)srcPrecision: The precision of the source integerdstPrecision: The precision for the resulting FixedPoint
import { fpFromInt } from '@hastom/fixed-point';
// Create a fixed-point number from a scaled integer
// 100000000000000000000n represents 100.0 with 18 decimal places
const num1 = fpFromInt(100000000000000000000n, 18, 18);
// Convert from one precision to another
// 12345 represents 1.2345 with 4 decimal places, converting to 8 decimal places
const num2 = fpFromInt(12345, 4, 8); // Will be 1.23450000 with 8 decimal places
// Using a string for a very large number
const num3 = fpFromInt('123456789012345678901234567890', 18, 18);Adds two fixed-point numbers.
const a = fpFromDecimal('10.5', 18);
const b = fpFromDecimal('20.3', 18);
const result = a.add(b);
console.log(result.toDecimalString()); // '30.800000000000000000'Subtracts a value from the fixed-point number.
const a = fpFromDecimal('30.5', 18);
const b = fpFromDecimal('10.3', 18);
const result = a.sub(b);
console.log(result.toDecimalString()); // '20.200000000000000000'Multiplies the fixed-point number by another value.
const a = fpFromDecimal('2.5', 18);
const b = fpFromDecimal('3', 18);
const result = a.mul(b);
console.log(result.toDecimalString()); // '7.500000000000000000'Divides the fixed-point number by another value.
const a = fpFromDecimal('10', 18);
const b = fpFromDecimal('2.5', 18);
const result = a.div(b);
console.log(result.toDecimalString()); // '4.000000000000000000'Negates the fixed-point number.
const a = fpFromDecimal('10.5', 18);
const result = a.neg();
console.log(result.toDecimalString()); // '-10.500000000000000000'Returns the absolute value of the fixed-point number.
const a = fpFromDecimal('-10.5', 18);
const result = a.abs();
console.log(result.toDecimalString()); // '10.5'Calculates the square root of the fixed-point number using the Newton-Raphson method.
// Perfect squares
const a = fpFromDecimal('25', 18);
const result = a.sqrt();
console.log(result.toDecimalString()); // '5.000000000000000000'
// Non-perfect squares
const b = fpFromDecimal('2', 18);
const result2 = b.sqrt();
console.log(result2.toDecimalString()); // '1.414213562373095049'
// Decimal numbers
const c = fpFromDecimal('0.25', 18);
const result3 = c.sqrt();
console.log(result3.toDecimalString()); // '0.500000000000000000'
// Error handling - negative numbers throw an error
const d = fpFromDecimal('-1', 18);
try {
d.sqrt(); // Throws: 'Cannot calculate square root of negative number'
} catch (error) {
console.error(error.message);
}Note: The sqrt() method preserves the precision of the original number and uses higher working precision internally for accurate calculations. There's also a squareRoot() alias available.
Checks if the fixed-point number is equal to another value.
const a = fpFromDecimal('10.5', 18);
const b = fpFromDecimal('10.5', 18);
console.log(a.eq(b)); // trueChecks if the fixed-point number is greater than another value.
const a = fpFromDecimal('10.5', 18);
const b = fpFromDecimal('5.2', 18);
console.log(a.gt(b)); // trueChecks if the fixed-point number is greater than or equal to another value.
const a = fpFromDecimal('10.5', 18);
const b = fpFromDecimal('10.5', 18);
console.log(a.gte(b)); // trueChecks if the fixed-point number is less than another value.
const a = fpFromDecimal('5.2', 18);
const b = fpFromDecimal('10.5', 18);
console.log(a.lt(b)); // trueChecks if the fixed-point number is less than or equal to another value.
const a = fpFromDecimal('10.5', 18);
const b = fpFromDecimal('10.5', 18);
console.log(a.lte(b)); // trueReturns the base fixed-point representation (the internal scaled value as a string).
const a = fpFromDecimal('10.5', 18);
console.log(a.toString()); // '10500000000000000000' (for precision of 18)Converts the fixed-point number to a decimal string.
const a = fpFromDecimal('10.5', 18);
console.log(a.toDecimalString()); // '10.500000000000000000'Converts the fixed-point number to a JavaScript number.
const a = fpFromDecimal('10.54321', 18);
console.log(a.toDecimal()); // 10.54321Checks if the fixed-point number is zero.
const a = fpFromDecimal('0', 18);
console.log(a.isZero()); // trueChecks if the fixed-point number is positive.
const a = fpFromDecimal('10.5', 18);
console.log(a.isPositive()); // trueChecks if the fixed-point number is negative.
const a = fpFromDecimal('-10.5', 18);
console.log(a.isNegative()); // trueConverts the fixed-point number to a different precision, returning a new FixedPoint instance.
import { Rounding } from '@hastom/fixed-point';
const a = fpFromDecimal('10.54321', 18);
const result = a.toPrecision(2); // Default rounding is ROUND_DOWN
console.log(result.toDecimalString()); // '10.54'
// With explicit rounding mode
const rounded = a.toPrecision(2, Rounding.ROUND_HALF_UP);
console.log(rounded.toDecimalString()); // '10.54'
// Increase precision (no rounding needed)
const expanded = a.toPrecision(20);
console.log(expanded.toDecimalString()); // '10.54321000000000000000'Modifies the precision of the current FixedPoint instance in place.
import { Rounding } from '@hastom/fixed-point';
const a = fpFromDecimal('10.987654', 18);
console.log(a.toDecimalString()); // '10.987654000000000000'
// Reduce precision with default rounding (ROUND_DOWN)
a.setPrecision(3);
console.log(a.toDecimalString()); // '10.987'
// Create another instance to show different rounding
const b = fpFromDecimal('10.987654', 18);
b.setPrecision(3, Rounding.ROUND_HALF_UP);
console.log(b.toDecimalString()); // '10.988'
// Increase precision
b.setPrecision(10);
console.log(b.toDecimalString()); // '10.9880000000'Rounds the fixed-point number to the nearest integer using the specified rounding mode.
import { Rounding } from '@hastom/fixed-point';
const a = fpFromDecimal('10.7', 18);
const b = fpFromDecimal('-10.7', 18);
const c = fpFromDecimal('10.5', 18);
const d = fpFromDecimal('-10.5', 18);
// ROUND_UP - Away from zero
console.log(a.round(Rounding.ROUND_UP).toDecimalString()); // '11.000000000000000000'
console.log(b.round(Rounding.ROUND_UP).toDecimalString()); // '-11.000000000000000000'
// ROUND_DOWN - Towards zero (default)
console.log(a.round(Rounding.ROUND_DOWN).toDecimalString()); // '10.000000000000000000'
console.log(b.round(Rounding.ROUND_DOWN).toDecimalString()); // '-10.000000000000000000'
// ROUND_CEIL - Towards positive infinity
console.log(a.round(Rounding.ROUND_CEIL).toDecimalString()); // '11.000000000000000000'
console.log(b.round(Rounding.ROUND_CEIL).toDecimalString()); // '-10.000000000000000000'
// ROUND_FLOOR - Towards negative infinity
console.log(a.round(Rounding.ROUND_FLOOR).toDecimalString()); // '10.000000000000000000'
console.log(b.round(Rounding.ROUND_FLOOR).toDecimalString()); // '-11.000000000000000000'
// ROUND_HALF_UP - If halfway (.5), rounds away from zero
console.log(c.round(Rounding.ROUND_HALF_UP).toDecimalString()); // '11.000000000000000000'
console.log(d.round(Rounding.ROUND_HALF_UP).toDecimalString()); // '-11.000000000000000000'
// ROUND_HALF_DOWN - If halfway (.5), rounds towards zero
console.log(c.round(Rounding.ROUND_HALF_DOWN).toDecimalString()); // '10.000000000000000000'
console.log(d.round(Rounding.ROUND_HALF_DOWN).toDecimalString()); // '-10.000000000000000000'
// ROUND_HALF_EVEN - If halfway (.5), rounds to even neighbor (banker's rounding)
const e = fpFromDecimal('2.5', 18);
const f = fpFromDecimal('3.5', 18);
console.log(e.round(Rounding.ROUND_HALF_EVEN).toDecimalString()); // '2.000000000000000000'
console.log(f.round(Rounding.ROUND_HALF_EVEN).toDecimalString()); // '4.000000000000000000'Rounds down to the nearest integer (shorthand for round(Rounding.ROUND_FLOOR)).
const a = fpFromDecimal('10.7', 18);
const b = fpFromDecimal('-10.3', 18);
console.log(a.floor().toDecimalString()); // '10.000000000000000000'
console.log(b.floor().toDecimalString()); // '-11.000000000000000000'Rounds up to the nearest integer (shorthand for round(Rounding.ROUND_CEIL)).
const a = fpFromDecimal('10.3', 18);
const b = fpFromDecimal('-10.7', 18);
console.log(a.ceil().toDecimalString()); // '11.000000000000000000'
console.log(b.ceil().toDecimalString()); // '-10.000000000000000000'The library supports various rounding modes through the Rounding enum:
ROUND_UP: Rounds away from zeroROUND_DOWN: Rounds towards zero (truncation)ROUND_CEIL: Rounds towards positive infinityROUND_FLOOR: Rounds towards negative infinityROUND_HALF_UP: Rounds to nearest neighbor, halfway cases away from zeroROUND_HALF_DOWN: Rounds to nearest neighbor, halfway cases towards zeroROUND_HALF_EVEN: Rounds to nearest neighbor, halfway cases to even neighbor (banker's rounding)ROUND_HALF_CEIL: Rounds to nearest neighbor, halfway cases towards positive infinityROUND_HALF_FLOOR: Rounds to nearest neighbor, halfway cases towards negative infinity