Exact money math for JavaScript — no floating-point errors, no lost cents. Zero dependencies, ~2 KB.
You cannot store money in a number:
0.1 + 0.2; // 0.30000000000000004 ❌
19.99 * 3; // 59.96999999999999 ❌This isn't a bug you can prompt your way around — it's how binary floating point
works. pennywise stores every amount as a BigInt count of minor units
(cents, pence, yen…), so arithmetic is exact, and splitting a bill
never loses or invents a cent.
import { Money } from "pennywise";
Money.of("0.1", "USD").add(Money.of("0.2", "USD")).toDecimalString(); // "0.30" ✅
Money.of("19.99", "USD").multiply(3).format("en-US"); // "$59.97" ✅
// Split $10 three ways — and get all of it back
Money.of("10.00", "USD").split(3).map(String);
// → ["3.34 USD", "3.33 USD", "3.33 USD"] (sums to exactly $10.00)- 🎯 Exact by construction.
BigIntminor units — no0.1 + 0.2surprises, ever. - 🪙 Never loses a cent.
allocate/splitdistribute every leftover unit deterministically; the parts always sum back to the original. (Proven by a fuzz test over hundreds of amounts.) - 🌍 Currency-aware. Knows minor-unit scales (USD = 2, JPY = 0, BHD = 3…) and formats with the built-in
Intl.NumberFormat— no locale data to ship. - 🧮 Real rounding modes. Banker's rounding (half-even) by default, plus half-up/half-down/up/down/ceil/floor.
- 🔒 Immutable & type-safe. Every operation returns a new
Money; full TypeScript types. - 🪶 ~2 KB gzipped, zero dependencies. Runs in Node 18+, Deno, Bun, Workers and the browser.
npm install pennywise
# or: pnpm add pennywise / yarn add pennywise / bun add pennywiseShips ESM and CommonJS:
import { Money } from "pennywise"; // ESM / TypeScript
const { Money } = require("pennywise"); // CommonJSMoney.of("19.99", "USD"); // from a decimal string (recommended — always exact)
Money.of(5, "USD"); // from a number → $5.00
Money.of("1000", "JPY"); // ¥1000 (0 decimal places, known automatically)
Money.ofMinor(1999, "USD"); // from minor units → $19.99
Money.of("1.005", "USD", { round: "half-up" }); // control rounding of excess digitsconst price = Money.of("100.00", "USD");
price.add(Money.of("8.25", "USD")); // $108.25
price.subtract(Money.of("10", "USD")); // $90.00
price.multiply(3); // $300.00
price.multiply("1.0825"); // $108.25 (e.g. tax)
sum([a, b, c]); // add a whole listThe classic problem: split $0.05 three ways. Naive code gives $0.0166…
three times and loses a cent. pennywise distributes the remainder:
Money.of("0.05", "USD").allocate([1, 1, 1]).map(String);
// → ["0.02 USD", "0.02 USD", "0.01 USD"] ✅ sums to $0.05
// Proportional splits (e.g. revenue share 70/20/10)
Money.of("999.99", "USD").allocate([70, 20, 10]);
// Equal split
Money.of("100.00", "USD").split(7); // 7 parts that sum back to $100.00a.equals(b); a.greaterThan(b); a.lessThanOrEqual(b); a.compare(b); // -1 | 0 | 1
a.isZero(); a.isNegative(); a.isPositive();Money.of("1234.5", "USD").format("en-US"); // "$1,234.50"
Money.of("1234.5", "EUR").format("de-DE"); // "1.234,50 €"
Money.of("19.99", "USD").toDecimalString(); // "19.99" (exact, no float)
const json = JSON.stringify(money); // { "amount": "1999", "currency": "USD", "scale": 2 }
Money.fromJSON(JSON.parse(json)); // back to a Money| Method | Description |
|---|---|
Money.of(amount, currency, opts?) |
From a decimal string/number. |
Money.ofMinor(units, currency, opts?) |
From minor units (cents). |
Money.fromJSON(json) |
Rebuild from toJSON output. |
.add(m) / .subtract(m) |
Exact addition/subtraction (same currency). |
.multiply(factor, opts?) |
Multiply by a count or rate, with rounding. |
.allocate(ratios) / .split(n) |
Distribute with no lost units. |
.compare(m) / .equals / .greaterThan / … |
Value comparison. |
.negate() / .absolute() |
Sign helpers. |
.isZero() / .isPositive() / .isNegative() |
Predicates. |
.toDecimalString() / .format(locale?, opts?) |
Exact string / localized string. |
.toJSON() / .toString() |
Serialize. |
sum(monies, zero?) |
Add a list. |
Options: scale (override decimal places) and round ("half-even" default,
"half-up", "half-down", "up", "down", "ceil", "floor").
pennywise |
number math |
decimal.js / big-number libs |
|
|---|---|---|---|
| Exact (no float error) | ✅ | ❌ | ✅ |
| No-lost-cent allocate | ✅ | ❌ | |
| Currency & Intl format | ✅ | ❌ | ❌ |
| Zero dependencies | ✅ | ✅ | |
| ~2 KB gzipped | ✅ | ✅ | ❌ |
Contributions are very welcome! Please read CONTRIBUTING.md and our Code of Conduct.
git clone https://github.com/didrod205/pennywise.git
cd pennywise
npm install
npm testpennywise is free and MIT-licensed, built and maintained in spare time. If it
keeps your invoices balanced and your cents accounted for, please consider
supporting it — every bit helps keep the project healthy.
- ⭐ Star this repo — the simplest, free way to help others discover it.
- 🍋 Sponsor via Lemon Squeezy — one-time or recurring support.
Sponsoring? Open an issue and we'll add your name/logo here. Thank you! 🙏
MIT © pennywise contributors