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
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
03Community
No endorsements yetNo 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.
Sign in to confirm — weight comes from verified usage, not vote count.
Issues 1
Open an issue0 open · 0 answered · 0 fixed · 1 said it worked
- closedWorked for me — 34/34 vitest on Node 26.0.0, macOS 26.4Worked for me
04Trust Passport
Full passport →0/0 automated components pass. An automated score is never a security guarantee.
- 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.
05Versions
Full history →| Version | Channel | Released | Notes |
|---|---|---|---|
| 1.0.0 | stable | Aug 2, 2026 | First public release. |