@arki/math

For @arki/[email protected] · source README.md

Zero-dependency math toolkit for TypeScript. Exact where it must be, fast where it can be, and honest about which is which. Runs unchanged in Node 22+, Bun, Deno, browsers, and React Native (Hermes).

Install

bun add @arki/math
# or: npm install @arki/math
# or: deno add npm:@arki/math

Quick start

Total an invoice exactly. Only the tax step rounds, and it says how.

import { decimal } from '@arki/math/decimal';
 
type InvoiceLine = { readonly price: string; readonly quantity: number };
 
/** Totals an invoice exactly. Only the tax step rounds, and it says how. */
export function invoiceTotals(lines: readonly InvoiceLine[], taxRate: string) {
  const subtotal = lines.reduce((total, line) => total.plus(decimal(line.price).times(line.quantity)), decimal(0));
  const tax = subtotal.times(taxRate).toScale(2, 'halfEven');
  return { subtotal: subtotal.toString(), tax: tax.toString(), total: subtotal.plus(tax).toString() };
}
 
invoiceTotals(
  [
    { price: '19.99', quantity: 3 },
    { price: '0.10', quantity: 3 },
  ],
  '0.19',
);
// { subtotal: '60.27', tax: '11.45', total: '71.72' }

Float math gives 60.269999999999996 here. Decimal gives 60.27.

When not to use this

  • You need symbolic algebra or expression parsing. Use @arki/expr and @arki/cas, built on this package.
  • You need money with a currency. Use @arki/money. It builds on this package: same rounding modes, same error family, and it accepts Decimal and Rational as amounts.
  • You need sparse matrices or big numerical linear algebra. linear-algebra works on plain arrays of float64.
  • You need arbitrary-precision trigonometry. complex, geometry and the rest of the numeric layer are float64.
  • You only add and compare small integers. Plain number is enough.

Runtime support

  • Node 22+, Bun, Deno, browsers: everything, verified by the test suite and a smoke run of the built package in each runtime (bun run test:runtimes).
  • React Native (Hermes V1): everything runs; bun run test:runtimes checks it when HERMES_BIN points at a Hermes binary. Two Intl gaps shape formatting and parsing:
    • Hermes formats decimal text through a double. Decimal values are rounded exactly first, so anything up to 15 shown digits (every normal amount) prints exactly; more throws UnsupportedFeatureError instead of printing a wrong digit. Compact notation accepts whole numbers only.
    • Hermes on iOS has no formatToParts, so locale parsing throws UnsupportedFeatureError; pass symbols to parse without Intl.
    • A non-default rounding mode throws UnsupportedFeatureError.
  • Pure ESM, no runtime-specific globals; the lint config rejects any node:*/bun:* import or process/Buffer use in library code.
RuntimeResultVersion testedDate (UTC)
nodepassv26.11.12026-10-17
bunpass1.4.22026-10-17
denopass2.9.72026-10-17
browserpassChromium 156.0.8078.42026-10-17
hermesnot run-2026-10-17

Public imports

Everything is a subpath import, tree-shakeable by concern. The root @arki/math re-exports it all except linear-algebra, whose short names (plus, times, norm, inverse) would clash at the root.

ImportWhat it gives you
@arki/math/decimaldecimal(), Decimal — exact decimals with retained scale
@arki/math/rationalratio(), Rational — exact fractions on bigint
@arki/math/numberclamp, lerp, inverseLerp, remap, inRange, round, roundTo, roundToSignificant, significantDigits, approxEqual, range, linspace
@arki/math/integergcd, lcm, divmod, factorial, combinations, permutations, isPrime, primeFactors, divisors, modPow, modInverse, fibonacci, toBase, fromBase
@arki/math/statssum, mean, median, mode, percentile, variance, stddev, minMax, correlation, zScore, confidenceInterval, sampleSize, linear/exponential/logarithmic/power/polynomial regression; mean…stddev exact on Decimal[]
@arki/math/unitsconvert, convertExact, formatUnit — 22 categories, exact ratios
@arki/math/financepayment, amortizationSchedule, futureValue, netPresentValue, internalRateOfReturn, … — on Decimal; finance/float has the same verbs on number
@arki/math/distributionsnormal, studentT, noncentralT, chiSquare, fisherF, exponential, uniform, binomial, poisson, erf, erfInv, gamma, gammaLn, beta, …
@arki/math/geometrysolveTriangle(s), circle, circleSector, ellipse, regularPolygon, solids, distance, polygonArea, pythagoreanTriples
@arki/math/linear-algebrasolve, inverse, determinant, LU, pivoted QR, Cholesky, SVD, leastSquares, pseudoInverse, conditionNumber, rank, eigenvalues and eigenvectors — on plain arrays (subpath only)
@arki/math/complexcomplex(), Complex — arithmetic, polar form, exp, ln, sqrt, pow, nthRoots, sin…atanh
@arki/math/polynomialevaluatePolynomial, polynomialDerivative, quadraticRoots, cubicRoots, polynomialRoots
@arki/math/decimal-mathsqrt, cbrt, nthRoot, exp, ln, log10, log2, log, pow, PI, E — correctly rounded, on Decimal
@arki/math/wordsspellNumber, spellOrdinal, formatOrdinal; locales en, enGB built in, words/ro, /de, /fr, /es, /it opt-in
@arki/math/formatformatNumber, formatCurrency, formatPercent, formatCompact, formatFileSize, createFormatter
@arki/math/parseparseNumber, parseDecimal — locale-aware, the parser Intl never shipped
@arki/math/randomcreateRandom(seed), randomInt, shuffle, sample, pick, …
@arki/math/fluentnum() — immutable fluent wrapper for plain numbers
@arki/math/roundingRoundingMode, ROUNDING_MODES, isRoundingMode
@arki/math/rootsfindRoot (Brent), bisect, newton, findRoots — with an error estimate
@arki/math/integrateintegrate — adaptive Gauss–Kronrod and tanh-sinh, infinite intervals
@arki/math/differentiatederivative (Ridders), limit (Richardson) — numerical estimates with an error estimate
@arki/math/errorsMathError and its subclasses

Deliberately not here: expression parsing and symbolic algebra (they are languages with their own security surface; they live in the sibling packages @arki/expr and @arki/cas), sparse matrices, and arbitrary-precision trigonometry. geometry, linear-algebra, complex, polynomial, roots, integrate and differentiate work on plain number (float64), like the rest of the numeric layer.

Failure behaviour

Every error has a stable code (ARKI_MATH_E001 to E011, also exported as MATH_ERROR_CODES) and a one-sentence remediation. Match on the code, never on the message. Wrong runtime types still throw the built-in TypeError.

import { decimal } from '@arki/math/decimal';
import { MathError, RoundingNecessaryError } from '@arki/math/errors';
 
try {
  decimal(1).dividedBy(3);
} catch (error) {
  if (error instanceof RoundingNecessaryError) {
    /* ask for a scale */
  }
  if (error instanceof MathError) {
    /* any failure from this package */
  }
}
CodeErrorWhen
ARKI_MATH_E001NumberFormatErrora string is not a number in the accepted syntax
ARKI_MATH_E002InvalidArgumentErrorreversed bounds, negative step, p out of range, …
ARKI_MATH_E003DivisionByZeroErrordivision by zero, zero to a negative power
ARKI_MATH_E004RoundingNecessaryErroran exact result needs rounding and no mode was given
ARKI_MATH_E005NumericRangeErrornon-finite, unsafe integer, overflow or underflow
ARKI_MATH_E006EmptyDataErrora statistic of an empty (or too small) data set
ARKI_MATH_E007ResourceLimitErrormore than 10 000 digits, or an exponent beyond ±10 000
ARKI_MATH_E008UnsupportedFeatureErrorthe runtime's Intl lacks a feature the call relies on
ARKI_MATH_E009SingularMatrixErrorsolve/inverse met a matrix singular to working precision
ARKI_MATH_E010ConvergenceErroran iterative method did not converge (polynomialRoots, eigenvalues, SVD; RootConvergenceError, IntegrationConvergenceError, ExtrapolationConvergenceError carry .best)
ARKI_MATH_E011NotPositiveDefiniteErrorcholeskyDecompose met a matrix that is not positive definite

Limits

Every Decimal and Rational — input or result — has at most 10 000 digits (integer digits plus scale) and an exponent magnitude of at most 10 000; anything larger throws ResourceLimitError, early from an estimate and exactly on the result. Internal guard digits do not count, so PI(9999) is the largest π, and every value the library returns parses back from its own toString(). Bigint factorial, permutations and combinations allow results up to 100 000 digits; fibonacci and fromBase keep 10 000. These are resource guards against hostile input, not precision settings — within them, arithmetic is exact.

Boundary example: a Decimal with 10 000 digits is the largest allowed. One more digit throws ResourceLimitError.

import { decimal } from '@arki/math/decimal';
import { ResourceLimitError } from '@arki/math/errors';
 
decimal('9'.repeat(10_000)); // the largest allowed
let limited = false;
try {
  decimal('9'.repeat(10_001));
} catch (error) {
  limited = error instanceof ResourceLimitError;
}
if (!limited) throw new Error('10 001 digits must throw ResourceLimitError');

Documentation

One page per module, with the full reference and examples. The accuracy claims and their evidence are in Accuracy.

ModulePage
@arki/math/complexcomplex
@arki/math/decimaldecimal
@arki/math/decimal-mathdecimal-math
@arki/math/differentiatedifferentiate
@arki/math/distributionsdistributions
@arki/math/financefinance
@arki/math/fluentfluent
@arki/math/formatformat
@arki/math/geometrygeometry
@arki/math/integerinteger
@arki/math/integrateintegrate
@arki/math/linear-algebralinear-algebra
@arki/math/numbernumber
@arki/math/parseparse
@arki/math/polynomialpolynomial
@arki/math/randomrandom
@arki/math/rationalrational
@arki/math/rootsroots
@arki/math/roundingrounding
@arki/math/statsstats
@arki/math/unitsunits
@arki/math/wordswords
  • Accuracy: what is exact, what is correctly rounded, and the reference data behind each claim.
  • Agent guide: the model, the task map and the rules.
  • llms.txt: the short index for agents.
  • CHANGELOG: what changed in each version.

Support

Report a bug or ask a question in the Arki issue tracker. Include the module, the input and the error code.

Two layers, by design

Exact types for money, measurements, calculators — anything where 0.1 + 0.2 !== 0.3 is a bug:

import { decimal } from '@arki/math/decimal';
import { ratio } from '@arki/math/rational';
 
decimal('0.1').plus('0.2').toString(); // '0.3'
decimal('19.99').times(3).toString(); // '59.97'  (scale is kept: 1.10 × 3 = 3.30)
decimal(1).dividedBy(8).toString(); // '0.125'  (exact, so no rounding needed)
decimal(1).dividedBy(3); // throws RoundingNecessaryError — say how to round:
decimal(1).dividedBy(3, { scale: 4, rounding: 'halfEven' }).toString(); // '0.3333'
 
ratio(1, 3).plus(ratio(1, 6)).toString(); // '1/2'
ratio('0.(3)').eq(ratio(1, 3)); // true  — repeating decimals parse
ratio('3.14159265').approximate(1000).toString(); // '355/113'

Plain-number helpers for UI, geometry, charts — fast float math with the sharp edges filed off:

import { clamp, inRange, lerp, round, roundTo } from '@arki/math/number';
 
round(1.005, 2); // 1.01  — rounds the number you typed, not its binary approximation
round(2.5, 0, 'halfEven'); // 2
roundTo(127.3, 5); // 125
clamp(105, 0, 100); // 100
lerp(0, 10, 0.5); // 5
inRange(5, 0, 5); // false — default bounds are [min, max)

Nothing in this package returns NaN or null to signal failure. Every deliberate failure is a typed error under MathError; wrong runtime types throw TypeError.

Accuracy

Every claim is checked by a test against independent reference data: mpmath for decimal-math, the special functions and the distributions, and the NIST StRD certified values for statistics and regression. The guarantees come in three kinds: exact, correctly rounded, and accurate to a stated bound. The table of every row, with its evidence, is in Accuracy.

Benchmarks

Speed of @arki/math against the libraries most people reach for: decimal.js, big.js and bignumber.js for decimal arithmetic; fraction.js for exact fractions; simple-statistics and jstat for descriptive statistics; jstat for probability distributions; simple-statistics and regression (regression-js) for curve fitting; ml-matrix and mathjs for linear algebra; complex.js and mathjs for complex numbers; mathjs for polynomial roots. Each table only ranks libraries that returned the same result on every checked input. The published tables come from a publish run on an idle machine and keep every case, including the ones where @arki/math is slower.

Node v22.13.1 on Apple M2 Max, 2026-10-13. Median time per operation, lower is better.

Decimal plus (1234.5678 + 98.76)

LibraryTime / opvs fastest
@arki/math102.0 nsfastest
decimal.js388.5 ns3.81×
big.js386.0 ns3.78×
bignumber.js268.1 ns2.63×

Decimal times

LibraryTime / opvs fastest
@arki/math98.7 nsfastest
decimal.js664.9 ns6.73×
big.js332.4 ns3.37×
bignumber.js502.8 ns5.09×

Decimal divide, 10 places, half-even

Every library rounds the quotient to 10 decimal places, half-even. The other libraries divide at a working precision first and round after; @arki/math rounds once, exactly.

LibraryTime / opvs fastest
@arki/math dividedBy({ scale, rounding })673.5 nsfastest
decimal.js div + toDecimalPlaces11.21 µs16.65×
big.js div + round4.12 µs6.11×
bignumber.js dividedBy + decimalPlaces13.34 µs19.81×

Decimal parse a string

LibraryTime / opvs fastest
@arki/math decimal()476.1 nsfastest
decimal.js712.0 ns1.50×
big.js699.4 ns1.47×
bignumber.js911.6 ns1.91×

Decimal toString (30 integer digits, 9 decimals)

LibraryTime / opvs fastest
@arki/math336.6 ns1.00×
decimal.js460.7 ns1.37×
big.js2.06 µs6.13×
bignumber.js336.3 nsfastest

Rational plus (355/113 + 22/7)

LibraryTime / opvs fastest
@arki/math ratio235.6 nsfastest
fraction.js441.5 ns1.87×

Rational times

LibraryTime / opvs fastest
@arki/math ratio619.0 ns1.77×
fraction.js349.4 nsfastest

Mean of 1,000 numbers

LibraryTime / opvs fastest
@arki/math34.69 µs12.80×
simple-statistics2.71 µsfastest

Standard deviation of 1,000 numbers (population)

LibraryTime / opvs fastest
@arki/math14.85 µs5.85×
simple-statistics standardDeviation2.54 µsfastest

90th percentile of 1,000 numbers

Both interpolate linearly between ranks, so results agree to rounding.

LibraryTime / opvs fastest
@arki/math percentile304.59 µs61.28×
simple-statistics quantile4.97 µsfastest

Normal distribution cdf at 1.96

LibraryTime / opvs fastest
@arki/math normal().cdf470.7 ns2.34×
jstat normal.cdf201.3 nsfastest

Normal distribution quantile at 0.975

LibraryTime / opvs fastest
@arki/math normal().quantile1.28 µsfastest
jstat normal.inv2.39 µs1.88×

Student t (10 degrees of freedom) quantile at 0.975

@arki/math solves the quantile until |cdf(x) − p| ≤ 1e-12 and rejects bad input; jstat uses a looser fixed approximation with no input checks.

LibraryTime / opvs fastest
@arki/math studentT(10).quantile4.90 µs5.03×
jstat studentt.inv974.2 nsfastest

Read the rows with the contracts in mind:

  • @arki/math Decimal is exact: it stores a bigint plus a scale and never rounds unless you ask. Division throws when the result does not terminate, so every divide case below passes an explicit { scale, rounding }.
  • decimal.js and bignumber.js round to a working precision on every operation (here 40 significant digits for decimal.js, 20 decimal places for bignumber.js and big.js). They are fast because they give up exactness; @arki/math gives up some speed to keep it.
  • Rational is exact and always reduced to lowest terms, as in fraction.js.
  • Statistics functions in @arki/math check every element is a finite number, copy the input, sum with Neumaier compensation (so the total is accurate) and sort the full array for percentiles. simple-statistics skips the checks and the copy, uses plain summation and selects the percentile without a full sort. That is why they are much faster on 1,000 numbers: @arki/math trades speed for validated input and accurate sums.

Reproduce with bun run bench in the package directory; regenerate this section with bun run bench --write-readme.

License

MIT

All docs for @arki/[email protected]