DEV Community

SEN LLC
SEN LLC

Posted on

A Recipe Scaler That Parses '1 1/2 cups' and Displays '3 ' — With Japanese Units

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

Screenshot

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 大さじ, 醤油
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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: [] },
];
Enter fullscreen mode Exit fullscreen mode

大さじ 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,
};
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)