Skip to content

kits/finance: high-precision variants via decimal.js (peerDependency, tree-shake-isolated) #41

Description

@the-simian

Why

JavaScript's IEEE-754 Float64 arithmetic is fine for most of what unitforge does (every kit factor is defined as an exact rational; single-step conversions are accurate to ~16 sig figs). It is not fine for finance: currency arithmetic with 0.1 + 0.2 = 0.30000000000000004 is the canonical example; settlement amounts, accrued interest, mortgage payments, and regulatory accounting all require decimal-exact arithmetic. Java's BigDecimal and Python's decimal module are the reference standards.

This kit gives unitforge a credible answer to the "neat library, but not for serious math" critique that lands on Hacker News every other units-library post, by demonstrating decimal precision in a concrete domain, not by hand-waving.

What ships

Atoms

  • Currency units (USD cents / USD dollars / EUR cents / EUR euros / GBP pence / GBP pounds / JPY yen).
  • Interest-rate dimensions (annual / monthly / daily; APR vs APY).
  • Loan / mortgage / amortization atoms.
  • Common business-arithmetic helpers (compounding, present value, future value, settlement-day-count fractions).

High-precision / low-precision split

Within kits/finance, two parallel surfaces:

  • Low-precision (default; Float64): fast, zero-dep, fine for UI estimates and over-the-counter change calculations. Bundle cost: same as any kit (a few hundred bytes per import).
  • High-precision (opt-in; decimal.js-backed): exact decimal arithmetic for currency, settlement, and audit-grade interest calculations. Imports decimal.js, which becomes a peerDependency the consumer installs explicitly.

The split serves two purposes:

  1. A consumer who only needs ballpark finance math pays nothing extra at the byte level.
  2. A consumer who needs precision opts in by importing from the high-precision sub-module, which is tree-shake-isolated so decimal.js never reaches their bundle otherwise.

Tree-shake quarantine strategy

Two separate files within the kit:

  • src/kits/finance/units.ts — Float64 atoms (the low-precision surface).
  • src/kits/finance/precise.ts — decimal.js-backed atoms + a forgePrecise variant of forge.

The kit's index.ts re-exports both, but the precise atoms touch a decimal.js import that lives only in precise.ts. A consumer who imports only Float64 atoms via the wildcard subpath (or via named imports that don't touch the precise surface) never pulls decimal.js. A consumer who imports a precise atom triggers the decimal.js dependency at install / build time.

Optional refinement (decide during implementation): ship precise.ts as a separate subpath (unitforge/kits/finance/precise) so the dependency surface is even louder to readers. Trade-off: stronger isolation vs. one more entry in the package.json exports map.

forgePrecise variant

forge today is typed (from: Unit, to: Unit) => (value: number) => number. A forgePrecise variant is typed (from: PreciseUnit, to: PreciseUnit) => (value: Decimal) => Decimal. The two variants share the same defineUnit / defineConversion substrate; the difference is what the closures consume and produce.

Architectural choice point (decide during implementation): is the numeric backend a TypeScript generic on defineUnit<T> and forge<T>, or are there two parallel APIs sharing zero type machinery? The generic approach is more elegant but bigger change; the parallel-API approach lands faster.

Demo

A side-by-side calculator on the kits/finance demo screen:

  • Left: Float64 interest accrual over 30 years on a $500,000 mortgage at 6.5%, computed with the low-precision atoms.
  • Right: decimal.js interest accrual on the same loan, computed with the high-precision atoms.

Show the divergence after enough compounding cycles. Make the precision boundary visible to a reader who doesn't already know the Float64 critique.

Bonus: a "fast vs precise" toggle on the bench so the user can pick the precision level for any finance calculation they explore.

README

Once this ships, the README's Numerical Precision section can say "unitforge uses Float64 by default; for decimal-exact requirements, reach for kits/finance and decimal.js." That's the inoculation against the HN comment.

Acceptance criteria

  • kits/finance ships with both low-precision (Float64) and high-precision (decimal.js) atoms.
  • decimal.js declared as a peerDependency in package.json.
  • Tree-shake verified: a fixture importing only low-precision atoms does not include decimal.js in its bundle.
  • Demo at #/finance shows the Float64-vs-decimal divergence on a real calculation.
  • README updated with the Numerical Precision section.
  • Reviewer pairing: a working-finance / accounting expert (perhaps a future review-financial-quant reviewer) plus the architect for the API-split surface.

Out of scope

  • General BigDecimal abstraction across all kits (not needed; the precision concern is domain-specific to finance).
  • Currency exchange rate data (no live FX feed; the kit ships atoms and arithmetic, not a market data source).
  • Settlement-system integration (not a units-library responsibility).

Sequencing

Per scope decision on PR #38: this kit is two PRs down. The demos for the six new kits land first (issue #40); this kit lands after that, and the Hacker News post happens once both are in place.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions