parse-ingredient
Version:
Recipe ingredient parser with support for mixed numbers and vulgar fractions
800 lines (799 loc) • 24.1 kB
JavaScript
import { numericQuantity, numericRegex } from "numeric-quantity";
//#region src/constants.ts
/**
* Escapes special regex characters in a string.
*/
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
/**
* Builds a regex that matches any of the given patterns at the start of a string,
* followed by whitespace. Strings are escaped and treated as literal prefixes.
* RegExp patterns have their source extracted and combined.
*/
const buildPrefixPatternRegex = (patterns) => {
if (patterns.length === 0) return null;
const parts = patterns.map((p) => p instanceof RegExp ? `(?:${p.source})` : `(?:${escapeRegex(p)})\\s`);
return new RegExp(`^(?:${parts.join("|")})`, "iu");
};
/**
* Builds a regex source string for range separators (dashes and word separators).
* Always includes dash characters (-, –, —), plus any custom word separators.
*/
const buildRangeSeparatorSource = (words) => {
return `(-|–|—|(?:${words.map((w) => w instanceof RegExp ? `(?:${w.source})` : `(?:${escapeRegex(w)})`).join("|")})\\s)`;
};
/**
* Builds a regex that matches range separators at the start of a string.
*/
const buildRangeSeparatorRegex = (words) => new RegExp(`^${buildRangeSeparatorSource(words)}`, "iu");
/**
* Builds a regex that matches any of the given words or patterns at the start of a string.
* Strings are matched as whole words followed by whitespace.
* RegExp patterns are used as-is for more complex matching (e.g., French elisions).
*/
const buildStripPrefixRegex = (patterns) => {
if (patterns.length === 0) return null;
const parts = patterns.map((p) => p instanceof RegExp ? `(?:${p.source})` : `(?:${escapeRegex(p)})\\s+`);
return new RegExp(`^(?:${parts.join("|")})`, "iu");
};
/**
* Builds a regex that matches approximation/modifier prefixes at the start of
* quantity expressions (e.g., "about", "ca.", "bis zu"), followed by optional whitespace.
*/
const buildLeadingQuantityPrefixRegex = (patterns) => {
if (patterns.length === 0) return null;
const parts = patterns.map((p) => p instanceof RegExp ? `(?:${p.source})\\s*` : `(?:${escapeRegex(p)})\\s*`);
return new RegExp(`^(?:${parts.join("|")})`, "iu");
};
/**
* Builds a regex that matches any of the given words at the end of a string,
* preceded by whitespace. Used for trailing quantity context like "from" or "of".
*/
const buildTrailingContextRegex = (words) => new RegExp(`\\s+(?:${words.map(escapeRegex).join("|")})$`, "iu");
/**
* Default group header prefixes (e.g., "For the icing:").
*/
const defaultGroupHeaderPatterns = ["For"];
/**
* Default range separator words (e.g., "1 to 2", "1 or 2").
*/
const defaultRangeSeparators = ["or", "to"];
/**
* Default words to strip from the beginning of descriptions.
*/
const defaultDescriptionStripPrefixes = ["of"];
/**
* Default words that indicate trailing quantity context.
*/
const defaultTrailingQuantityContext = ["from", "of"];
/**
* Default words/patterns that are stripped from the beginning of quantity expressions.
*/
const defaultLeadingQuantityPrefixes = [];
/**
* Default options for {@link parseIngredient}.
*/
const defaultOptions = {
additionalUOMs: {},
allowLeadingOf: false,
normalizeUOM: false,
ignoreUOMs: [],
decimalSeparator: ".",
groupHeaderPatterns: defaultGroupHeaderPatterns,
rangeSeparators: defaultRangeSeparators,
descriptionStripPrefixes: defaultDescriptionStripPrefixes,
trailingQuantityContext: defaultTrailingQuantityContext,
leadingQuantityPrefixes: defaultLeadingQuantityPrefixes,
includeMeta: false,
partialUnitMatching: false
};
/**
* List of "for" equivalents.
* @deprecated Use `defaultGroupHeaderPatterns` instead.
*/
const fors = defaultGroupHeaderPatterns;
/**
* Regex to capture "for" equivalents.
* @deprecated Build dynamically using `buildPrefixPatternRegex(options.groupHeaderPatterns)`.
*/
const forsRegEx = buildPrefixPatternRegex(defaultGroupHeaderPatterns);
/**
* List of range separators.
* @deprecated Use `defaultRangeSeparators` instead.
*/
const rangeSeparatorWords = defaultRangeSeparators;
/**
* Regex to capture range separators.
* @deprecated Build dynamically using `buildRangeSeparatorRegex(options.rangeSeparators)`.
*/
const rangeSeparatorRegEx = buildRangeSeparatorRegex(defaultRangeSeparators);
/**
* Regex to capture the first word of a description to check if it's a unit of measure.
*/
const firstWordRegEx = /^(fl(?:uid)?(?:\s+|-)(?:oz|ounces?)|[\p{L}\p{N}_]+(?:[./-][\p{L}\p{N}_]+|\([\p{L}\p{N}_]+\))*[-.]?)(.+)?/iu;
const numericRegexAnywhere = numericRegex.source.replace("^", "").replace(/\$$/, "");
/**
* Builds a regex to capture trailing quantity and unit of measure,
* using the provided range separator words.
*/
const buildTrailingQuantityRegex = (rangeSeparators) => {
const rangeSeparatorSource = buildRangeSeparatorSource(rangeSeparators);
return new RegExp(`(,|:|-|–|—|x|⨯)?\\s*((${numericRegexAnywhere})\\s*(${rangeSeparatorSource}))?\\s*(${numericRegexAnywhere})\\s*(fl(?:uid)?(?:\\s+|-)(?:oz|ounces?)|[\\p{L}\\p{N}_]+(?:[./-][\\p{L}\\p{N}_]+|\\([\\p{L}\\p{N}_]+\\))*[-.]?)?$`, "iu");
};
/**
* Regex to capture trailing quantity and unit of measure.
* @deprecated Build dynamically using `buildTrailingQuantityRegex(options.rangeSeparators)`.
*/
const trailingQuantityRegEx = buildTrailingQuantityRegex(defaultRangeSeparators);
/**
* List of "of" equivalents.
* @deprecated Use `defaultDescriptionStripPrefixes` instead.
*/
const ofs = defaultDescriptionStripPrefixes;
/**
* Regex to capture "of" equivalents at the beginning of a string.
* @deprecated Build dynamically using `buildStripPrefixRegex(options.descriptionStripPrefixes)`.
*/
const ofRegEx = buildStripPrefixRegex(defaultDescriptionStripPrefixes);
/**
* List of "from" equivalents.
* @deprecated Use `defaultTrailingQuantityContext` instead.
*/
const froms = defaultTrailingQuantityContext;
/**
* Regex to capture "from" equivalents at the end of a string.
* @deprecated Build dynamically using `buildTrailingContextRegex(options.trailingQuantityContext)`.
*/
const fromRegEx = buildTrailingContextRegex(defaultTrailingQuantityContext);
/**
* Default unit of measure specifications.
*/
const unitsOfMeasure = {
bag: {
short: "bag",
plural: "bags",
alternates: [],
type: "count"
},
box: {
short: "box",
plural: "boxes",
alternates: [],
type: "count"
},
bunch: {
short: "bunch",
plural: "bunches",
alternates: [],
type: "count"
},
can: {
short: "can",
plural: "cans",
alternates: [],
type: "count"
},
carton: {
short: "carton",
plural: "cartons",
alternates: [],
type: "count"
},
clove: {
short: "clove",
plural: "cloves",
alternates: [],
type: "count"
},
container: {
short: "container",
plural: "containers",
alternates: [],
type: "count"
},
dozen: {
short: "dz",
plural: "dozen",
alternates: ["dz."],
type: "count"
},
ear: {
short: "ear",
plural: "ears",
alternates: [],
type: "count"
},
head: {
short: "head",
plural: "heads",
alternates: [],
type: "count"
},
pack: {
short: "pack",
plural: "packs",
alternates: [],
type: "count"
},
package: {
short: "pkg",
plural: "packages",
alternates: [
"pkg.",
"pkgs",
"pkgs."
],
type: "count"
},
piece: {
short: "piece",
plural: "pieces",
alternates: [
"pc",
"pc.",
"pcs",
"pcs."
],
type: "count"
},
sprig: {
short: "sprig",
plural: "sprigs",
alternates: [],
type: "count"
},
stick: {
short: "stick",
plural: "sticks",
alternates: [],
type: "count"
},
large: {
short: "lg",
plural: "large",
alternates: ["lg", "lg."],
type: "other"
},
medium: {
short: "md",
plural: "medium",
alternates: [
"med",
"med.",
"md."
],
type: "other"
},
small: {
short: "sm",
plural: "small",
alternates: ["sm."],
type: "other"
},
centimeter: {
short: "cm",
plural: "centimeters",
alternates: ["cm."],
type: "length",
conversionFactor: 10
},
foot: {
short: "ft",
plural: "feet",
alternates: ["ft."],
type: "length",
conversionFactor: 304.8
},
inch: {
short: "in",
plural: "inches",
alternates: ["in."],
type: "length",
conversionFactor: 25.4
},
meter: {
short: "m",
plural: "meters",
alternates: ["m."],
type: "length",
conversionFactor: 1e3
},
millimeter: {
short: "mm",
plural: "millimeters",
alternates: ["mm."],
type: "length",
conversionFactor: 1
},
yard: {
short: "yd",
plural: "yards",
alternates: ["yd.", "yds."],
type: "length",
conversionFactor: 914.4
},
gram: {
short: "g",
plural: "grams",
alternates: ["g."],
type: "mass",
conversionFactor: 1
},
kilogram: {
short: "kg",
plural: "kilograms",
alternates: ["kg."],
type: "mass",
conversionFactor: 1e3
},
milligram: {
short: "mg",
plural: "milligrams",
alternates: ["mg."],
type: "mass",
conversionFactor: .001
},
ounce: {
short: "oz",
plural: "ounces",
alternates: ["oz."],
type: "mass",
conversionFactor: 28.349523
},
pound: {
short: "lb",
plural: "pounds",
alternates: [
"lb.",
"lbs",
"lbs."
],
type: "mass",
conversionFactor: 453.59237
},
cup: {
short: "c",
plural: "cups",
alternates: ["c.", "C"],
type: "volume",
conversionFactor: {
us: 236.58824,
imperial: 284.13063,
metric: 250
}
},
deciliter: {
short: "dl",
plural: "deciliters",
alternates: ["dl."],
type: "volume",
conversionFactor: 100
},
"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"
],
type: "volume",
conversionFactor: {
us: 29.57353,
imperial: 28.413063
}
},
gallon: {
short: "gal",
plural: "gallons",
alternates: ["gal."],
type: "volume",
conversionFactor: {
us: 3785.4118,
imperial: 4546.09
}
},
liter: {
short: "l",
plural: "liters",
alternates: ["l."],
type: "volume",
conversionFactor: 1e3
},
milliliter: {
short: "ml",
plural: "milliliters",
alternates: [
"mL",
"ml.",
"mL."
],
type: "volume",
conversionFactor: 1
},
pint: {
short: "pt",
plural: "pints",
alternates: ["pt."],
type: "volume",
conversionFactor: {
us: 473.17647,
imperial: 568.26125
}
},
quart: {
short: "qt",
plural: "quarts",
alternates: [
"qt.",
"qts",
"qts."
],
type: "volume",
conversionFactor: {
us: 946.35295,
imperial: 1136.5225
}
},
tablespoon: {
short: "tbsp",
plural: "tablespoons",
alternates: [
"tbsp.",
"T",
"Tbsp.",
"Tbsp",
"tablespoonful"
],
type: "volume",
conversionFactor: {
us: 14.786765,
imperial: 15,
metric: 15
}
},
teaspoon: {
short: "tsp",
plural: "teaspoons",
alternates: [
"tsp.",
"t",
"teaspoonful"
],
type: "volume",
conversionFactor: {
us: 4.9289216,
imperial: 5,
metric: 5
}
},
dash: {
short: "dash",
plural: "dashes",
alternates: [],
type: "volume"
},
drop: {
short: "drop",
plural: "drops",
alternates: [],
type: "volume"
},
pinch: {
short: "pinch",
plural: "pinches",
alternates: [],
type: "volume"
}
};
//#endregion
//#region src/unitLookup.ts
/**
* Builds Maps for unit lookup. Returns both case-sensitive and case-insensitive maps.
* The case-sensitive map should be checked first to handle cases like 'T' (tablespoon)
* vs 't' (teaspoon).
*/
const buildUnitLookupMaps = (additionalUOMs = {}) => {
const caseSensitive = /* @__PURE__ */ new Map();
const caseInsensitive = /* @__PURE__ */ new Map();
const addToMaps = (id, def) => {
const versions = [
id,
def.short,
def.plural,
...def.alternates
];
for (const version of versions) {
caseSensitive.set(version, id);
caseInsensitive.set(version.toLowerCase(), id);
}
};
for (const [id, def] of Object.entries(unitsOfMeasure)) addToMaps(id, def);
for (const [id, def] of Object.entries(additionalUOMs)) addToMaps(id, def);
return {
caseSensitive,
caseInsensitive
};
};
/**
* Looks up a unit ID from the maps, trying case-sensitive first.
*/
const lookupUnit = (unit, maps) => {
var _ref, _maps$caseSensitive$g;
return (_ref = (_maps$caseSensitive$g = maps.caseSensitive.get(unit)) !== null && _maps$caseSensitive$g !== void 0 ? _maps$caseSensitive$g : maps.caseInsensitive.get(unit.toLowerCase())) !== null && _ref !== void 0 ? _ref : null;
};
/**
* Cached lookup maps for the default unitsOfMeasure (no additionalUOMs).
* Lazily initialized on first use.
*/
let defaultLookupMaps = null;
/**
* Gets the default lookup maps, creating them if needed.
*/
const getDefaultUnitLookupMaps = () => {
var _defaultLookupMaps;
return (_defaultLookupMaps = defaultLookupMaps) !== null && _defaultLookupMaps !== void 0 ? _defaultLookupMaps : defaultLookupMaps = buildUnitLookupMaps();
};
/**
* Collects all known UOM strings from the lookup maps, sorted longest-first.
*/
const collectUOMStrings = (maps) => {
const keys = [...maps.caseSensitive.keys()];
keys.sort((a, b) => b.length - a.length);
return keys;
};
//#endregion
//#region src/convertUnit.ts
/**
* Identifies a unit of measure from a string, returning the canonical unit ID.
* Matches against the unit ID, short form, plural form, and all alternates.
* Case-sensitive matches are tried first (e.g., 'T' = tablespoon, 't' = teaspoon),
* then falls back to case-insensitive matching.
*
* @returns The canonical unit ID (e.g., 'cup'), or `null` if the unit is not recognized
* or is in the `ignoreUOMs` list.
*
* @example
* ```ts
* identifyUnit('cups') // 'cup'
* identifyUnit('c') // 'cup'
* identifyUnit('T') // 'tablespoon'
* identifyUnit('t') // 'teaspoon'
* identifyUnit('tbsp') // 'tablespoon'
* identifyUnit('unknown') // null
* identifyUnit('large', { ignoreUOMs: ['large'] }) // null
* ```
*/
const identifyUnit = (unit, options = {}) => {
const { additionalUOMs = {}, ignoreUOMs = [] } = options;
if (ignoreUOMs.length > 0) {
const unitLC = unit.toLowerCase();
if (ignoreUOMs.some((ignored) => ignored.toLowerCase() === unitLC)) return null;
}
return lookupUnit(unit, Object.keys(additionalUOMs).length > 0 ? buildUnitLookupMaps(additionalUOMs) : getDefaultUnitLookupMaps());
};
/**
* Gets the conversion factor for a unit, handling both single and multi-system factors.
*/
const getConversionFactor = (factor, system) => {
var _factor$system;
return factor === void 0 ? null : typeof factor === "number" ? factor : (_factor$system = factor[system]) !== null && _factor$system !== void 0 ? _factor$system : null;
};
/**
* Converts a value from one unit to another.
*
* @returns The converted value, or `null` if conversion is not possible
* (incompatible types, missing conversion factors, or unknown units).
*
* @example
* ```ts
* convertUnit(1, 'cup', 'milliliter') // ~236.588 (US)
* convertUnit(1, 'cup', 'milliliter', { fromSystem: 'imperial' }) // ~284.131
* convertUnit(1, 'pound', 'gram') // ~453.592
* convertUnit(1, 'cup', 'gram') // null (incompatible types)
* ```
*/
const convertUnit = (value, fromUnit, toUnit, options = {}) => {
const { fromSystem = "us", toSystem = "us", additionalUOMs = {} } = options;
const normalizedFromSystem = fromSystem.toLowerCase();
const normalizedToSystem = toSystem.toLowerCase();
const mergedUOMs = {
...unitsOfMeasure,
...additionalUOMs
};
const fromUnitID = identifyUnit(fromUnit, { additionalUOMs });
const toUnitID = identifyUnit(toUnit, { additionalUOMs });
if (!fromUnitID || !toUnitID) return null;
if (fromUnitID === toUnitID && normalizedFromSystem === normalizedToSystem) return value;
const fromDef = mergedUOMs[fromUnitID];
const toDef = mergedUOMs[toUnitID];
if (!fromDef.type || !toDef.type || fromDef.type !== toDef.type) return null;
const fromFactor = getConversionFactor(fromDef.conversionFactor, normalizedFromSystem);
const toFactor = getConversionFactor(toDef.conversionFactor, normalizedToSystem);
if (fromFactor === null || toFactor === null) return null;
const result = value * fromFactor / toFactor;
return Math.round(result * 1e6) / 1e6;
};
//#endregion
//#region src/parseIngredient.ts
const newLineRegExp = /\r?\n/;
const nextWordRegExp = /^([\p{L}\p{N}_]+(?:[.-]?[\p{L}\p{N}_]+)*[-.]?)(?:\s+|$)/iu;
/**
* Repeatedly strips configured quantity prefixes from the start of a string.
*/
const stripLeadingQuantityPrefixes = (text, prefixRegex) => {
if (!text || !prefixRegex) return text;
let out = text.trimStart();
while (out) {
const match = prefixRegex.exec(out);
if (!match || !match[0]) break;
out = out.slice(match[0].length).trimStart();
}
return out;
};
/**
* Parses a string or array of strings into an array of recipe ingredient objects
*/
const parseIngredient = (ingredientText, options = defaultOptions) => {
const opts = {
...defaultOptions,
...options
};
const nqOpts = opts.decimalSeparator === "," ? { decimalSeparator: "," } : void 0;
const ignoredUOMsLC = opts.ignoreUOMs.map((u) => u.toLowerCase());
const groupHeaderRegex = buildPrefixPatternRegex(opts.groupHeaderPatterns);
const rangeSeparatorRegex = buildRangeSeparatorRegex(opts.rangeSeparators);
const stripPrefixRegex = buildStripPrefixRegex(opts.descriptionStripPrefixes);
const trailingContextRegex = buildTrailingContextRegex(opts.trailingQuantityContext);
const trailingQuantityRegex = buildTrailingQuantityRegex(opts.rangeSeparators);
const leadingQuantityPrefixRegex = buildLeadingQuantityPrefixRegex(opts.leadingQuantityPrefixes);
const uomStrings = opts.partialUnitMatching ? collectUOMStrings(Object.keys(opts.additionalUOMs).length > 0 ? buildUnitLookupMaps(opts.additionalUOMs) : getDefaultUnitLookupMaps()) : [];
return (Array.isArray(ingredientText) ? ingredientText : ingredientText.split(newLineRegExp)).map((line, index) => ({
line: line.trim(),
sourceIndex: index
})).filter(({ line }) => Boolean(line)).map(({ line, sourceIndex }) => {
const lineToParse = stripLeadingQuantityPrefixes(line, leadingQuantityPrefixRegex);
const oIng = {
quantity: null,
quantity2: null,
unitOfMeasureID: null,
unitOfMeasure: null,
description: "",
isGroupHeader: false
};
if (opts.includeMeta) oIng.meta = {
sourceText: line,
sourceIndex
};
if (lineToParse && (!isNaN(numericQuantity(lineToParse[0], nqOpts)) || lineToParse[0] === opts.decimalSeparator && !isNaN(numericQuantity(lineToParse.slice(0, 2), nqOpts)))) {
let lenNum = 6;
let nqResult = NaN;
while (lenNum > 0 && isNaN(nqResult)) {
nqResult = numericQuantity(lineToParse.substring(0, lenNum).trim(), nqOpts);
if (nqResult > -1) {
oIng.quantity = nqResult;
oIng.description = lineToParse.substring(lenNum).trim();
}
lenNum--;
}
} else {
var _trailingQtyResult$at;
const trailingQtyResult = trailingQuantityRegex.exec(lineToParse);
const trailingQtyMaybeUom = trailingQtyResult === null || trailingQtyResult === void 0 || (_trailingQtyResult$at = trailingQtyResult.at(-1)) === null || _trailingQtyResult$at === void 0 ? void 0 : _trailingQtyResult$at.toLowerCase();
if (trailingQtyMaybeUom && ignoredUOMsLC.includes(trailingQtyMaybeUom)) oIng.description = lineToParse;
else if (trailingQtyResult) {
oIng.description = lineToParse.replace(trailingQuantityRegex, "").trim();
const firstQty = trailingQtyResult[3];
const secondQty = trailingQtyResult[12];
if (!firstQty) oIng.quantity = numericQuantity(secondQty, nqOpts);
else {
oIng.quantity = numericQuantity(firstQty, nqOpts);
oIng.quantity2 = numericQuantity(secondQty, nqOpts);
}
const uomRaw = trailingQtyResult.at(-1);
if (uomRaw) {
let uomID = null;
let finalUomRaw = uomRaw;
if (oIng.description) {
const descWords = oIng.description.trim().split(/\s+/);
if (descWords.length >= 1) {
const twoWordUnit = descWords[descWords.length - 1] + " " + uomRaw;
const twoWordID = identifyUnit(twoWordUnit, options);
if (twoWordID) {
uomID = twoWordID;
finalUomRaw = twoWordUnit;
oIng.description = descWords.slice(0, -1).join(" ");
}
}
}
if (!uomID) {
uomID = identifyUnit(uomRaw, options);
finalUomRaw = uomRaw;
}
if (uomID) {
oIng.unitOfMeasureID = uomID;
oIng.unitOfMeasure = opts.normalizeUOM ? uomID : finalUomRaw;
} else if (oIng.description.match(trailingContextRegex)) oIng.description += ` ${uomRaw}`;
}
} else {
oIng.description = lineToParse;
if (oIng.description.endsWith(":") || (groupHeaderRegex === null || groupHeaderRegex === void 0 ? void 0 : groupHeaderRegex.test(oIng.description))) oIng.isGroupHeader = true;
}
}
const q2reMatch = rangeSeparatorRegex.exec(oIng.description);
if (q2reMatch) {
const q2reMatchLen = q2reMatch[1].length;
const q2Portion = stripLeadingQuantityPrefixes(oIng.description.substring(q2reMatchLen).trim(), leadingQuantityPrefixRegex);
if (q2Portion) {
const nqResultFirstChar = numericQuantity(q2Portion[0], nqOpts);
if (!isNaN(nqResultFirstChar)) {
let lenNum = 7;
let nqResult = NaN;
while (--lenNum > 0 && isNaN(nqResult)) {
nqResult = numericQuantity(q2Portion.substring(0, lenNum), nqOpts);
if (!isNaN(nqResult)) {
oIng.quantity2 = nqResult;
oIng.description = q2Portion.substring(lenNum).trim();
}
}
}
}
}
const firstWordREMatches = firstWordRegEx.exec(oIng.description);
if (firstWordREMatches) {
var _firstWordREMatches$;
const firstWord = firstWordREMatches[1].replace(/\s+/g, " ");
const remainingDesc = ((_firstWordREMatches$ = firstWordREMatches[2]) !== null && _firstWordREMatches$ !== void 0 ? _firstWordREMatches$ : "").trim();
if (remainingDesc) {
let uomID = identifyUnit(firstWord, options);
let matchedUnit = firstWord;
let finalDesc = remainingDesc;
const nextWords = remainingDesc.match(nextWordRegExp);
if (nextWords) {
const twoWordCombo = firstWord + " " + nextWords[1];
const twoWordID = identifyUnit(twoWordCombo, options);
const multiWordFinalDesc = remainingDesc.substring(nextWords[0].length).trim();
if (twoWordID && (oIng.quantity !== null || multiWordFinalDesc)) {
uomID = twoWordID;
matchedUnit = twoWordCombo;
finalDesc = multiWordFinalDesc;
}
}
if (uomID) {
oIng.unitOfMeasureID = uomID;
oIng.unitOfMeasure = opts.normalizeUOM ? uomID : matchedUnit;
oIng.description = finalDesc;
}
}
}
if (!oIng.unitOfMeasureID && opts.partialUnitMatching && oIng.description) {
const descLower = oIng.description.toLowerCase();
for (const uomStr of uomStrings) {
const idx = descLower.indexOf(uomStr.toLowerCase());
if (idx === -1) continue;
const matchedText = oIng.description.substring(idx, idx + uomStr.length);
const uomID = identifyUnit(matchedText, options);
if (!uomID) continue;
const newDesc = [oIng.description.substring(0, idx).trim(), oIng.description.substring(idx + uomStr.length).trim()].filter(Boolean).join(" ");
if (!newDesc) continue;
oIng.unitOfMeasureID = uomID;
oIng.unitOfMeasure = opts.normalizeUOM ? uomID : matchedText;
oIng.description = newDesc;
break;
}
}
if (!opts.allowLeadingOf && stripPrefixRegex && oIng.description.match(stripPrefixRegex)) oIng.description = oIng.description.replace(stripPrefixRegex, "");
return oIng;
});
};
//#endregion
export { buildLeadingQuantityPrefixRegex, buildPrefixPatternRegex, buildRangeSeparatorRegex, buildRangeSeparatorSource, buildStripPrefixRegex, buildTrailingContextRegex, buildTrailingQuantityRegex, convertUnit, defaultDescriptionStripPrefixes, defaultGroupHeaderPatterns, defaultLeadingQuantityPrefixes, defaultOptions, defaultRangeSeparators, defaultTrailingQuantityContext, escapeRegex, firstWordRegEx, fors, forsRegEx, fromRegEx, froms, identifyUnit, ofRegEx, ofs, parseIngredient, rangeSeparatorRegEx, rangeSeparatorWords, trailingQuantityRegEx, unitsOfMeasure };
//# sourceMappingURL=parse-ingredient.mjs.map