Useful? A star is how other developers find it — ★ GitHub · letools.dev/tools/numbers-le
Somebody has to verify that the rate in the code is the rate in the specification. A tax percentage, a retention window, a rounding boundary, a retry limit. In a regulated setting that check is a deliverable, and the person doing it is an auditor or an actuary or a compliance reviewer — usually without a checkout, always without the editor open.
A magic-number linter does not serve them. It tells a developer to hoist a literal into a constant; it never hands anyone the list of every literal with its file and its line.
Sixty seconds
|
# the point of the whole thing:
|
./config.json:2:11 8080
./rates.env:1:5 0.2
./src/pricing.ts:1:17 1.15
3 numbers in 3 files
Exit codes follow grep — 0 numbers found, 1 none found, 2 the
question was malformed. Finding none is an answer, not an error.
Install
| Route | Command | Worth knowing |
|---|---|---|
| cargo | cargo install numbers-le |
Any platform, needs Rust 1.88+. |
| From source | git clone https://github.com/nolindnaidoo/numbers-lecd numbers-le/crate && cargo build --release |
The same build CI runs. |
No runtime, no network, nothing written.
How a number is printed is the contract
JavaScript numbers are IEEE-754 doubles and so are Rust's, so the values agree by construction. The strings do not.
| value | JavaScript | Rust's default |
|---|---|---|
1e21 |
1e+21 |
1000000000000000000000 |
1e-7 |
1e-7 |
0.0000001 |
-0 |
0 |
-0 |
This tool's whole output is numbers rendered as text, so it implements
ECMAScript's Number::toString rather than reaching for Rust's: shortest
round-trip digits, decimal notation while 1e-6 ≤ |x| < 1e21,
exponential with an explicit sign outside it. Both boundaries are pinned
by the corpus the extension also builds against.
That is also why value is a string in the JSON report. Re-encoding
through a JSON number would hand you whatever your parser prints, which
is the one thing this exists to control.
What counts as a number
One policy, shared by every format:
- Only finite numbers.
NaNand±Infinityare rejected even where a format can express them — YAML.inf, TOMLnan. An extractedInfinityis noise to everything downstream. - Coercion is per format. INI,
.envand CSV values are inherently text, soPORT=8080is the number 8080. JSON, YAML and TOML tell42from"42", and a quoted number there is data — a version pinned as a string, an id that must not lose its leading zero. - A coerced string must be numeric in full.
12abc,1.2.3,0x1Aand1_000are rejected.parseFloatread the first two as12and1.2, and a version string quietly becoming a number is the kind of wrong an audit cannot see. - Dates are not numbers, so a TOML datetime stays out.
The parsers decide more than the policy does. 0x1A is rejected as a
coerced string in INI and accepted as 26 in YAML and TOML, because
those parsers resolve it before the policy ever sees it. Both frontends
inherit that, and the corpus pins it.
The text scan has no grammar
For a format nothing here parses — a .ts, a .sql, a .log — numbers
come from scanning the raw text. v1.2.3 reads as 1.2 and 0.3.
That is not a defect to report; it is what the extension does, and a scan with no parser cannot know a version string is one token. It is why the scan is only used when the format is unknown — and why it is still worth having, because a hardcoded constant in a source file is exactly what an audit is looking for.
Positions, and where they stop
Each number is reported with its file and, where it can be found, a 1-based line and column in UTF-16 units — the number your editor shows.
Finding it is harder than for text, because a number's source and its
printed form are often different: 0x1A is reported as 26, +7 as
7, 1e21 as 1e+21. So the search is by value, not by text — scan
the document for numeric runs, and pair each number with the next run
that parses to the same double.
Two consequences, both honest:
- A run the scanner cannot see — a hex literal, an underscored literal —
has no offset to give. That number reports no position and
summary.unlocatedcounts them. - A run in a key can take the match. In
k26 = 0x1Athe extracted26finds the digits in the key. The number is right; the position is a best effort, and it is forward-only so it can never point above a number already reported.
JSON and the text scan skip all of that: one walks an AST with real ranges, the other is the scanner.
It has no opinions
No magic-number heuristic. No range check. No "this looks like a rate" guess. No arithmetic, and nothing is ever rewritten.
Which numbers matter is the reviewer's call, and a tool that pre-filtered would decide the audit before the auditor saw it. A contract test asserts no flag asks for a judgment.
Options
--dedupe collapse repeated values to their first occurrence
--format <format> force a format instead of inferring from the name;
an unknown name falls back to a text scan
--values print only the numbers, one per line, for piping
--stdin read one document from stdin
--hidden walk hidden files and directories too
--no-ignore walk files that .gitignore excludes
As an MCP server
Two tools, both returning { ok, data, diagnostics, meta }:
extract_numbers— content in, numbers out, no positions. Touches no filesystem. The npm server ships the same tool with byte-identical output, tokens included; one corpus runs against both.numbers_le_scan— files or directories in, the same reports the CLI writes, positions included.
The other four ways to run it
| Where | What you get | Install |
|---|---|---|
| VS Code | The same extraction, in your editor, on a keystroke | Marketplace |
| Cursor, VSCodium, Windsurf | The same extension | Open VSX |
| Any MCP agent, via Node | extract_numbers over stdio |
npx numbers-le-mcp · npm |
| Zed | The MCP server as a context server | add it by hand (no listing yet) |
All ten LE tools are on letools.dev.
Also by nolindnaidoo
Rust — pixelcoords and pixelactions are one loop: pixelcoords answers where, pixelactions acts there. The seven LE crates are the terminal half of the extensions they sit in — the same extraction, held to the extension's own corpus, and an exit code instead of a results editor.
- pixelcoords — Freeze your screen, mark regions, get pixel-exact coordinates and crops pixelcoords.dev · crates.io · docs.rs
- pixelactions — Consume human-verified coordinates, perform the interaction, confirm it landed pixelactions.dev · crates.io · docs.rs
- paths-le — Find every path in a codebase and report whether it still points at anything crates.io
- secrets-le — Find hardcoded credentials, and never print one crates.io
- urls-le — Extract every URL from a codebase, with its protocol and exact position crates.io
- regex-le — Find every regex in a codebase and report which can be driven into catastrophic backtracking crates.io
- string-le — Get every string in a codebase out where a person can read them crates.io
- envsync-le — Compare the dotenv files in a tree and say which keys are missing from which crates.io
- colors-le — Find every colour in a codebase, and say which are not in your palette crates.io
- scrape-le — Check whether a page is scrapeable before the scraper is written crates.io
Contact Developer — nolindnaidoo.com · GitHub · LinkedIn
License
MIT — see LICENSE.