@wanshi-kg/metricat
v0.2.0
Published
Parse messy human measurement strings and canonicalize them — deterministic, dimension-safe, affine-correct unit conversion.
Maintainers
Readme
metricat
Parse messy human measurement strings and canonicalize them — deterministic, dimension-safe, affine-correct unit conversion.
metricat is a TypeScript library (and CLI) that turns ¾ cup, 2 1/2 tbsp, 0,5 ml,
3/10 kg, −40 °C into structured, canonicalized measurements. It owns the unsolved half —
parsing the messy human number + unit (fractions, Unicode vulgar fractions, decimal commas, signs) —
and libraries the solved half — dimension-safe conversion with correct affine handling
(temperature) via unitmath. No LLM; every result is verifiable.
It's built to be embedded: a small, semver-tagged package (the document-outline-gen pattern) that
knowledge-graph pipelines consume to stamp measurement facts and merge equivalent quantities.
Install · Quick start · CLI · API · Extensibility · Design
Install
npm install @wanshi-kg/metricatOr straight from GitHub over git+https (no registry, no SSH key — handy in CI/sandboxes), where
dist/ is built on install via the prepare hook:
npm install github:wanshi-kg/metricat#semver:^0.2.0Pin by semver tag, never by SHA — that is the whole point of tagging from day one.
Quick start
import metricat, { parse, extractAll, convert, normalize, areEquivalent } from '@wanshi-kg/metricat';
parse('¾ cup');
// { raw: '¾ cup', value: 0.75, unit: 'cup', dimension: 'volume',
// canonical: { value: 0.000177…, unit: 'm^3' } }
parse('2 fluid ounces'); // multi-word units resolve too → { unit: 'floz', dimension: 'volume', … }
parse('350 degrees Fahrenheit');// → { unit: 'degF', dimension: 'temperature', … }
parse('hello world'); // null — no false positives in prose
extractAll('Add 2½ cups flour, ¾ tsp salt, bake at 350°F for 25 min.');
// → measurements for "2½ cups", "¾ tsp", "350°F", "25 min", each with a { start, end } span
convert(parse('0°C')!, '°F'); // value ≈ 32 (affine-correct: NOT a linear multiple)
convert(parse('3 g')!, 's'); // null (dimension-safe: incompatible)
normalize(parse('2 cups')!); // → value in m^3 (the dimension's base unit)
areEquivalent(parse('¾g')!, parse('0.00075 kg')!); // true
areEquivalent(parse('0°C')!, parse('32°F')!); // true
areEquivalent(parse('3 g')!, parse('2 s')!); // falseThe Measurement shape
interface Measurement {
raw: string; // the exact substring parsed
value: number; // numeric value in `unit`
unit: string; // resolved unit, e.g. 'g', 'cup', 'degC', 'ohm'
dimension: string; // 'mass' | 'volume' | 'temperature' | … (or base signature)
canonical: { value: number; unit: string }; // normalized to the dimension's base unit
span?: { start: number; end: number }; // source span (set by extractAll)
}API
| Function | Description |
| --- | --- |
| parse(input): Measurement \| null | One string → one measurement, or null for non-measurements. |
| extractAll(text): Measurement[] | Scan a block of text for every measurement, each with a span. |
| convert(m, toUnit): Measurement \| null | Dimension-safe conversion; null on incompatible dimensions. |
| normalize(m): Measurement | Normalize to the dimension's canonical base unit. |
| areEquivalent(a, b): boolean | Same physical quantity? The merge/dedup hook. |
| createMetricat(options): Metricat | A bound instance with custom units/aliases. |
CLI
Installing the package provides a metricat binary. Point it at a file, or pipe text through it:
metricat recipe.txt # extract all measurements → JSON array
cat notes.md | metricat --ndjson # one JSON object per line (pipe-friendly)
echo "350°F" | metricat --parse # parse the input as a single measurement
cat data.txt | metricat --ndjson | jq -r '.unit'| Flag | Effect |
| --- | --- |
| -n, --ndjson | Emit one JSON object per line. |
| -c, --compact | Compact JSON (no indentation). |
| --parse | Treat the whole input as one measurement; exit 1 if it isn't one. |
| -h, --help | Show help. |
| -V, --version | Print the version. |
With no file argument it reads stdin (pass - to force it). Default output is the extractAll
result as a pretty JSON array; each item carries a span locator into the source text.
Extensibility
One package, many consumers. Register domain units against the same package without touching the shared default registry — this is how a downstream electronics knowledge base adds the units it needs that aren't built in:
import { createMetricat } from '@wanshi-kg/metricat';
const m = createMetricat({
unitDefinitions: { smoot: { value: '1.7018 m' } }, // unitmath definition syntax
aliases: { sqm: 'm^2' }, // extra token normalizations
});
m.parse('1 smoot'); // dimension: 'length'
m.parse('5 sqm'); // dimension: 'area'(unitmath already knows the common electronics set — V, A, ohm/Ω, F, H, W, Hz — out
of the box; createMetricat is for what it doesn't.)
Design
- Two layers. Parse (a salvaged + rebuilt Peggy grammar) produces
(value, unitToken); the units layer (unitmath) resolves, converts, and compares. The grammar never does unit math; the library never parses messy strings. - Affine-correct.
0 °Ccanonicalizes to273.15 K, not0 K. This package exists partly to not repeat the legacy extension's linear temperature bug — there is a standing regression test. - Deterministic & safe-by-default. No network, no LLM.
parsereturnsnullandextractAllreturns[]rather than guessing.
Versioning
Published to npm and semver-tagged from day one. npm consumers use @wanshi-kg/metricat@^0.2; git consumers pin
#semver:^0.2.0 and get patch/minor updates without re-pinning a commit. The API graduates to
v1.0.0 once stable. Releases ship via CI on a published GitHub Release (see .github/workflows/publish.yml).
License
MIT © Alex Sabaka
