parse-ingredient
Version:
Recipe ingredient parser with support for mixed numbers and vulgar fractions
375 lines (373 loc) • 9.28 kB
JavaScript
// 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