Every unit converter has a smart input sooner or later, and every smart input has the same quiet problem: people do not type like a form. They type "7 lb 4 oz", "7lb4oz", "1/2 lb", "200g", "1.5 kg", and occasionally "7 lbs 4 ozs" — and they expect the box to understand all of it. The parsing looks trivial until you count the ways a weight can be written, and then it becomes a small language with a tokenizer, a unit table, and a display contract.
I built the weight converter at ToolkitLoop, and its smart input reads compound values like "7 lb 4 oz to kg" directly. Here is how that kind of parser works, where it breaks, and the design decision at the end that I still go back and forth on.
The input is a little language
Start with what the parser actually has to accept. A single value with a unit: "200g", "1.5 kg", "7 lb". A compound value: "7 lb 4 oz", "11 st 4 lb". A fraction: "1/2 lb". Then the noise: extra spaces, missing spaces, plural units, mixed case ("7 LB 4 OZ"), and the occasional comma decimal ("1,5 kg") from European keyboards.
The first decision is to normalize before parsing anything: lowercase the input, collapse repeated whitespace, and treat the string as a sequence of number-unit pairs. A single regex does most of the work:
const PAIR = /(\d+(?:\.\d+)?|\d+\/\d+)\s*([a-z]+)/g;
That matches "7 lb", "4 oz", "1/2 lb", and "200g" alike, because the space between number and unit is optional. What it does not do is validate — validation is a separate step, and keeping the two apart is what makes the parser debuggable.
The unit table is the source of truth
The second decision is to keep every conversion factor in one table, in grams, and never convert between two non-gram units directly. Grams are the pivot: every unit converts to grams, and grams convert to the display unit. The definitions are exact, which matters more than it sounds — the only rounding in the whole system should happen at display time, never in the middle.
const GRAMS = {
g: 1,
kg: 1000,
oz: 28.349523125,
lb: 453.59237,
st: 6350.29318, // 14 lb, exactly
};
const ALIASES = {
g: 'g', gram: 'g', grams: 'g',
kg: 'kg', kilogram: 'kg', kilograms: 'kg',
oz: 'oz', ounce: 'oz', ounces: 'oz',
lb: 'lb', lbs: 'lb', pound: 'lb', pounds: 'lb',
st: 'st', stone: 'st', stones: 'st',
};
The alias table is where plural handling lives, and it is deliberately boring: a flat map from every accepted spelling to one canonical key. Adding "lbs" or "ounces" is a one-line change, and there is no clever stemming to go wrong. The stone entry is worth noting — 1 stone is exactly 14 pounds, so 6350.29318 grams, and the tool page states it as about 6.35029 kilograms, which is the same number rounded for display.
Summing compounds
With pairs and a pivot table, compound input is a loop: parse each pair, convert to grams, add. "7 lb 4 oz" becomes 7 × 453.59237 + 4 × 28.349523125, which is 3288.54 grams, or about 3.29 kilograms. "11 st 4 lb" becomes 11 × 6350.29318 + 4 × 453.59237, which is 71,667.6 grams, or about 71.67 kilograms — the same figure the tool page quotes for 11 stone 4 pounds.
function parseWeight(input) {
const text = input.toLowerCase().replace(/\s+/g, ' ').trim();
const pairs = [...text.matchAll(PAIR)];
if (pairs.length === 0) return null;
let grams = 0;
for (const [, amount, unit] of pairs) {
const key = ALIASES[unit];
if (!key) return null; // unknown unit: reject the whole input
const value = amount.includes('/')
? amount.split('/').reduce((a, b) => a / b)
: parseFloat(amount);
grams += value * GRAMS[key];
}
return grams || null;
}
Two details in that function deserve attention. First, an unknown unit rejects the whole input instead of silently dropping the pair: "7 lb 4 parsecs" should be an error, not 7 pounds. Silent partial parsing is how converters produce confidently wrong answers. Second, fractions are evaluated before conversion — "1/2 lb" is 226.8 grams, not a parse error — because recipe quantities are full of halves and quarters.
The traps: ambiguity and display
The parser has three genuine traps, and all three are design decisions rather than bugs.
The first is ambiguity between units that share a shape. "5'8"" is a length, not a weight, and a weight parser that sees feet and inches should refuse rather than guess. The same applies to bare numbers: "7" with no unit is not seven of anything. A converter that guesses the unit is a converter that will eventually guess wrong in front of a user who trusted it.
The second is the comma decimal. "1,5 kg" is 1.5 kilograms to much of Europe and a thousand-separator typo to everyone else. Accepting it means deciding which convention wins, and the safe answer for a weight input is to accept the comma only when there is exactly one comma and no period in the number. It is a small rule, but it is the difference between a parser that works for a German baker and one that silently turns their 1.5 kilograms into 15.
The third trap is the display contract, and it is the one that matters most: store full precision, round only for display. The tool page says 1 kg is about 2.20462 pounds — that "about" is doing real work. The stored value is 2.2046226218, and the display shows five decimal places. If you round at parse time, then convert back and forth between units, the rounding compounds: kilograms to pounds to kilograms drifts a little further every round trip. Keep the grams value exact, format at the last moment, and the drift disappears. This is the same rule behind the rounding discussion in the Day A piece — display rounding versus storage rounding — but from the parser's side: the parser's job is to never destroy information the display might need later.
The open question
That leaves one decision I have not settled, and it is the one I would genuinely like to hear other developers argue about: should the parser accept spaceless compound input like "7lb4oz"?
The regex above already accepts it — the space between number and unit is optional, so "7lb4oz" tokenizes into two pairs without any special handling. The question is whether accepting it is good design or over-fitting. On one side, people type fast and omit spaces constantly, and a parser that handles it feels magical. On the other side, every input you accept is an input you have to explain when it fails, and "7lb4oz" failing in a future version would break someone's muscle memory. The forgiving parser wins users; the strict parser wins debuggability.
My current answer is to accept it, because the cost of accepting is one regex and the cost of rejecting is a confused user staring at a red error for input that is obviously a weight. But I keep the acceptance narrow — only the exact aliases in the table, no fuzzy matching — so the parser stays predictable. What would you do: accept spaceless compounds, or draw the line at a space?
Disclosure: I built the ToolkitLoop weight converter, and the parser above is a simplified version of how its smart input works — the real one handles the same compound values, including "7 lb 4 oz", with the same grams-pivot table. The tool itself is free, needs no sign-up, and runs entirely in your browser: https://toolkitloop.com/weight-converter
About the author
- Founder of ToolkitLoop (toolkitloop.com) — 30 free online tools that run 100% in your browser. No sign-ups, no uploads, private by design.
Comments
Join the discussionNo comments yet. Be the first to comment.
