NumWiz: A Complete Number Utility Library for JavaScript & TypeScript
"What if JavaScript had a
mathmodule 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
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"
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
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";
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
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
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
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
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
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 }
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"
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]]
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
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]
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 }
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
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
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]
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
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"
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"
| 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..."
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")
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
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);
}
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"
}
If you only need Statistics, import it directly:
import Statistics from "numwiz/statistics";
// No arithmetic.js, no matrix.js, no fft.js loaded
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%", ... }
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],
};
}
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
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 ✓
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`
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:
- Pure functions — no hidden state, no mutation
-
Validate early — clear errors at the boundary, not
NaNsurprises - Tree-shakeable — import only what you need
- TypeScript-first — types are part of the API, not an afterthought
- 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: npmjs.com/package/numwiz
- 🐙 GitHub: github.com/isubroto/numwiz
- 📖 README: Full API documentation
npm install numwiz
Have questions, found a bug, or want to contribute? Open an issue on GitHub — PRs are very welcome!
Top comments (0)