UNPKG

parse-ingredient

Version:

Recipe ingredient parser with support for mixed numbers and vulgar fractions

375 lines (373 loc) 9.28 kB
// src/parseIngredient.ts import { numericQuantity } from "numeric-quantity"; // src/constants.ts import { numericRegex } from "numeric-quantity"; var defaultOptions = { additionalUOMs: {}, allowLeadingOf: false, normalizeUOM: false, ignoreUOMs: [] }; var fors = ["For"]; var forsRegEx = new RegExp(`^(?:${fors.join("|")})\\s`, "i"); var rangeSeparatorWords = ["or", "to"]; var rangeSeparatorRegExSource = `(-|\u2013|\u2014|(?:${rangeSeparatorWords.join("|")})\\s)`; var rangeSeparatorRegEx = new RegExp(`^${rangeSeparatorRegExSource}`, "i"); var firstWordRegEx = /^(fl(?:uid)?(?:\s+|-)(?:oz|ounces?)|\w+[-.]?)(.+)?/; var numericRegexAnywhere = numericRegex.source.replace(/^\^/, "").replace(/\$$/, ""); var trailingQuantityRegEx = new RegExp( `(,|:|-|\u2013|\u2014|x|\u2A2F)?\\s*((${numericRegexAnywhere})\\s*(${rangeSeparatorRegExSource}))?\\s*(${numericRegexAnywhere})\\s*(fl(?:uid)?(?:\\s+|-)(?:oz|ounces?)|\\w+)?$`, "i" ); var ofs = ["of"]; var ofRegEx = new RegExp(`^(?:${ofs.join("|")})\\s+`, "i"); var froms = ["from", "of"]; var fromRegEx = new RegExp(`\\s+(?:${froms.join("|")})$`, "i"); var unitsOfMeasure = { bag: { short: "bag", plural: "bags", alternates: [] }, box: { short: "box", plural: "boxes", alternates: [] }, bunch: { short: "bunch", plural: "bunches", alternates: [] }, can: { short: "can", plural: "cans", alternates: [] }, carton: { short: "carton", plural: "cartons", alternates: [] }, centimeter: { short: "cm", plural: "centimeters", alternates: ["cm."] }, clove: { short: "clove", plural: "cloves", alternates: [] }, container: { short: "container", plural: "containers", alternates: [] }, cup: { short: "c", plural: "cups", alternates: ["c.", "C"] }, dash: { short: "dash", plural: "dashes", alternates: [] }, drop: { short: "drop", plural: "drops", alternates: [] }, ear: { short: "ear", plural: "ears", alternates: [] }, "fluid ounce": { short: "fl oz", plural: "fluid ounces", alternates: [ "fluidounce", "floz", "fl-oz", "fluid-ounce", "fluid-ounces", "fluidounces", "fl ounce", "fl ounces", "fl-ounce", "fl-ounces", "fluid oz", "fluid-oz" ] }, foot: { short: "ft", plural: "feet", alternates: ["ft."] }, gallon: { short: "gal", plural: "gallons", alternates: ["gal."] }, gram: { short: "g", plural: "grams", alternates: ["g."] }, head: { short: "head", plural: "heads", alternates: [] }, inch: { short: "in", plural: "inches", alternates: ["in."] }, kilogram: { short: "kg", plural: "kilograms", alternates: ["kg."] }, large: { short: "lg", plural: "large", alternates: ["lg", "lg."] }, liter: { short: "l", plural: "liters", alternates: ["l."] }, medium: { short: "md", plural: "medium", alternates: ["med", "med.", "md."] }, meter: { short: "m", plural: "meters", alternates: ["m."] }, milligram: { short: "mg", plural: "milligrams", alternates: ["mg."] }, milliliter: { short: "ml", plural: "milliliters", alternates: ["mL", "ml.", "mL."] }, millimeter: { short: "mm", plural: "millimeters", alternates: ["mm."] }, ounce: { short: "oz", plural: "ounces", alternates: ["oz."] }, pack: { short: "pack", plural: "packs", alternates: [] }, package: { short: "pkg", plural: "packages", alternates: ["pkg.", "pkgs", "pkgs."] }, piece: { short: "piece", plural: "pieces", alternates: ["pc", "pc.", "pcs", "pcs."] }, pinch: { short: "pinch", plural: "pinches", alternates: [] }, pint: { short: "pt", plural: "pints", alternates: ["pt."] }, pound: { short: "lb", plural: "pounds", alternates: ["lb.", "lbs", "lbs."] }, quart: { short: "qt", plural: "quarts", alternates: ["qt.", "qts", "qts."] }, small: { short: "sm", plural: "small", alternates: ["sm."] }, sprig: { short: "sprig", plural: "sprigs", alternates: [] }, stick: { short: "stick", plural: "sticks", alternates: [] }, tablespoon: { short: "tbsp", plural: "tablespoons", alternates: ["tbsp.", "T", "Tbsp.", "Tbsp"] }, teaspoon: { short: "tsp", plural: "teaspoons", alternates: ["tsp.", "t"] }, yard: { short: "yd", plural: "yards", alternates: ["yd.", "yds."] } }; // src/parseIngredient.ts var newLineRegExp = /\r?\n/; var addIdToUomDefinition = ([uom, def]) => ({ id: uom, ...def }); var parseIngredient = (ingredientText, options = defaultOptions) => { const opts = { ...defaultOptions, ...options }; const mergedUOMs = { ...unitsOfMeasure, ...opts.additionalUOMs }; const uomArray = Object.entries(mergedUOMs).map(addIdToUomDefinition); const uomArrayLength = uomArray.length; const ingredientArray = ingredientText.split(newLineRegExp).map((line) => line.trim()).filter(Boolean); return ingredientArray.map((line) => { const oIng = { quantity: null, quantity2: null, unitOfMeasureID: null, unitOfMeasure: null, description: "", isGroupHeader: false }; if (isNaN(numericQuantity(line[0]))) { const trailingQtyResult = trailingQuantityRegEx.exec(line); if (trailingQtyResult && opts.ignoreUOMs.includes(trailingQtyResult.at(-1) ?? "")) { oIng.description = line; } else if (trailingQtyResult) { oIng.description = line.replace(trailingQuantityRegEx, "").trim(); const firstQty = trailingQtyResult[3]; const secondQty = trailingQtyResult[12]; if (!firstQty) { oIng.quantity = numericQuantity(secondQty); } else { oIng.quantity = numericQuantity(firstQty); oIng.quantity2 = numericQuantity(secondQty); } const uomRaw = trailingQtyResult.at(-1); if (uomRaw) { let uom = ""; let uomID = ""; let i = -1; while (++i < uomArrayLength && !uom) { const { alternates, id, short, plural } = uomArray[i]; const versions = [...alternates, id, short, plural]; if (versions.includes(uomRaw)) { uom = uomRaw; uomID = id; } } if (uom) { oIng.unitOfMeasureID = uomID; oIng.unitOfMeasure = opts.normalizeUOM ? uomID : uom; } else if (oIng.description.match(fromRegEx)) { oIng.description += ` ${uomRaw}`; } } } else { oIng.description = line; if (oIng.description.endsWith(":") || forsRegEx.test(oIng.description)) { oIng.isGroupHeader = true; } } } else { let lenNum = 6; let nqResult = NaN; while (lenNum > 0 && isNaN(nqResult)) { nqResult = numericQuantity(line.substring(0, lenNum).trim()); if (nqResult > -1) { oIng.quantity = nqResult; oIng.description = line.substring(lenNum).trim(); } lenNum--; } } const q2reMatch = rangeSeparatorRegEx.exec(oIng.description); if (q2reMatch) { const q2reMatchLen = q2reMatch[1].length; const nqResultFirstChar = numericQuantity(oIng.description.substring(q2reMatchLen).trim()[0]); if (!isNaN(nqResultFirstChar)) { let lenNum = 7; let nqResult = NaN; while (--lenNum > 0 && isNaN(nqResult)) { nqResult = numericQuantity(oIng.description.substring(q2reMatchLen, lenNum)); if (!isNaN(nqResult)) { oIng.quantity2 = nqResult; oIng.description = oIng.description.substring(lenNum).trim(); } } } } const firstWordREMatches = firstWordRegEx.exec(oIng.description); if (firstWordREMatches) { const firstWord = firstWordREMatches[1].replace(/\s+/g, " "); const remainingDesc = (firstWordREMatches[2] ?? "").trim(); if (remainingDesc) { let uom = ""; let uomID = ""; let i = -1; while (++i < uomArrayLength && !uom) { const { alternates, id, short, plural } = uomArray[i]; const versions = [...alternates, id, short, plural].filter( (unit) => !opts.ignoreUOMs.includes(unit) ); if (versions.includes(firstWord)) { uom = firstWord; uomID = id; } } if (uom) { oIng.unitOfMeasureID = uomID; oIng.unitOfMeasure = opts.normalizeUOM ? uomID : uom; oIng.description = remainingDesc; } } } if (!opts.allowLeadingOf && oIng.description.match(ofRegEx)) { oIng.description = oIng.description.replace(ofRegEx, ""); } return oIng; }); }; export { defaultOptions, firstWordRegEx, fors, forsRegEx, fromRegEx, froms, ofRegEx, ofs, parseIngredient, rangeSeparatorRegEx, rangeSeparatorWords, trailingQuantityRegEx, unitsOfMeasure }; //# sourceMappingURL=parse-ingredient.mjs.map