A Recipe Scaler That Parses '1 1/2 cups' and Displays '3 ¾' — With Japanese Units
Paste a recipe as plain text, set the target servings, get a scaled version with proper fractions. The hard parts: parsing mixed fractions like "1 1/2 cups", converting decimals back to human-readable fractions ("0.25" → "¼"), and handling Japanese cooking units like 大さじ (tablespoon) and 小さじ (teaspoon) alongside English ones.
Most recipe scalers are either too rigid (they want you to fill in a form for each ingredient) or too dumb (they just multiply numbers without parsing the unit). This one takes pasted text, parses each line, and displays the scaled version with smart fraction formatting.
🔗 Live demo: https://sen.ltd/portfolio/recipe-scale/
📦 GitHub: https://github.com/sen-ltd/recipe-scale
Features:
- Parse mixed fractions, decimals, unicode fractions (½, ¼, ⅓)
- English units (cup, tbsp, tsp, oz, lb, g, kg, ml, l)
- Japanese units (大さじ, 小さじ, 杯, 個, 本, 片)
- Metric ↔ Imperial conversion toggle
- Smart fraction display (0.75 → ¾)
- Edit individual items after parsing
- Japanese / English UI
- Zero dependencies, 65 tests
Parsing mixed fractions
The parser accepts all of these:
"1 cup flour" → 1 cup, flour
"1.5 cups flour" → 1.5 cups, flour
"1 1/2 cups flour" → 1.5 cups, flour
"1½ cups flour" → 1.5 cups, flour
"200 g sugar" → 200 g, sugar
"salt to taste" → null quantity, "salt to taste"
"大さじ 2 の醤油" → 2 大さじ, 醤油
The quantity parser first tries to extract a leading number (including fraction notation):
export function parseQuantity(str) {
// Unicode fractions
const unicode = { '½': 0.5, '⅓': 1/3, '⅔': 2/3, '¼': 0.25, '¾': 0.75, '⅛': 0.125 };
// Mixed: "1 1/2"
const mixed = str.match(/^(\d+)\s+(\d+)\/(\d+)/);
if (mixed) return parseInt(mixed[1]) + parseInt(mixed[2]) / parseInt(mixed[3]);
// Decimal: "1.5"
const decimal = str.match(/^(\d+(?:\.\d+)?)/);
if (decimal) return parseFloat(decimal[1]);
// Fraction: "1/2"
const frac = str.match(/^(\d+)\/(\d+)/);
if (frac) return parseInt(frac[1]) / parseInt(frac[2]);
// Unicode: "½" or "1½"
for (const [char, value] of Object.entries(unicode)) {
if (str.startsWith(char)) return value;
const m = str.match(new RegExp(`^(\\d+)${char}`));
if (m) return parseInt(m[1]) + value;
}
return null;
}
Order matters: check mixed (1 1/2) before fraction (1/2) before integer. Otherwise "1 1/2" would parse as "1" and leave "1/2" in the rest.
Smart fraction display
Scaling produces decimals: halving 1 cup gives 0.5, tripling 1/3 cup gives 1. We want to display these as human-readable fractions:
export function formatQuantity(n) {
if (n == null || n === 0) return '';
const whole = Math.floor(n);
const frac = n - whole;
const FRACTIONS = [
{ value: 0.25, symbol: '¼' },
{ value: 0.333, symbol: '⅓' },
{ value: 0.5, symbol: '½' },
{ value: 0.667, symbol: '⅔' },
{ value: 0.75, symbol: '¾' },
];
for (const { value, symbol } of FRACTIONS) {
if (Math.abs(frac - value) < 0.01) {
return whole === 0 ? symbol : `${whole}${symbol}`;
}
}
// Fall back to decimal
return n.toString();
}
A tolerance of 0.01 catches 0.33 and 0.67 which are the float-rounding of thirds. The output feels natural: 1.5 → 1½, 0.25 → ¼, 2.75 → 2¾.
Japanese units
大さじ (ōsaji, literally "big spoon") = tablespoon = 15 ml. 小さじ (kosaji, "small spoon") = teaspoon = 5 ml. These are fundamental in Japanese recipes. The parser recognizes them as units:
const KNOWN_UNITS = [
// English
{ name: 'cup', aliases: ['cups', 'c'] },
{ name: 'tbsp', aliases: ['tablespoon', 'tablespoons', 'T'] },
{ name: 'tsp', aliases: ['teaspoon', 'teaspoons', 't'] },
// Japanese
{ name: '大さじ', aliases: ['おおさじ'] },
{ name: '小さじ', aliases: ['こさじ'] },
{ name: '個', aliases: [] },
{ name: '本', aliases: [] },
{ name: '片', aliases: [] },
];
大さじ and 小さじ convert cleanly to ml for metric/imperial toggling. 個/本/片 are "count units" (piece / stick / clove) — they don't convert, just scale numerically.
Conversion table
Standard kitchen conversions:
const TO_ML = {
'cup': 240, // US customary
'tbsp': 15,
'tsp': 5,
'大さじ': 15,
'小さじ': 5,
'ml': 1,
'l': 1000,
};
const TO_G = {
'oz': 28.35,
'lb': 453.59,
'g': 1,
'kg': 1000,
};
Conversion goes through a canonical unit (ml for volume, g for mass). 15 conversions × 15 conversions would need 225 constants; through ml/g, it's 15. Adding a new unit means adding one entry to one of the tables.
Series
This is entry #65 in my 100+ public portfolio series.
- 📦 Repo: https://github.com/sen-ltd/recipe-scale
- 🌐 Live: https://sen.ltd/portfolio/recipe-scale/
- 🏢 Company: https://sen.ltd/

Top comments (0)