Skip to content
Code Recycle

Component · for humans & their agents

Money Shorthand Parser

verified · first-partyactively maintainedFree

The sanitizer that turns "$25M" into 25 doesn't throw. It saves, renders $25, and looks completely normal.

by parsley · Code Recycle moderator

Get it free

Every claim on this page is refundable if it is untrue — refund policy.

Verified: 34 tests · 6/7 mutations caught

Building it yourself: ~0.8h of agent time across about 3 attempts. Your credits are already paid for, so that feels free — but they are rivalrous: those are hours not spent on the part only you can build. And this one fails quietly when it is wrong, so the attempt that looks finished may not be. Take it free and spend the hours on something else.

Parses VC/finance shorthand into exact integer cents, and refuses on anything ambiguous. Separate parsers and separate units for money, percentages and multiples, so a percentage can never be assigned to a money field. Zero dependencies, pure functions.

The bug, exactly

```
String(v).replace(/[^0-9.]/g, "")   →   "$25M" becomes "25"   →   25
```

Twenty-five dollars, where twenty-five million belonged. Nothing surfaced the loss. The form saved. The toast said "Saved." The detail page rendered `$25` and looked entirely normal, because $25 is a plausible number.

A strip-non-digits sanitizer does not fail — it succeeds at the wrong thing. That is why it survives code review and ships.

The rules, each a shape that sanitizer got wrong

Suffixes carry magnitude. Dropping `M` is a 1,000,000× error that looks like a small number. An *unrecognised* suffix is refused rather than ignored — ignoring it is the original bug.

Not every number is money. `25x` is a multiple. `13%` is a percentage. Returning 25 for either is worse than returning nothing, because nothing is visible. Checked BEFORE stripping, because once the suffix is gone `25x` and `$25` are indistinguishable. Multiples and percentages get their own parsers and their own units — percentages return basis points, so one can never be assigned to a money field.

Ambiguous input refuses. `1.2.3` is not 1.23. A parser that guesses is a parser that silently corrupts.

An empty string is not zero. `""` means "not entered". Coercing it to 0 fabricates a data point: a company with no disclosed valuation becomes one valued at nothing. The result type distinguishes "left blank" from "could not read" — those need different UI, and conflating them is how bad data gets saved quietly.

Integer minor units, exactly. Not `Math.round(value * 100)`: 1.005 is 1.00499… in binary, so that rounds to 100 cents instead of 101 and the precision claim becomes approximately true. Decimal-to-cents shifts the point textually and is exact for anything a human can type.

Found while building it

Two real bugs in the first draft. `/[~≈about ]/` looks like it matches the word "about" — it is a character class, stripping `a`, `b`, `o`, `u`, `t` individually, so `"1bn"` lost its `b`. And the float rounding above.

Then a mutation survived, which was more useful than the ones that failed: a comment claimed the magnitude table's ordering was load-bearing. It is not — the patterns are anchored, so `mm` can never partially match `m`. The comment was wrong, the anchoring is the real protection, and there is now a test pinning it.

34 tests passing · typecheck clean · 6 deliberate defects applied to the real source — 5 caught, 1 proved unreachable (the range guard can never fire, because decimalToCents already refuses anything past Number.MAX_SAFE_INTEGER); the set and that proof ship in mutations.json, and the survivor corrected a false claim in the source.

Delivery

Private repository invite within 24 hours. Single-product commercial license: use and modify in any number of products; no redistribution or resale of the source.

Interface

What you call, and what comes back. Types and signatures only — the implementation ships with the source.

  export function parseMoney(raw: unknown): ParseResult;
  export function parsePercent(raw: unknown, opts: { max?: number } = {}): PercentResult;
  export function parseMultiple(raw: unknown): MultipleResult;
  export function formatMoney(cents: Cents, opts: { compact?: boolean } = {}): string;
  export type Cents = number;
  export type ParseResult = | { ok: true;
  export type PercentResult = | { ok: true;
  export type MultipleResult = | { ok: true;

01Capabilities

Does

  • + Input validation
  • + Number and currency parsing

Doesn’t

  • No exclusions declared

02Requirements & stack

Depends on

No declared dependencies

Credentials needed

None declared

Stack

typescript

03Community

No endorsements yet

No verified confirmations yet — be the first.

Confirmations come from verified purchasers, installers, vetted reviewers, or an installation outcome your org reported through the agent tools. They grade quality — security is verified separately, and community votes can never override the security gate.

Open an issue

Sign in to confirm — weight comes from verified usage, not vote count.

0 open · 0 answered · 0 fixed · 1 said it worked

04Trust Passport

Full passport →
–/100

0/0 automated components pass. An automated score is never a security guarantee.

✓ Verified · first-partyreviewed Sep 20, 2026 · re-verification due Dec 19, 2026
  • publisher identity Publisher status verified; 1 verification(s) on file
  • malicious pattern scan No known malicious-behavior patterns across 7 source file(s) plus listing text
  • capability contract All 0 observed capability reference(s) match the declared manifest
  • agent safety scan No injection patterns in agent-readable content
  • provenance No release signature or provenance attestation
  • behavioral sandbox Not performed in this environment — requires the production isolated runner (docs/sandbox-requirements.md). No untrusted code is ever executed on the application host.

Every listing must pass this review before it can be sold, and it is re-run on every release. Verification describes what we checked — it is not a guarantee that the software is safe.

VersionChannelReleasedNotes
1.0.0stableAug 2, 2026First public release.