UNPKG

recipe-ingredient-parser-v3

Version:

Natural language parser for recipes and ingredient lists, incl. combining ingredients

295 lines (267 loc) โ€ข 9.61 kB
import * as convert from "./convert"; import { unitsMap } from "./units"; import { repeatingFractions } from "./repeatingFractions"; import { toTasteMap } from "./numbers"; //import * as Natural from 'natural'; //const nounInflector = new Natural.NounInflector(); export interface Ingredient { ingredient: string; quantity: string | null; unit: string | null; minQty: string | null; maxQty: string | null; } export function toTasteRecognize(input: string, language: string) { const toTaste = toTasteMap[language]; const firstLetter = toTaste.match(/\b(\w)/g); //componing first two word //const word = firstWord.concat(' ').concat(secondWord) if (firstLetter) { //checking the extended version let regEx = new RegExp("\\b" + toTaste + "\\b", "gi"); if (input.match(regEx)) { return [ (firstLetter.join(".") + ".").toLocaleLowerCase(), convert.getFirstMatch(input, regEx), true, ] as [string, string, boolean]; } const regExString = firstLetter.join("[.]?") + "[.]?"; regEx = new RegExp("\\b" + regExString + "\\b", "gi"); //const a = input.toString().split(/[\s-]+/); if (input.match(regEx)) { return [ (firstLetter.join(".") + ".").toLocaleLowerCase(), convert.getFirstMatch(input, regEx), false, ] as [string, string, boolean]; } } return ["", "", false] as [string, string, boolean]; } function getUnit(input: string, language: string) { // const word = input.concat(' ').concat(secondWord) let unit = unitsMap.get(language); let units = unit[0]; let pluralUnits = unit[1]; let symbolUnits = unit[3]; let response = [] as string[]; const [toTaste, match, extFlag] = toTasteRecognize(input, language); // If there is already a unit found for the ingredient, don't add toTaste. Fixes t.t. bug from words with two t's if (toTaste && !unit) { if (extFlag) { response = [toTaste, toTaste, match]; } else { response = [toTaste, toTaste, match]; } } else { if (units[input] || pluralUnits[input]) { response = [input, pluralUnits[input], input]; } for (const unit of Object.keys(units)) { for (const shorthand of units[unit]) { const regex = new RegExp("(?=\\b" + shorthand + "\\b)", "gi"); if (input.match(regex)) { response = [unit, pluralUnits[unit], shorthand]; } } } for (const pluralUnit of Object.keys(pluralUnits)) { const regex = new RegExp( "(?=\\b" + pluralUnits[pluralUnit] + "\\b)", "gi" ); if (input.match(regex)) { response = [ pluralUnit, pluralUnits[pluralUnit], pluralUnits[pluralUnit], ]; } } } let symbol = symbolUnits[response[0]]; response.splice(2, 0, symbol); return response; } /* return the proposition if it's used before of the name of the ingredient */ function getPreposition(input: string, language: string) { let prepositionMap = unitsMap.get(language); let prepositions = prepositionMap[2]; for (const preposition of prepositions) { let regex = new RegExp("^" + preposition); if (convert.getFirstMatch(input, regex)) return preposition; } return null; } export function parse(recipeString: string, language: string) { let ingredientLine = recipeString.trim().replace(/^(-)/, "").toLowerCase(); // removes leading and trailing whitespace /* restOfIngredient represents rest of ingredient line. For example: "1 pinch salt" --> quantity: 1, restOfIngredient: pinch salt */ let [quantity, restOfIngredient] = convert.findQuantityAndConvertIfUnicode( ingredientLine, language ) as string[]; quantity = convert.convertFromFraction(quantity); /* extraInfo will be any info in parantheses. We'll place it at the end of the ingredient. For example: "sugar (or other sweetener)" --> extraInfo: "(or other sweetener)" */ let extraInfo; if (convert.getFirstMatch(restOfIngredient, /\(([^\)]+)\)/)) { extraInfo = convert.getFirstMatch(restOfIngredient, /\(([^\)]+)\)/); restOfIngredient = restOfIngredient.replace(extraInfo, "").trim(); } // grab unit and turn it into non-plural version, for ex: "Tablespoons" OR "Tsbp." --> "tablespoon" let [unit, unitPlural, symbol, originalUnit] = getUnit( restOfIngredient, language ) as string[]; // remove unit from the ingredient if one was found and trim leading and trailing whitespace let regex_originalunit = RegExp("\\b" + originalUnit + "\\b", "gi"); let regex_unit = RegExp("\\b" + unit + "\\b", "gi"); let ingredient = !!originalUnit ? restOfIngredient.replace(regex_originalunit, "").trim() : restOfIngredient.replace(regex_unit, "").trim(); ingredient = ingredient.split(".").join("").trim(); let preposition = getPreposition(ingredient.split(" ")[0], language); if (preposition) { let regex = new RegExp("^" + preposition); ingredient = ingredient.replace(regex, "").trim(); } let minQty = quantity; // default to quantity let maxQty = quantity; // default to quantity // if quantity is non-nil and is a range, for ex: "1-2", we want to get minQty and maxQty if (quantity && quantity.includes("-")) { [minQty, maxQty] = quantity.split("-"); } if ((!quantity || quantity == "0") && !unit) { unit = "q.b."; unitPlural = "q.b."; } return { quantity: +quantity, unit: !!unit ? unit : null, unitPlural: !!unitPlural ? unitPlural : null, symbol: !!symbol ? symbol : null, ingredient: extraInfo ? `${ingredient} ${extraInfo}` : ingredient.replace(/( )*\.( )*/g, ""), minQty: +minQty, maxQty: +maxQty, }; } export function multiLineParse(recipeString: string, language: string) { let ingredients = recipeString.split(/,|๐Ÿ‘‰๐Ÿป|๐Ÿ‘‰|\r|\n|-|;/g); ingredients = ingredients.filter((line) => { // Verifica se la riga contiene una qualsiasi delle varianti della parola "ingredienti" if (/ingredient[ei]/i.test(line)) { return false; } // Verifica se la riga contiene solo numeri if (/^\d+$/.test(line)) { return false; } // Verifica se la riga contiene solo spazi bianchi o รจ vuota if (/^\s*$/.test(line)) { return false; } return true; }); let result = []; let i; for (var ingredient of ingredients) { i = parse(ingredient, language); if (i["ingredient"]) { result.push(i); } } return result; } export function combine(ingredientArray: Ingredient[]) { const combinedIngredients = ingredientArray.reduce((acc, ingredient) => { const key = ingredient.ingredient + ingredient.unit; // when combining different units, remove this from the key and just use the name const existingIngredient = acc[key]; if (existingIngredient) { return Object.assign(acc, { [key]: combineTwoIngredients(existingIngredient, ingredient), }); } else { return Object.assign(acc, { [key]: ingredient }); } }, {} as { [key: string]: Ingredient }); return Object.keys(combinedIngredients) .reduce((acc, key) => { const ingredient = combinedIngredients[key]; return acc.concat(ingredient); }, [] as Ingredient[]) .sort(compareIngredients); } export function prettyPrintingPress(ingredient: Ingredient) { let quantity = ""; let unit = ingredient.unit; if (ingredient.quantity) { const [whole, remainder] = ingredient.quantity.split("."); if (+whole !== 0 && typeof whole !== "undefined") { quantity = whole; } if (+remainder !== 0 && typeof remainder !== "undefined") { let fractional; if (repeatingFractions[remainder]) { fractional = repeatingFractions[remainder]; } else { const fraction = "0." + remainder; const len = fraction.length - 2; let denominator = Math.pow(10, len); let numerator = +fraction * denominator; const divisor = gcd(numerator, denominator); numerator /= divisor; denominator /= divisor; fractional = Math.floor(numerator) + "/" + Math.floor(denominator); } quantity += quantity ? " " + fractional : fractional; } /* if (((+whole !== 0 && typeof remainder !== 'undefined') || +whole > 1) && unit) { unit = nounInflector.pluralize(unit); }*/ } else { return ingredient.ingredient; } return `${quantity}${unit ? " " + unit : ""} ${ingredient.ingredient}`; } function gcd(a: number, b: number): number { if (b < 0.0000001) { return a; } return gcd(b, Math.floor(a % b)); } // TODO: Maybe change this to existingIngredients: Ingredient | Ingredient[] function combineTwoIngredients( existingIngredients: Ingredient, ingredient: Ingredient ): Ingredient { const quantity = existingIngredients.quantity && ingredient.quantity ? ( Number(existingIngredients.quantity) + Number(ingredient.quantity) ).toString() : null; const minQty = existingIngredients.minQty && ingredient.minQty ? ( Number(existingIngredients.minQty) + Number(ingredient.minQty) ).toString() : null; const maxQty = existingIngredients.maxQty && ingredient.maxQty ? ( Number(existingIngredients.maxQty) + Number(ingredient.maxQty) ).toString() : null; return Object.assign({}, existingIngredients, { quantity, minQty, maxQty }); } function compareIngredients(a: Ingredient, b: Ingredient) { if (a.ingredient === b.ingredient) { return 0; } return a.ingredient < b.ingredient ? -1 : 1; }