DEV Community

Cover image for NumWiz: A Complete Number Utility Library for JavaScript & TypeScript — From Arithmetic to Scientific Computing
Subroto Saha
Subroto Saha

Posted on • Originally published at dev.to

NumWiz: A Complete Number Utility Library for JavaScript & TypeScript — From Arithmetic to Scientific Computing

NumWiz: A Complete Number Utility Library for JavaScript & TypeScript

"What if JavaScript had a math module as rich as Python's?"

That question is what led to NumWiz — a TypeScript-first, zero-dependency number utility library that covers everything from basic arithmetic to scientific computing, FFT, and arbitrary-precision decimals.

In this post I'll walk through the full library, show real code, and explain the design decisions behind it.


📦 What's Inside?

NumWiz ships 25 modules and 854 passing tests across 11 test suites. Here's the full inventory:

Module What it does
Arithmetic add, subtract, multiply, divide, pow, sqrt, log, exp...
Formatting Commas, Indian commas, Roman numerals, fractions, ordinals, abbreviations
Validation Type guards: prime, Armstrong, Harshad, perfect square, power-of-2...
Comparison Strict/approximate equality, clamp, sign, max/min
Conversion Base, temperature, length, weight, data, time, angle conversions
Bitwise AND, OR, XOR, shifts, bit-get/set/toggle, popcount, power-of-2
Trigonometry Full trig, hyperbolic, inverse trig, angle conversion + 9 math constants
Statistics mean, median, mode, variance, stdDev, percentile, quartiles, correlation
Financial EMI, SIP, compound interest, FV/PV, ROI, CAGR, amortization
Advanced Factorial, GCD/LCM, Fibonacci, Euler's totient, Collatz, lerp, map
Sequences Fibonacci, Lucas, primes, triangular, arithmetic/geometric progressions
Random Uniform, Gaussian, weighted, shuffle, sample, UUID
Range Create ranges, wrap, bounce, chunk
Currency Format, convert, abbreviate (Western & Indian), multilingual words
NumberWords Number-to-words in 10 languages + ordinals
Matrix 80+ methods: arithmetic, decompositions (LU, QR), eigenvalues, solve, solve
NDArray NumPy-style N-dimensional arrays with broadcasting
LinAlg SVD, inverse, eig, QR, LU, Cholesky, pinv, lstsq, norm, rank
Polynomial Arithmetic, roots, derivative, integral, least-squares fit
Calculus Differentiation (central diff, Jacobian, Hessian), integration (5 methods), root-finding
FFT Fast Fourier Transform, IFFT, real FFT, windowing, power spectrum, STFT
Interpolation Linear, polynomial, barycentric, cubic spline, bilinear, bicubic
Signal FIR filter design/apply, convolution, moving average, normalize, SNR
BigPrecision Arbitrary-precision decimals (powered by decimal.js)
numwiz() Chainable wrapper API for all of the above

🚀 Installation

npm install numwiz
Enter fullscreen mode Exit fullscreen mode

Everything is TypeScript-native. No @types/numwiz package needed — types are bundled.


Part 1 — The Core: A Fluent Chainable API

Before diving into individual modules, let's look at NumWiz's top-level chainable API:

import numwiz from "numwiz";

// Chain arithmetic, formatting, and output
numwiz(9876543)
  .add(123457)     // 10000000
  .multiply(2)     // 20000000
  .abbreviate();   // "20M"

// Multi-locale number to words
numwiz(1500000).locale("hi").toWords();   // "पंद्रह लाख"
numwiz(1500000).locale("en").toWords();   // "one million five hundred thousand"
numwiz(1500000).locale("de").toWords();   // "eine Million fünfhunderttausend"

// Currency formatting
numwiz(99.5).round(0).toCurrency("USD"); // "$100.00"

// Indian numeric system
numwiz(10000000).locale("en").toWordsIndian(); // "one crore"
Enter fullscreen mode Exit fullscreen mode

The chain is lazy and immutable — each method returns this with the updated numeric value. Terminal methods (.val(), .toString(), .toWords(), etc.) return plain values.

Safe Mode

Operations that would throw in strict mode become NaN in safe mode:

numwiz.safe(10).divide(0).val();     // NaN  (not a throw)
numwiz.safe(-9).sqrt().val();        // NaN
numwiz.safe(10).divide(0).isValid(); // false
Enter fullscreen mode Exit fullscreen mode

This is especially useful in form handlers or data pipelines where inputs are untrusted.


Part 2 — Static Modules

Each module is independently tree-shakeable:

import { Arithmetic, Statistics, Formatting } from "numwiz";
// or via subpath (tree-shakeable):
import Arithmetic from "numwiz/arithmetic";
import Statistics from "numwiz/statistics";
Enter fullscreen mode Exit fullscreen mode

Arithmetic

The basics, plus every edge case you'd ever want:

import { Arithmetic } from "numwiz";

Arithmetic.add(1, 2, 3, 4, 5);        // 15
Arithmetic.power(2, 10);               // 1024
Arithmetic.sqrt(2);                    // 1.4142135623730951
Arithmetic.log(Math.E);               // 1
Arithmetic.log10(1000);               // 3
Arithmetic.exp(1);                    // 2.718...
Arithmetic.floorDivide(17, 5);        // 3

// throws RangeError for sqrt(-1) — use BigPrecision for complex
Arithmetic.sqrt(-1); // ❌ RangeError: Cannot take square root of negative number
Enter fullscreen mode Exit fullscreen mode

Formatting — Numbers the Way Users Expect

import { Formatting } from "numwiz";

// Western vs Indian comma grouping
Formatting.addCommas(1234567.89);       // "1,234,567.89"
Formatting.addIndianCommas(1234567.89); // "12,34,567.89"

// Abbreviation
Formatting.abbreviate(1_500_000);       // "1.5M"
Formatting.abbreviateIndian(1_500_000); // "15L"
Formatting.abbreviateIndian(10_000_000);// "1Cr"

// Roman numerals
Formatting.toRoman(2026);    // "MMXXVI"
Formatting.fromRoman("XIV"); // 14

// Fractions
Formatting.toFraction(0.333);    // "1/3"
Formatting.toFraction(0.625, 16);// "5/8"

// Ordinals (multilingual)
Formatting.toOrdinal(3, "en"); // "3rd"
Formatting.toOrdinal(3, "hi"); // "3वाँ"
Formatting.toOrdinal(2, "de"); // "2."

// Scientific / engineering notation
Formatting.toScientific(12345, 2);  // "1.23e+4"
Formatting.toEngineering(12345);    // "12.345×10^3"

// Banker's rounding (round half to even)
Formatting.bankersRound(0.5);   // 0
Formatting.bankersRound(1.5);   // 2
Enter fullscreen mode Exit fullscreen mode

Validation — Beyond isNaN

import { Validation } from "numwiz";

Validation.isPrime(97);           // true
Validation.isArmstrong(153);      // true  (1³+5³+3³ = 153)
Validation.isPerfectNumber(28);   // true  (1+2+4+7+14 = 28)
Validation.isHarshad(18);         // true  (18 / (1+8) = 2, exact)
Validation.isPerfectSquare(144);  // true
Validation.isPowerOfTwo(1024);    // true
Validation.isAbundant(12);        // true  (sum of proper divisors > 12)
Validation.isInRange(5, 1, 10);   // true
Enter fullscreen mode Exit fullscreen mode

Trigonometry — With Constants

One of the recent additions — 9 named mathematical constants as static readonly properties:

import { Trigonometry } from "numwiz";

// Python math module equivalents
Trigonometry.PI;    // 3.141592653589793   (math.pi)
Trigonometry.E;     // 2.718281828459045   (math.e)
Trigonometry.TAU;   // 6.283185307179586   (math.tau)
Trigonometry.PHI;   // 1.618033988749895   (golden ratio)
Trigonometry.SQRT2; // 1.4142135623730951
Trigonometry.LN2;   // 0.6931471805599453
Trigonometry.LN10;  // 2.302585092994046

// Full trig family
Trigonometry.sin(Trigonometry.PI / 2);  // 1
Trigonometry.atan2(1, 1);               // Math.PI/4
Trigonometry.sinh(0);                   // 0

// Angle conversion
Trigonometry.toRadians(180);  // Math.PI
Trigonometry.toDegrees(Math.PI); // 180
Trigonometry.normalizeDegrees(370); // 10
Enter fullscreen mode Exit fullscreen mode

Statistics

import { Statistics } from "numwiz";

const data = [2, 4, 4, 4, 5, 5, 7, 9];

Statistics.mean(data);         // 5
Statistics.median(data);       // 4.5
Statistics.mode(data);         // [4]
Statistics.variance(data);     // 3.5
Statistics.stdDev(data);       // ≈1.87
Statistics.sampleStdDev(data); // 2

Statistics.percentile(data, 75);  // 5.5
Statistics.quartiles(data);       // { Q1: 4, Q2: 4.5, Q3: 5.5 }
Statistics.iqr(data);             // 1.5

Statistics.correlation(
  [1, 2, 3, 4, 5],
  [2, 4, 6, 8, 10]
); // 1   (perfect positive correlation)

Statistics.skewness(data); // ≈0.95
Statistics.geometricMean([1, 2, 4, 8]); // ≈2.83
Enter fullscreen mode Exit fullscreen mode

Financial

import { Financial } from "numwiz";

// Loan EMI: ₹5 lakh at 10% p.a. over 10 years
Financial.emi(500_000, 10, 120); // ≈6607

// SIP maturity: ₹5000/month at 12% for 10 years
Financial.sipFutureValue(5000, 12, 10); // ≈1,163,390

// Compound interest
Financial.compoundInterest(10_000, 8, 5); // ≈4693 (interest earned)

// CAGR
Financial.cagr(10_000, 20_000, 5); // ≈14.87%

// Amortization schedule
const schedule = Financial.amortizationSchedule(500_000, 10, 12);
console.log(schedule[0]);
// { month: 1, emi: 43956.46, principal: 39789.79, interest: 4166.67, balance: 460210.21 }
Enter fullscreen mode Exit fullscreen mode

NumberWords — 10 Languages

import { NumberWords } from "numwiz";

NumberWords.toWords(42, "en"); // "forty-two"
NumberWords.toWords(42, "hi"); // "बयालीस"
NumberWords.toWords(42, "de"); // "zweiundvierzig"
NumberWords.toWords(42, "es"); // "cuarenta y dos"
NumberWords.toWords(42, "fr"); // "quarante-deux"
NumberWords.toWords(42, "ar"); // "اثنان وأربعون"
NumberWords.toWords(42, "bn"); // "বিয়াল্লিশ"
NumberWords.toWords(42, "mr"); // "बेचाळीस"
NumberWords.toWords(42, "gu"); // "બેતાળીસ"
NumberWords.toWords(42, "ta"); // "நாற்பத்திரண்டு"

// Scale systems
NumberWords.toWords(10_000_000, "en", "indian");  // "one crore"
NumberWords.toWords(10_000_000, "en", "western"); // "ten million"
Enter fullscreen mode Exit fullscreen mode

Part 3 — Scientific Computing

NumWiz v1.1+ ships a full scientific computing stack. Let's work through each module.

NDArray — NumPy-style N-Dimensional Arrays

The NDArray class is the backbone of the scientific stack. It stores data in a flat Float64Array with C-order strides and supports full broadcasting.

import { NDArray } from "numwiz";

// --- Creation ---
const a = NDArray.arange(0, 12).reshape([3, 4]);
// [[0,1,2,3],[4,5,6,7],[8,9,10,11]]

NDArray.zeros([2, 3]);         // 2×3 all-zero
NDArray.ones([2, 3]);          // 2×3 all-one
NDArray.eye(4);                // 4×4 identity
NDArray.linspace(0, 1, 5);    // [0, 0.25, 0.5, 0.75, 1]
NDArray.logspace(0, 2, 3);    // [1, 10, 100]
NDArray.diag([1, 2, 3]);       // 3×3 diagonal matrix

// --- Shape ops ---
a.shape;       // [3, 4]
a.ndim;        // 2
a.size;        // 12
a.T;           // transposed (4×3)
a.flatten();   // [0,1,2,...,11]
a.reshape([2, 6]);
a.squeeze();
a.expandDims(0);

// --- Element-wise math ---
const b = NDArray.from([4, 9, 16]);
NDArray.sqrt(b).toArray();   // [2, 3, 4]
NDArray.log(b).toArray();    // [1.386, 2.197, 2.773]
NDArray.clip(b, 5, 12);      // [5, 9, 12]
NDArray.square(b);           // [16, 81, 256]

// --- Broadcasting ---
const x = NDArray.ones([3, 1]);
const y = NDArray.from([1, 2, 3]);
NDArray.add(x, y).toArray(); // [[2,3,4],[2,3,4],[2,3,4]]

// --- Reductions ---
const m = NDArray.from([[1, 2, 3], [4, 5, 6]]);
m.sum();       // 21
m.sum(0);      // [5, 7, 9]   column sums
m.sum(1);      // [6, 15]     row sums
m.mean();      // 3.5
m.std();       // ≈1.71
m.argmax();    // 5

// --- Matrix dot product ---
const A = NDArray.from([[1, 2], [3, 4]]);
A.dot(A).toArray(); // [[7,10],[15,22]]
Enter fullscreen mode Exit fullscreen mode

The internal layout is inspired by NumPy: a contiguous Float64Array, a Shape tuple, and C-order strides. Broadcasting uses the same shape-alignment algorithm NumPy uses — pad shorter shapes on the left, expand dimensions of size 1.

LinAlg — Full Linear Algebra Suite

import { LinAlg } from "numwiz";

const A = [[1, 2], [3, 4]];

// Inverse & determinant
LinAlg.det(A);   // -2
LinAlg.inv(A);   // [[-2, 1], [1.5, -0.5]]

// Eigenvalues & eigenvectors
const { values, vectors } = LinAlg.eig([[2, 1], [1, 2]]);
values;  // [3, 1]

// Singular Value Decomposition
const { U, S, Vt } = LinAlg.svd([[1, 2], [3, 4], [5, 6]]);

// Solve Ax = b
LinAlg.solve([[2, 1], [1, 3]], [5, 10]);  // [1, 3]

// Least-squares (overdetermined systems)
LinAlg.lstsq([[1, 1], [1, 2], [1, 3]], [6, 5, 7]);

// Decompositions
const { L, U, P } = LinAlg.lu(A);
const { Q, R } = LinAlg.qr(A);
const { L: Ch } = LinAlg.cholesky([[4, 2], [2, 3]]);

// Utilities
LinAlg.norm(A);         // Frobenius norm ≈5.477
LinAlg.matrixRank(A);   // 2
LinAlg.cond(A);         // condition number
LinAlg.pinv(A);         // Moore-Penrose pseudo-inverse
LinAlg.kron(A, [[1, 0], [0, 1]]); // Kronecker product
Enter fullscreen mode Exit fullscreen mode

Polynomial — Arithmetic, Roots, Fitting

import { Polynomial, PolyModule } from "numwiz";

// Represent x² - 3x + 2  as coefficients [1, -3, 2] (highest degree first)
const p = new Polynomial([1, -3, 2]);

p.eval(3);         // 2          → 9 - 9 + 2
p.roots();         // [2, 1]
p.derivative();    // 2x - 3     → Polynomial([2, -3])
p.integrate(0);    // (1/3)x³ - (3/2)x² + 2x
p.degree();        // 2
p.toString();      // "x^2 + -3x + 2"

// Arithmetic
const q = new Polynomial([1, 1]);  // x + 1
p.add(q);   // x² - 2x + 3
p.mul(q);   // x³ - 2x² - x + 2

// Static methods
PolyModule.fromRoots([1, 2, 3]); // builds poly from roots
PolyModule.fit(
  [0, 1, 2, 3, 4],   // x values
  [0, 1, 4, 9, 16],  // y values (x²)
  2                   // degree
);  // returns coefficients ≈ [1, 0, 0]
Enter fullscreen mode Exit fullscreen mode

Calculus — Differentiation & Integration

import { Calculus } from "numwiz";

// Numerical differentiation (central difference, h = 1e-5)
Calculus.derivative(Math.sin, 0);        // ≈1.0  (cos(0))
Calculus.derivative(x => x ** 3, 2);    // ≈12   (3x²|x=2)
Calculus.derivative2(x => x ** 3, 2);   // ≈12   (6x|x=2)

// Gradient vector of f(x, y) = x² + y²
Calculus.gradient(([x, y]) => x**2 + y**2, [1, 2]);
// ≈ [2, 4]

// Jacobian of vector function
Calculus.jacobian(
  [([x, y]) => x * y, ([x, y]) => x + y],
  [1, 2]
);
// ≈ [[2, 1], [1, 1]]

// Integration — 5 methods available
Calculus.integrate(x => x ** 2, 0, 3);              // ≈9   (simpson, default)
Calculus.integrate(x => x ** 2, 0, 3, 'trapz');     // ≈9
Calculus.integrate(x => x ** 2, 0, 3, 'romberg');   // ≈9   (adaptive, high precision)
Calculus.integrate(x => x ** 2, 0, 3, 'gauss5');    // ≈9   (5-point Gauss-Legendre)

// Work with data arrays (like scipy.integrate.trapz)
Calculus.trapz([0, 1, 4, 9, 16]);       // area under x² samples
Calculus.simps([0, 1, 4, 9, 16]);       // Simpson's rule on array

// Root-finding
Calculus.bisection(x => x**2 - 2, 1, 2);    // ≈1.4142   (√2)
Calculus.newton(x => x**2 - 2, x => 2*x, 1); // ≈1.4142   (faster convergence)
Calculus.brentq(x => Math.cos(x) - x, 0, 1); // ≈0.7391   (Brent's method)

// Optimization
Calculus.minimize(x => (x - 3)**2, 0);       // ≈{ x: 3, fx: 0 }
Enter fullscreen mode Exit fullscreen mode

FFT — Fast Fourier Transform

import { FFT } from "numwiz";

// Full complex FFT
const X = FFT.fft([1, 0, -1, 0]);
// NDArray of {re, im} objects

// Real-only FFT (only positive frequencies)
const halfSpectrum = FFT.rfft([1, 2, 3, 4, 5, 6, 7, 8]);

// Frequency bins for N-point FFT at sample rate fs
FFT.fftFreq(8, 1/1000).toArray();
// [-500, -375, -250, -125, 0, 125, 250, 375] Hz

// Power spectrum
FFT.powerSpectrum([1, 2, 3, 4]).toArray();  // |X|²/N

// Windowing (reduce spectral leakage)
const sig = [1, 2, 3, 4, 5, 6, 7, 8];
const windowed = FFT.applyWindow(sig, FFT.hanning(sig.length));
const spectrum = FFT.rfft(windowed);

// Magnitude and phase
FFT.magnitude(X);
FFT.phase(X);

// Shift: move DC to centre (like numpy.fft.fftshift)
FFT.fftShift(X);

// Available windows: hanning, hamming, blackman, bartlett
Enter fullscreen mode Exit fullscreen mode

Interpolation

import { Interpolation, CubicSpline } from "numwiz";

const xs = [0, 1, 2, 3, 4];
const ys = [0, 1, 8, 27, 64]; // x³

// Piecewise linear
Interpolation.linear(xs, ys, 1.5);      // 4.5

// Lagrange polynomial
Interpolation.polynomial(xs, ys, 2.5);  // ≈15.625

// Barycentric (numerically stable)
Interpolation.barycentric(xs, ys, 2.5);

// Cubic spline (natural boundary conditions)
const spline = new CubicSpline(xs, ys);
spline.interpolate(2.5);         // single point → number
spline.interpolate([0.5, 1.5]);  // array of points → number[]

// 2D bilinear interpolation
const grid = [[1, 2], [3, 4]];
Interpolation.bilinear(grid, 0.5, 0.5); // 2.5
Enter fullscreen mode Exit fullscreen mode

Signal Processing

import { Signal } from "numwiz";

// Generate a pure sine wave: 440 Hz, 8000 Hz sample rate, 0.1 sec
const tone = Signal.generateSine(440, 8000, 0.1);

// Add Gaussian noise
const noisy = Signal.addNoise(tone, 0.1);   // stdDev = 0.1

// Design a FIR low-pass filter (cutoff = 0.2, 31 taps)
const coeffs = Signal.firDesign(0.2, 31);

// Apply the filter
const filtered = Signal.firFilter(noisy, coeffs);

// Moving average smooth
const smooth = Signal.movingAverage(noisy, 5);

// Compute Signal-to-Noise Ratio
const noise = noisy.map((v, i) => v - tone[i]);
Signal.snr(tone, noise);  // dB

// Normalize to [-1, 1] (peak normalization)
Signal.normalize(filtered);

// Signal energy and RMS
Signal.energy(tone);  // sum of squares
Signal.rms(tone);     // root mean square

// Hilbert transform (analytic signal / envelope)
Signal.hilbert(tone);

// Convolution
Signal.convolve([1, 2, 3], [1, 1]);  // [1, 3, 5, 3]
Enter fullscreen mode Exit fullscreen mode

Part 4 — BigPrecision: Solving the 0.1 + 0.2 Problem

If you've been writing JavaScript long enough, you've seen this:

0.1 + 0.2 === 0.3  // false
0.1 + 0.2           // 0.30000000000000004
Enter fullscreen mode Exit fullscreen mode

This is a fundamental limitation of IEEE 754 double-precision floating point. Python's decimal module solves it. NumWiz does too — with BigPrecision.

import { BigPrecision, RoundingMode } from "numwiz";

// Exact decimal arithmetic
new BigPrecision("0.1").add("0.2").toString(); // "0.3" ✓
new BigPrecision("1.1").mul("3").toString();   // "3.3" ✓

// High-precision constants
BigPrecision.setPrecision(50);
BigPrecision.pi().toString();
// "3.14159265358979323846264338327950288419716939937510"

BigPrecision.e().toString();
// "2.71828182845904523536028747135266249775724709369995"
Enter fullscreen mode Exit fullscreen mode

Rounding Modes

One of the most important features — full control over how numbers are rounded:

// Default: ROUND_HALF_UP (standard rounding)
new BigPrecision("1.005").quantize(2).toString();   // "1.01"

// Banker's rounding (ROUND_HALF_EVEN) — used in finance
new BigPrecision("0.5").quantize(0, RoundingMode.ROUND_HALF_EVEN).toString(); // "0"
new BigPrecision("1.5").quantize(0, RoundingMode.ROUND_HALF_EVEN).toString(); // "2"

// Truncate toward zero
new BigPrecision("1.999").quantize(2, RoundingMode.ROUND_DOWN).toString(); // "1.99"

// Ceiling / floor
new BigPrecision("1.001").quantize(2, RoundingMode.ROUND_CEIL).toString(); // "1.01"
new BigPrecision("1.009").quantize(2, RoundingMode.ROUND_FLOOR).toString();// "1.00"
Enter fullscreen mode Exit fullscreen mode
Mode Rule
ROUND_UP Away from zero
ROUND_DOWN Toward zero (truncate)
ROUND_CEIL Toward +∞
ROUND_FLOOR Toward −∞
ROUND_HALF_UP Standard rounding (default)
ROUND_HALF_DOWN Ties toward zero
ROUND_HALF_EVEN Banker's rounding

Math Functions at Arbitrary Precision

BigPrecision.setPrecision(30);

// Square root
new BigPrecision("2").sqrt().toString();
// "1.41421356237309504880168872420"

// Natural log
new BigPrecision("1").exp().ln().toString(); // "1"

// Log base 10
new BigPrecision("1000").log10().toString(); // "3"

// Exponential
new BigPrecision("1").exp().toString();
// "2.71828182845904523536..."
Enter fullscreen mode Exit fullscreen mode

The Full API

const a = new BigPrecision("3.14159");

// Arithmetic
a.add("1");    a.sub("1");    a.mul("2");    a.div("2");
a.mod("1");    a.pow("2");    a.abs();       a.neg();
a.reciprocal();

// Math
a.sqrt();  a.cbrt();  a.ln();  a.log10();  a.log2();  a.log(3);  a.exp();

// Rounding
a.quantize(2);      // round to 2 d.p.
a.toPrecision(4);   // 4 significant digits
a.ceil();  a.floor();  a.trunc();

// Comparison
a.gt("3");   a.lt("4");   a.equals("3.14159");
a.compareTo("3"); // 1

// Output
a.toString();           // "3.14159"
a.toFixed(2);           // "3.14"
a.toSignificantDigits(3); // "3.14"
a.toNumber();           // 3.14159  (JS float, may lose precision)

// Static
BigPrecision.max("1", "5", "3");         // BigPrecision("5")
BigPrecision.sum(["1.1", "2.2", "3.3"]); // BigPrecision("6.6")
Enter fullscreen mode Exit fullscreen mode

Part 5 — Architecture & Design Decisions

1. Pure Static Methods with No Mutation

Every method in every module is pure:

// All of these return NEW values — no mutation
const original = [1, 2, 3];
Statistics.mean(original);   // 2
// original is unchanged
Enter fullscreen mode Exit fullscreen mode

This makes them safe to use in React reducers, functional pipelines, and async code.

2. Validate at the Edge, Not Internally

Arithmetic methods call _validateNum on every input:

static add(...nums: number[]): number {
  Arithmetic._validateNums(nums);
  return nums.reduce((sum, n) => sum + n, 0);
}
Enter fullscreen mode Exit fullscreen mode

This gives you clear TypeError messages before any computation happens, rather than getting NaN silently propagating through a pipeline.

3. Subpath Exports for Tree-Shaking

Every module is a separate entry point in package.json:

"./statistics": {
  "require": "./dist/src/statistics.js",
  "types": "./dist/src/statistics.d.ts"
}
Enter fullscreen mode Exit fullscreen mode

If you only need Statistics, import it directly:

import Statistics from "numwiz/statistics";
// No arithmetic.js, no matrix.js, no fft.js loaded
Enter fullscreen mode Exit fullscreen mode

4. NDArray Uses Float64Array Internally

Numeric performance matters. NDArray stores all values in a Float64Array (not number[]) to enable:

  • Better memory locality
  • Faster iteration (V8 handles typed arrays more efficiently)
  • Easy interop with WebGL / WASM if you want to extend it

5. The Chainable Wrapper Is Thin

The numwiz() wrapper is only ~350 lines and delegates to the static modules. There is zero duplication of logic. This means bugs in Arithmetic.sqrt() are fixed in one place and the chainable .sqrt() gets the fix automatically.


Part 6 — Test Coverage

854 tests across 11 test suites. Here's the breakdown:

Test File Tests What It Covers
index.test.ts ~474 Core chainable API, all formatting, words, currency
matrix.test.ts ~100 Matrix 80+ methods, chainable instance
ndarray.test.ts ~70 NDArray creation, shape, math, broadcasting
linalg.test.ts ~35 SVD, eigenvalues, solve, decompositions
signal.test.ts ~37 FIR design, convolution, normalize, SNR
calculus.test.ts ~36 Derivatives, integrals (5 methods), root-finding
polynomial.test.ts ~38 Polynomial arithmetic, roots, fit
fft.test.ts ~28 FFT, IFFT, rfft, windowing, power spectrum
interpolation.test.ts ~24 All interpolation methods + CubicSpline
precision.test.ts 63 BigPrecision: all arithmetic, rounding modes, precision
constants.test.ts 23 Trigonometry constants: PI, E, TAU, PHI...

Every edge case is covered: division by zero, negative sqrt in strict mode, singular matrices, out-of-range interpolation, empty arrays, NaN propagation in safe mode, and more.


Part 7 — Real-World Use Cases

Financial Dashboard

import { Financial, Formatting } from "numwiz";

function loanSummary(principal: number, annualRate: number, months: number) {
  const emi = Financial.emi(principal, annualRate, months);
  const schedule = Financial.amortizationSchedule(principal, annualRate, months);
  const totalPaid = emi * months;
  const totalInterest = totalPaid - principal;

  return {
    emi: Formatting.toFixed(emi, 2),
    totalPaid: Formatting.addCommas(Math.round(totalPaid)),
    totalInterest: Formatting.addCommas(Math.round(totalInterest)),
    interestRatio: Formatting.toPercentage((totalInterest / totalPaid) * 100),
    schedule: schedule.slice(0, 3), // first 3 months
  };
}

loanSummary(500_000, 10, 60);
// { emi: "10624.40", totalPaid: "637,464", totalInterest: "137,464", interestRatio: "21.57%", ... }
Enter fullscreen mode Exit fullscreen mode

Signal Processing Pipeline

import { Signal, FFT, NDArray } from "numwiz";

function analyzeAudio(samples: number[], sampleRate: number) {
  // 1. Normalize the audio
  const normalized = Signal.normalize(samples);

  // 2. Apply Hanning window to reduce spectral leakage
  const windowed = FFT.applyWindow(normalized, FFT.hanning(normalized.length));

  // 3. Compute the spectrum
  const spectrum = FFT.rfft(windowed);
  const power = FFT.powerSpectrum(windowed);

  // 4. Get frequency bins
  const freqs = FFT.fftFreq(FFT.nextPow2(samples.length), 1 / sampleRate);

  // 5. Find dominant frequency
  const peakIndex = power.argmax();

  return {
    rms: Signal.rms(samples),
    energy: Signal.energy(samples),
    dominantFreq: freqs.toArray()[peakIndex],
  };
}
Enter fullscreen mode Exit fullscreen mode

Data Science Pipeline

import { NDArray, LinAlg, Statistics } from "numwiz";

// Standardize a dataset (z-score normalization)
function zNormalize(data: number[][]): number[][] {
  const X = NDArray.from(data);        // shape: [n_samples, n_features]
  const mu = X.mean(0);               // column means
  const sigma = X.std(0);             // column std devs
  return NDArray.divide(
    NDArray.subtract(X, mu),
    sigma
  ).toArray() as number[][];
}

// Compute covariance matrix
function covMatrix(data: number[][]): number[][] {
  const X = NDArray.from(zNormalize(data));
  const n = X.shape[0];
  const Xt = X.T;
  return NDArray.divide(Xt.dot(X), n).toArray() as number[][];
}

// Eigendecomposition for PCA
const cov = covMatrix([[2, 0, -1.4], [0, 3, 1.4], [-1.4, 1.4, 1]]);
const { values, vectors } = LinAlg.eig(cov);
// values = principal component variances
// vectors = principal component directions
Enter fullscreen mode Exit fullscreen mode

Exact Currency Arithmetic

import { BigPrecision, RoundingMode } from "numwiz";

function splitBill(total: string, people: number): string[] {
  const t = new BigPrecision(total);
  const share = t.div(people).quantize(2); // round each share to 2 d.p.
  const shares = Array(people).fill(null).map(() => share.toString());

  // Adjust last person's share for rounding residual
  const allocated = share.mul(people - 1);
  const last = t.sub(allocated).quantize(2);
  shares[shares.length - 1] = last.toString();

  return shares;
}

splitBill("100.00", 3);
// ["33.33", "33.33", "33.34"]
// Sum is exactly 100.00 ✓
Enter fullscreen mode Exit fullscreen mode

Part 8 — TypeScript Integration

NumWiz is written in TypeScript 5.x. All types are bundled — no separate @types package needed.

import numwiz, {
  Arithmetic,
  Matrix,
  NDArray,
  LinAlg,
  BigPrecision,
  RoundingMode,
  type NumWizOptions,
} from "numwiz";
import type {
  Matrix2D,
  LUResult,
  QRResult,
  AmortizationEntry,
} from "numwiz/types";

// Fully typed
const m: Matrix2D = [[1, 2], [3, 4]];
const { L, U, P }: LUResult = Matrix.lu(m);
const schedule: AmortizationEntry[] = Financial.amortizationSchedule(500000, 10, 12);

// Generic random
const item = Random.pick<string>(["apple", "banana", "cherry"]);
// item is typed as `string`
Enter fullscreen mode Exit fullscreen mode

Key exported types:

Type Description
Matrix2D number[][]
LUResult { L, U, P: Matrix2D }
QRResult { Q, R: Matrix2D }
EigenvalueResult `number \
{% raw %}AmortizationEntry { month, emi, principal, interest, balance }
WeightedItem<T> { value: T; weight: number }
RoundingMode Union of rounding mode constants

Conclusion

NumWiz started as a simple number formatting helper and grew into a full numeric computing library for JavaScript. The design philosophy throughout has been:

  1. Pure functions — no hidden state, no mutation
  2. Validate early — clear errors at the boundary, not NaN surprises
  3. Tree-shakeable — import only what you need
  4. TypeScript-first — types are part of the API, not an afterthought
  5. Comprehensive tests — 854 tests covering edge cases, not just happy paths

Whether you're building a financial dashboard, a data visualization tool, a DSP pipeline, or just need a reliable way to say "forty-two" in Bengali — NumWiz has you covered.


Links

npm install numwiz
Enter fullscreen mode Exit fullscreen mode

Have questions, found a bug, or want to contribute? Open an issue on GitHub — PRs are very welcome!

Top comments (0)