import { parseQuantity } from "./parse-quantity"; const KNOWN_UNITS = new Set([ "cup", "cups", "c", "tablespoon", "tablespoons", "tbsp", "tbsps", "tbs", "teaspoon", "teaspoons", "tsp", "tsps", "gram", "grams", "g", "kilogram", "kilograms", "kg", "ounce", "ounces", "oz", "pound", "pounds", "lb", "lbs", "milliliter", "milliliters", "millilitre", "millilitres", "ml", "liter", "liters", "litre", "litres", "l", "pinch", "pinches", "dash", "dashes", "clove", "cloves", "slice", "slices", "piece", "pieces", "pcs", "pc", "can", "cans", "jar", "jars", "package", "packages", "pkg", "bunch", "bunches", "sprig", "sprigs", "stick", "sticks", "quart", "quarts", "qt", "pint", "pints", "pt", "fl", // matches the first token of "fl oz" — handled below // French "tasse", "tasses", "cas", "cac", "gramme", "grammes", "kilogramme", "kilogrammes", "litre", "litres", "pincée", "pincees", "tranche", "tranches", "gousse", "gousses", "sachet", "sachets", "boîte", "boite", "boîtes", "boites", "morceau", "morceaux", "pièce", "pièces", ]); // Leading numeric token: plain int/decimal, unicode fraction, mixed number ("1 1/2"), or // simple fraction ("1/2") — mirrors what parseQuantity already knows how to parse. const LEADING_NUMBER = /^\s*(\d+\s+\d+\s*\/\s*\d+|\d+\s*\/\s*\d+|\d+(?:\.\d+)?|[¼½¾⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞])\s*/; /** * Some AI-generated (or pasted/imported) ingredients arrive with the quantity baked into * the name itself (e.g. rawName: "2 cups flour"), sometimes ALONGSIDE an already-populated * quantity/unit pair (e.g. rawName: "1 avocat", quantity: "2", unit: "pcs" — the redundant * "1" was never stripped because an earlier version of this function only looked at * rawName when quantity/unit were both empty). This always strips a leading * quantity(+unit) token from the NAME when one is present, so the displayed ingredient is * just "avocat", never "1 avocat" — but only ever backfills the quantity/unit fields * themselves when they weren't already provided, so an existing explicit quantity/unit * (e.g. from summing across recipes) is never overwritten by whatever number happened to * be sitting in the name. */ export function extractIngredientQuantity( rawName: string, quantity: string | undefined, unit: string | undefined ): { rawName: string; quantity: string | undefined; unit: string | undefined } { const match = rawName.match(LEADING_NUMBER); if (!match) return { rawName, quantity, unit }; const numberToken = match[1]!.trim(); const parsedQuantity = parseQuantity(numberToken); if (parsedQuantity === undefined) return { rawName, quantity, unit }; let rest = rawName.slice(match[0].length).trim(); if (!rest) return { rawName, quantity, unit }; // nothing left — not actually a name+quantity string let strippedUnit: string | undefined; const unitMatch = rest.match(/^([a-zA-Z.]+)\s+(.*)$/); if (unitMatch) { const candidate = unitMatch[1]!.replace(/\.$/, "").toLowerCase(); if (candidate === "fl" && unitMatch[2]!.toLowerCase().startsWith("oz")) { strippedUnit = "fl oz"; rest = unitMatch[2]!.replace(/^oz\.?\s*/i, "").trim(); } else if (KNOWN_UNITS.has(candidate)) { strippedUnit = candidate; rest = unitMatch[2]!.trim(); } } if (!rest) return { rawName, quantity, unit }; // stripping the unit ate the whole string — bail out const hasExplicitQuantity = quantity !== undefined && quantity !== ""; return { rawName: rest, quantity: hasExplicitQuantity ? quantity : parsedQuantity, unit: unit !== undefined && unit !== "" ? unit : strippedUnit, }; }