Skip to content

didrod205/pennywise

pennywise

Exact money math for JavaScript — no floating-point errors, no lost cents. Zero dependencies, ~2 KB.

npm version bundle size CI types license

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)

Why pennywise?

  • 🎯 Exact by construction. BigInt minor units — no 0.1 + 0.2 surprises, ever.
  • 🪙 Never loses a cent. allocate/split distribute 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.

Install

npm install pennywise
# or: pnpm add pennywise  /  yarn add pennywise  /  bun add pennywise

Ships ESM and CommonJS:

import { Money } from "pennywise";        // ESM / TypeScript
const { Money } = require("pennywise");   // CommonJS

Usage

Creating money

Money.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 digits

Arithmetic

const 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 list

Splitting without losing money

The 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.00

Comparing

a.equals(b);  a.greaterThan(b);  a.lessThanOrEqual(b);  a.compare(b); // -1 | 0 | 1
a.isZero();   a.isNegative();    a.isPositive();

Formatting & serialization

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

API

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").

Comparison

pennywise number math decimal.js / big-number libs
Exact (no float error)
No-lost-cent allocate ⚠️ (DIY)
Currency & Intl format
Zero dependencies ⚠️
~2 KB gzipped

Contributing

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 test

💖 Sponsor

pennywise 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! 🙏

License

MIT © pennywise contributors

About

Exact, zero-dependency money math — BigInt minor units mean no floating-point errors; allocate/split never lose a cent. ~2KB.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors