@recipe-book/core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/README.md +1 -1
- package/dist/{chunk-VWVIBCTG.cjs → chunk-D5CV3UF7.cjs} +188 -173
- package/dist/chunk-D5CV3UF7.cjs.map +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/format/number.js +1 -1
- package/dist/format/number.js.map +1 -1
- package/dist/format/options.js.map +1 -1
- package/dist/index.cjs +83 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -19
- package/dist/index.d.ts +35 -19
- package/dist/ingredient/parse.js +27 -22
- package/dist/ingredient/parse.js.map +1 -1
- package/dist/issues.js.map +1 -1
- package/dist/quantity/amount.js +11 -0
- package/dist/quantity/amount.js.map +1 -1
- package/dist/quantity/parse.js +58 -81
- package/dist/quantity/parse.js.map +1 -1
- package/dist/quantity/scale.js +5 -2
- package/dist/quantity/scale.js.map +1 -1
- package/dist/recipe/parse.js +70 -55
- package/dist/recipe/parse.js.map +1 -1
- package/dist/recipe/schema.js +8 -6
- package/dist/recipe/schema.js.map +1 -1
- package/dist/resolve.js +6 -1
- package/dist/resolve.js.map +1 -1
- package/dist/schema-org/find.js +46 -13
- package/dist/schema-org/find.js.map +1 -1
- package/dist/schema-org/from-json-ld.js +127 -57
- package/dist/schema-org/from-json-ld.js.map +1 -1
- package/dist/schema-org/text.js.map +1 -1
- package/dist/schema-org/to-json-ld.js +12 -15
- package/dist/schema-org/to-json-ld.js.map +1 -1
- package/dist/schema-org.cjs +231 -132
- package/dist/schema-org.cjs.map +1 -1
- package/dist/schema-org.d.cts +32 -19
- package/dist/schema-org.d.ts +32 -19
- package/dist/steps/temperature.js +13 -13
- package/dist/steps/temperature.js.map +1 -1
- package/dist/{types-DSMf45H5.d.cts → types-BknM8KOd.d.cts} +4 -3
- package/dist/{types-DSMf45H5.d.ts → types-BknM8KOd.d.ts} +4 -3
- package/dist/units/convert.js +16 -8
- package/dist/units/convert.js.map +1 -1
- package/dist/units/temperature.js +7 -1
- package/dist/units/temperature.js.map +1 -1
- package/dist/validation.js +1 -1
- package/dist/validation.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-VWVIBCTG.cjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ingredient/parse.ts"],"sourcesContent":["import {\n describeValue,\n type ParseResult,\n quote,\n type RecipeIssue,\n} from '../errors'\nimport { warning } from '../issues'\nimport { isPositive } from '../quantity/amount'\nimport { matchAmount, type Problem } from '../quantity/parse'\nimport type { Amount, Quantity } from '../quantity/types'\nimport { findUnit } from '../units/lookup'\nimport { units, type UnitId } from '../units/registry'\nimport { error, toResult } from '../validation'\n\nconst NUMBER_WORDS =\n 'one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve'\n\n/** A space or dash between an amount and its unit, as in \"15-ounce\". */\nconst DASH = '[ –—-]'\n\n/**\n * An ingredient read from a line of text. It has no `id`, so pass it through\n * `parseRecipe` before resolving it.\n */\nexport type IngredientLine = {\n name: string\n plural?: string | undefined\n quantity?: Quantity | undefined\n equivalents?: Quantity[] | undefined\n note?: string | undefined\n optional?: boolean | undefined\n}\n\ntype Read<T> = [value: T, end: number]\n\nconst OPTIONAL = /(?:\\(optional\\)|optional\\b ?:?) ?/iy\nconst ABOUT = /(about|approx(?:imately|\\.)?) /iy\nconst PHRASE =\n /((?:an? )?(?:handful|few|couple|little|splash|drizzle|knob|squeeze|sprinkle|dollop))(?: of)? /iy\nconst HYPHENATED_MIXED = /^(\\d+)-(?=(\\d+)[/⁄](\\d+)(?![\\d/⁄]))/\n// Text an amount can be made of, so a measure reads only up to its unit.\nconst AMOUNT_CHARS = /(?:an? )?(?:[\\d .,/⁄¼-¾⅐-⅞–—-]|to )*/iy\nconst UNIT_OF = /([a-z]+) of /iy\nconst TIMES = / ?[x×] ?(?=\\d)/iy\n// Quantity text left after a bare count, as in \"2 or 3\" or \"1 000 g\".\n// A number joined to a word, as in \"2 9-inch pie crusts\", is a name.\nconst UNREAD = / ?(?:(?:or|to|and|plus|half)\\b|[-–—/+&]|\\d+(?:\\.\\d+)?(?= |$))/iy\n// A range or alternative after the unit, as in \"1 cup to 1 ¼ cups\",\n// \"1 lb or 450 g\", or \"100g-150g\".\nconst UNREAD_AFTER_UNIT = / (?:or|to|[-–—&])(?= )|[-–—+&](?=\\d)/iy\nconst PAREN = / ?\\(([^()]*)\\)/y\nconst PARENS = / ?\\(([^()]*)\\)/g\nconst MODIFIER =\n / ?(heaping|heaped|level|scant|rounded|generous|good|small|medium|large|big)(?= )/iy\nconst JOIN = /(?: (plus|and|\\+)(?![a-z]))? ?/iy\nconst SEPARATOR = / ?[;/] ?/y\nconst OF = / ?of(?: (?:an?|the))?(?= |$)/iy\nconst PHRASE_NOTE =\n / (?=to taste|as needed|as required|plus more|plus extra|or more|or to taste|for (?:serving|garnish|dusting|greasing|frying|drizzling|decoration))/i\nconst OPTIONAL_NOTE = /(?:^|,) ?optional(?= ?(?:,|$))/gi\nconst OPTIONAL_PAREN = /^optional\\b[,;:]? ?/i\nconst CONTAINER =\n /^(?:stalk|rib|ear|sheet|loa(?:f|ves)|knob|strip|bag|box|jar|bottle|packet|envelope|tin|block|ball|handful)(?:e?s)?$/i\nconst BONELESS = /(?:^|[^a-z])(?:bone|skin)less$/i\n\nlet patterns: ReturnType<typeof compilePatterns> | undefined\n\n// A bundler drops an unused regex literal but may keep a `new RegExp` call, so\n// the patterns built from strings compile on first use.\nfunction getPatterns() {\n return (patterns ??= compilePatterns())\n}\n\nfunction compilePatterns() {\n const word = String.raw`([a-z]+(?:-[a-z]+)?)\\.?`\n const end = String.raw`(?=[ ,;()/]|[-–—+&]\\d|$)`\n return {\n count: new RegExp(`(an?|${NUMBER_WORDS}) (?=\\\\S)`, 'iy'),\n unit: new RegExp(`${DASH}?${word}${end}`, 'iy'),\n twoWordUnit: new RegExp(`${DASH}?${word} ${word}${end}`, 'iy'),\n }\n}\n\n/**\n * Parses an ingredient line, such as `'1 ½ cups (190 g) flour, sifted'`, into\n * ingredient input. Never throws. Pass the result through `parseRecipe` before\n * resolving, or pass this function to `parseRecipe` as\n * `{ parseIngredientLine }` to put lines in recipe input.\n *\n * Reads a leading amount (`2`, `1–2`, `a`, `one`) and unit, and a second\n * measure in parentheses that hold nothing but measures, or after a slash or\n * semicolon, as `equivalents`. With no amount, a unit before \"of\" reads as 1\n * when it starts the line, after any leading \"optional\" and a space or colon,\n * and is written in letters only, so `'pinch of salt'` gets\n * `quantity: { amount: 1, unit: 'pinch' }`, but `'big pinch of salt'`,\n * `'tsp. of salt'`, `'fl oz of water'` and `'Optional - pinch of salt'` get\n * none. With no\n * leading amount or unit, the first measure in parentheses that hold nothing\n * but measures is the `quantity` instead, so `'Butter (100g)'` gets\n * `quantity: { amount: 100, unit: 'g' }`. Measures after a leading \"optional\"\n * in parentheses are read the same way, except straight after the amount,\n * where the first is dropped (`'2 (optional, 15 oz) cans beans'`). A measure\n * in the same dimension, mass or volume, after \"plus\", \"and\" or \"+\", or\n * straight after the first, is added to it (`'1 lb 2 oz flour'` is 1.125 lb).\n * In parentheses, any other sum (across dimensions, in count units such as\n * \"pinch\", or of three measures) makes every measure in them part of the note\n * (`'1 cup (8 oz + 1 tbsp) milk'`).\n * It also reads a note after the first comma outside parentheses that doesn't\n * follow \"boneless\" or \"skinless\", phrases such as \"to taste\", and \"optional\"\n * at the start of the line or of text in parentheses (the innermost when they\n * nest), or as its own comma-separated item after the first comma. After a\n * leading \"optional\" and any space or colon, or \"(optional)\" and any space,\n * the rest is read as if it were the whole line, so\n * `'Optional - 2 cups flour'` gets `optional: true` and\n * `name: '- 2 cups flour'` with no quantity. With a comma there, not even\n * \"optional\" is read: `'optional, 2 cups flour'` becomes\n * `{ name: 'optional, 2 cups flour' }` with a warning. Text in parentheses\n * before the first comma that isn't a measure or a leading \"optional\"\n * (only the innermost when they nest) is a note too, ahead of the text after\n * the comma, so `'1 onion (diced), peeled'` gets `note: 'diced, peeled'`. When\n * parentheses before the first comma nest, the outer ones stay in the name with\n * their text, so `'2 onions (diced (fine)), peeled'` gets\n * `name: 'onions (diced)'` and `note: 'fine, peeled'`, and\n * `'115 g butter ((1/2 cup))'` gets `name: 'butter ()'`.\n * A bare count gets a `plural` from the word before \"of\", a leading container\n * such as \"jars\", or else the last word. There's none when that word gets no\n * other form, as when it ends in parentheses (`'2 onions (diced (fine))'`),\n * has no plural ending after a count above 1 or a range (`'12 shrimp'`), or\n * ends in \"s\" after a count of 1 or less (`'1 eggs'`, `'½ onions'`). A\n * line it can't read becomes `{ name: line }` with a warning; only blank text\n * or a value that isn't a string is an error.\n *\n * @example\n * parseIngredientLine('1 ½ cups (190 g) flour, sifted').data\n * // { name: 'flour', quantity: { amount: 1.5, unit: 'cup' }, equivalents: [{ amount: 190, unit: 'g' }], note: 'sifted' }\n * parseIngredientLine('2 large eggs').data // { name: 'large egg', plural: 'large eggs', quantity: { amount: 2 } }\n */\nexport function parseIngredientLine(text: string): ParseResult<IngredientLine> {\n const issues: RecipeIssue[] = []\n\n if (typeof text !== 'string') {\n error(\n issues,\n 'invalid_type',\n [],\n `Expected ingredient text such as \"2 cups flour\", received ${describeValue(text)}.`,\n )\n return toResult<IngredientLine>('invalid_ingredient', undefined, issues)\n }\n\n // U+200B is invisible but isn't whitespace. U+200C and U+200D are kept,\n // because they join letters in some scripts and in emoji.\n const line = text.replace(/[\\s\\u200b]+/g, ' ').trim()\n\n if (line === '') {\n error(\n issues,\n 'missing_field',\n [],\n 'Add the ingredient text, e.g. \"2 cups flour\".',\n )\n return toResult<IngredientLine>(\n 'invalid_ingredient',\n undefined,\n issues,\n quote(text),\n )\n }\n\n let ingredient = readLine(line)\n\n if (isProblem(ingredient)) {\n warning(\n issues,\n ingredient.code,\n [],\n `${ingredient.message} The whole line is kept as the name.`,\n )\n ingredient = { name: line }\n }\n\n return toResult('invalid_ingredient', ingredient, issues)\n}\n\nfunction readLine(line: string): IngredientLine | Problem {\n const p = getPatterns()\n const notes: string[] = []\n const equivalents: Quantity[] = []\n let optional = false\n let quantity: Quantity | undefined\n let amount: Amount | undefined\n let unitText = ''\n let i = 0\n let match: RegExpExecArray | null\n\n const eat = (pattern: RegExp): RegExpExecArray | null => {\n const found = exec(pattern, line, i)\n if (found) {\n i = pattern.lastIndex\n }\n return found\n }\n\n const addNote = (text: string): void => {\n const note = /[^ ,](?:.*[^ ,])?/s.exec(text)\n if (note) {\n notes.push(note[0])\n }\n }\n\n const readParen = (content: string, keep: boolean): string => {\n const text = content.replace(OPTIONAL_PAREN, () => {\n optional = true\n return ''\n })\n const measures = text ? readMeasures(text) : undefined\n\n if (measures) {\n quantity ??= measures.shift()\n // A spread passes each measure as an argument, which overflows the stack\n // for a long list.\n for (const measure of measures) {\n equivalents.push(measure)\n }\n return ''\n }\n\n if (keep && text) {\n return ` (${text})`\n }\n\n addNote(text)\n return ''\n }\n\n if (eat(OPTIONAL)) {\n optional = true\n }\n\n match = exec(ABOUT, line, i)\n if (match && readAmount(line, ABOUT.lastIndex)) {\n addNote(match[1]!)\n i = ABOUT.lastIndex\n }\n\n if ((match = eat(PHRASE))) {\n addNote(match[1]!)\n } else if ((match = eat(p.count))) {\n amount = NUMBER_WORDS.split('|').indexOf(match[1]!.toLowerCase()) + 1 || 1\n } else {\n const found = readAmount(line, i, true)\n\n if (isProblem(found)) {\n return found\n }\n if (found) {\n ;[amount, i] = found\n } else if ((match = exec(UNIT_OF, line, i))) {\n const unit = findUnit(match[1]!)\n if (unit) {\n quantity = { amount: 1, unit }\n i = UNIT_OF.lastIndex\n }\n }\n }\n\n if (amount !== undefined) {\n let each: Quantity[] | undefined\n eat(TIMES)\n\n if ((match = eat(PAREN))) {\n each = readMeasures(match[1]!)\n if (!each) {\n readParen(match[1]!, false)\n }\n } else {\n const measure = readMeasure(line, i, false)\n if (measure) {\n each = [measure[0]]\n i = measure[1]\n }\n }\n\n const start = i\n const modifier = eat(MODIFIER)\n const unit = readUnit(line, i)\n\n if (unit) {\n if (modifier) {\n addNote(modifier[1]!)\n }\n unitText = line.slice(i, unit[1])\n quantity = { amount, unit: unit[0] }\n i = unit[1]\n\n const size =\n !each &&\n units[unit[0]].dimension === 'count' &&\n readMeasure(line, i, false)\n\n if (size) {\n equivalents.push(size[0])\n i = size[1]\n } else if (!each) {\n const extended = extend(line, [quantity, i])\n if (isProblem(extended)) {\n return extended\n }\n ;[quantity, i] = extended\n }\n\n while ((match = eat(SEPARATOR))) {\n const measure = readMeasure(line, i, true)\n if (!measure) {\n return noMeasure(quote(match[0].trim()), '100 g/3½ oz')\n }\n equivalents.push(measure[0])\n i = measure[1]\n }\n } else {\n i = start\n quantity = { amount }\n }\n\n if (exec(unit ? UNREAD_AFTER_UNIT : UNREAD, line, i)) {\n return {\n code: 'invalid_quantity',\n message: `Couldn't read the quantity in ${quote(line)}. Write one amount or range before the unit, e.g. \"2-3 eggs\" or \"2 ½ cups\".`,\n }\n }\n\n for (const item of each ?? []) {\n const total = combine(amount, item.amount, (a, b) => a * b)\n if (isProblem(total)) {\n return total\n }\n equivalents.push({ ...item, amount: total })\n }\n }\n\n const of = quantity !== undefined && eat(OF)\n\n const rest = line.slice(i).trim()\n const comma = topLevelComma(rest)\n let name = rest\n .slice(0, comma)\n .replace(PARENS, (_, content: string) => readParen(content, false))\n const phrase = PHRASE_NOTE.exec(name)\n\n if (phrase) {\n addNote(name.slice(phrase.index))\n name = name.slice(0, phrase.index)\n }\n\n addNote(\n rest\n .slice(comma + 1)\n .replace(PARENS, (_, content: string) => readParen(content, true))\n .replace(OPTIONAL_NOTE, () => {\n optional = true\n return ''\n }),\n )\n name = name.trim()\n\n if (!name) {\n if (\n of ||\n !unitText ||\n !quantity?.unit ||\n units[quantity.unit].dimension !== 'count'\n ) {\n return {\n code: 'missing_field',\n message: 'Add a name after the quantity, e.g. \"2 cups flour\".',\n }\n }\n name = unitText.replace(/^[ –—-]/, '')\n quantity = { amount: quantity.amount }\n }\n\n let plural: string | undefined\n if (quantity && !quantity.unit) {\n const many = typeof quantity.amount !== 'number' || quantity.amount > 1\n ;[name, plural] = countForms(name, many)\n }\n\n return {\n name,\n ...(plural !== undefined && { plural }),\n ...(quantity && { quantity }),\n ...(equivalents.length > 0 && { equivalents }),\n ...(notes.length > 0 && { note: notes.join(', ') }),\n ...(optional && { optional }),\n }\n}\n\nfunction exec(\n pattern: RegExp,\n text: string,\n start: number,\n): RegExpExecArray | null {\n pattern.lastIndex = start\n return pattern.exec(text)\n}\n\n/**\n * Reads the amount at `start` with `matchAmount`, ending where the text after\n * it starts. In a line, \"1-1/2\" is 1 ½, where `parseQuantity` reads a reversed\n * range. Unless `whole` is set, `matchAmount` sees only the amount and the\n * character after it, so a run of measures is read in linear time; a problem's\n * message then quotes only that.\n */\nfunction readAmount(\n text: string,\n start: number,\n whole?: boolean,\n): Read<Amount> | Problem | undefined {\n exec(AMOUNT_CHARS, text, start)\n const rest = text\n .slice(start, whole ? undefined : AMOUNT_CHARS.lastIndex + 1)\n .replace(\n HYPHENATED_MIXED,\n (dash, whole: string, numerator: string, denominator: string) =>\n Number(numerator) < Number(denominator) ? `${whole} ` : dash,\n )\n // matchAmount refuses text with a \"1,5\" anywhere in it, but in a line only\n // one in the amount counts.\n const comma = rest.search(/\\d,\\d/)\n const head = comma < 0 ? rest : rest.slice(0, comma).trimEnd()\n const match = matchAmount(head)\n\n if (comma >= 0 && (match ? !('code' in match) && !match.rest : !head)) {\n return matchAmount(rest) as Problem\n }\n if (!match || 'code' in match) {\n return match\n }\n\n return [match.amount, start + head.length - match.rest.length]\n}\n\nfunction isProblem(value: unknown): value is Problem {\n return typeof value === 'object' && value !== null && 'code' in value\n}\n\nfunction readUnit(text: string, start: number): Read<UnitId> | undefined {\n const { twoWordUnit, unit } = getPatterns()\n\n for (const pattern of [twoWordUnit, unit]) {\n const match = exec(pattern, text, start)\n const id =\n match && findUnit(match[2] ? `${match[1]} ${match[2]}` : match[1]!)\n if (id) {\n return [id, pattern.lastIndex]\n }\n }\n\n return undefined\n}\n\n/** A numeric amount and a unit, such as \"190 g\", \"14-ounce\", or \"1 lb 2 oz\". */\nfunction readMeasure(\n text: string,\n start: number,\n compound: boolean,\n): Read<Quantity> | undefined {\n const amount = readAmount(text, text[start] === ' ' ? start + 1 : start)\n\n if (!amount || isProblem(amount)) {\n return undefined\n }\n\n const unit = readUnit(text, amount[1])\n\n if (!unit) {\n return undefined\n }\n\n const measure: Read<Quantity> = [\n { amount: amount[0], unit: unit[0] },\n unit[1],\n ]\n const extended = compound ? extend(text, measure) : measure\n return isProblem(extended) ? undefined : extended\n}\n\n/**\n * Adds a second measure in the same dimension, as in \"1 cup plus 2 tbsp\" or\n * \"1 lb 2 oz\". After \"plus\", \"and\", or \"+\", the second measure is required.\n */\nfunction extend(\n text: string,\n measure: Read<Quantity>,\n): Read<Quantity> | Problem {\n const [{ amount, unit }, end] = measure\n const connector = exec(JOIN, text, end)![1]\n const extra = readMeasure(text, JOIN.lastIndex, false)\n const from = units[unit!]\n const to = extra && units[extra[0].unit!]\n\n if (!from.toBase || !to?.toBase || from.dimension !== to.dimension) {\n return connector\n ? noMeasure(\n `${quote(connector)} in the same kind of unit`,\n '1 cup plus 2 tbsp',\n )\n : measure\n }\n\n const ratio = to.toBase / from.toBase\n const total = combine(amount, extra![0].amount, (a, b) => a + b * ratio)\n return isProblem(total) ? total : [{ amount: total, unit }, extra![1]]\n}\n\nfunction noMeasure(after: string, example: string): Problem {\n return {\n code: 'invalid_quantity',\n message: `Expected a measure after ${after}, e.g. \"${example}\".`,\n }\n}\n\n/** Measures separated by \";\" or \"/\", as in \"12 Tbsp; 170g\", filling `text`. */\nfunction readMeasures(text: string): Quantity[] | undefined {\n const measures: Quantity[] = []\n let i = 0\n\n for (;;) {\n const measure = readMeasure(text, i, true)\n if (!measure) {\n return undefined\n }\n measures.push(measure[0])\n i = measure[1]\n\n if (i === text.length) {\n return measures\n }\n if (!exec(SEPARATOR, text, i)) {\n return undefined\n }\n i = SEPARATOR.lastIndex\n }\n}\n\n/** Multiplies or adds two amounts. A total that overflows or underflows is a problem. */\nfunction combine(\n a: Amount,\n b: Amount,\n op: (a: number, b: number) => number,\n): Amount | Problem {\n const [aMin, aMax] = typeof a === 'number' ? [a, a] : [a.min, a.max]\n const [bMin, bMax] = typeof b === 'number' ? [b, b] : [b.min, b.max]\n const min = op(aMin, bMin)\n const max = op(aMax, bMax)\n\n if (!isPositive(min) || !isPositive(max)) {\n return {\n code: 'invalid_amount',\n message:\n 'The total amount is out of range. Write it as one amount, e.g. \"800 g\".',\n }\n }\n\n return min < max ? { min, max } : min\n}\n\n/** The first comma outside parentheses that ends a name, skipping \"boneless, skinless\". */\nfunction topLevelComma(text: string): number {\n let depth = 0\n\n for (let j = 0; j < text.length; j++) {\n const char = text[j]\n if (char === '(') {\n depth++\n } else if (char === ')') {\n depth = Math.max(0, depth - 1)\n } else if (\n char === ',' &&\n depth === 0 &&\n !BONELESS.test(text.slice(0, j).slice(-9))\n ) {\n return j\n }\n }\n\n return text.length\n}\n\n/**\n * Returns the singular name and its plural. The counted word is the one before\n * \"of\", a leading container such as \"stalks\", or else the last word.\n */\nfunction countForms(\n name: string,\n many: boolean,\n): [name: string, plural: string | undefined] {\n const words = name.split(' ')\n let k = words.indexOf('of') - 1\n\n if (k < 0) {\n k = CONTAINER.test(words[0]!) ? 0 : words.length - 1\n }\n\n const word = words[k]!\n const other = many ? singular(word) : pluralize(word)\n\n if (other === word) {\n return [name, undefined]\n }\n\n words[k] = other\n const swapped = words.join(' ')\n return many ? [swapped, name] : [name, swapped]\n}\n\nfunction singular(word: string): string {\n return word.replace(\n /(?:(lea|hal|loa)ves|([rtdv])ies|(o|ch|sh|x|ss|chill?i)es|([^su])s)$/i,\n (_, f?: string, y?: string, es?: string, s?: string) =>\n f ? `${f}f` : y ? `${y}y` : (es ?? s)!,\n )\n}\n\nfunction pluralize(word: string): string {\n return word.replace(\n /(?:(lea|hal|loa)f|([^aeiou])y|(ch|sh|x|z|tomato|potato|mango|chill?i)|[a-rt-z])$/i,\n (match, f?: string, y?: string, es?: string) =>\n f ? `${f}ves` : y ? `${y}ies` : es ? `${es}es` : `${match}s`,\n )\n}\n"],"mappings":"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","names":["whole"]}
|
|
1
|
+
{"version":3,"sources":["../../src/ingredient/parse.ts"],"sourcesContent":["import {\n describeValue,\n type ParseResult,\n quote,\n type RecipeIssue,\n} from '../errors'\nimport { warning } from '../issues'\nimport { isPositive } from '../quantity/amount'\nimport { matchAmount, type Problem } from '../quantity/parse'\nimport type { Amount, Quantity } from '../quantity/types'\nimport { findUnit } from '../units/lookup'\nimport { units, type UnitId } from '../units/registry'\nimport { error, toResult } from '../validation'\n\nconst NUMBER_WORDS =\n 'one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve'\n\n/** A space or dash between an amount and its unit, as in \"15-ounce\". */\nconst DASH = '[ –—-]'\n\n/**\n * An ingredient read from a line of text. It has no `id`, so pass it through\n * `parseRecipe` before resolving it.\n */\nexport type IngredientLine = {\n name: string\n plural?: string | undefined\n quantity?: Quantity | undefined\n equivalents?: Quantity[] | undefined\n note?: string | undefined\n optional?: boolean | undefined\n}\n\ntype Read<T> = [value: T, end: number]\n\nconst OPTIONAL = /(?:\\(optional\\)|optional\\b ?:?) ?/iy\nconst ABOUT = /(about|approx(?:imately|\\.)?) /iy\nconst PHRASE =\n /((?:an? )?(?:handful|few|couple|little|splash|drizzle|knob|squeeze|sprinkle|dollop))(?: of)? /iy\nconst HYPHENATED_MIXED = /^(\\d+)-(?=(\\d+)[/⁄](\\d+)(?![\\d/⁄]))/\n// Text an amount can be made of, so a measure reads only up to its unit.\nconst AMOUNT_CHARS = /(?:an? )?(?:[\\d .,/⁄¼-¾⅐-⅞–—-]|to )*/iy\nconst TIMES = / ?[x×] ?(?=\\d)/iy\n// Quantity text left after a bare count, as in \"2 or 3\" or \"1 000 g\".\n// A number joined to a word, as in \"2 9-inch pie crusts\", is a name.\nconst UNREAD =\n / ?(?:(?:or|to|and|plus|half)(?![\\p{L}\\p{M}\\d_])|[-–—/+&]|\\d+(?:\\.\\d+)?(?= |$))/iuy\n// A range or alternative after the unit, as in \"1 cup to 1 ¼ cups\",\n// \"1 lb or 450 g\", or \"100g-150g\".\nconst UNREAD_AFTER_UNIT = / (?:or|to|[-–—&])(?= )|[-–—+&](?=\\d)/iy\nconst PAREN = / ?\\(([^()]*)\\)/y\nconst PARENS = / ?\\(([^()]*)\\)/g\nconst MODIFIER =\n / ?(heaping|heaped|level|scant|rounded|generous|good|small|medium|large|big)(?= )/iy\nconst JOIN = /(?: (plus|and|\\+)(?![\\p{L}\\p{M}]))? ?/iuy\nconst SEPARATOR = / ?[;/] ?/y\nconst OF = / ?of(?: (?:an?|the))?(?= |$)/iy\n// A name and a colon, as in \"water: 320 ml\", that isn't a heading such as\n// \"For the sauce:\" or \"Yield:\", or just a servings or nutrition label such as\n// \"Serves:\" or \"Protein:\".\nconst NAMED =\n /(?!(?:for|to|an?|yields?|total)(?![\\p{L}\\p{M}\\d_])|(?:makes|serv(?:es|ings?)(?: size)?|protein|carbs|carbohydrates?|fiber|fibre|sodium|cholesterol|saturated fat|fat) ?:)([^\\d:]+):/iuy\nconst PHRASE_NOTE =\n / (?=to taste|as needed|as required|plus more|plus extra|or more|or to taste|for (?:serving|garnish|dusting|greasing|frying|drizzling|decoration))/i\nconst OPTIONAL_NOTE = /(?:^|,) ?optional(?= ?(?:,|$))/gi\nconst OPTIONAL_PAREN = /^optional\\b[,;:]? ?/i\nconst CONTAINER =\n /^(?:stalk|rib|ear|sheet|loa(?:f|ves)|knob|strip|bag|box|jar|bottle|packet|envelope|tin|block|ball|handful)(?:e?s)?$/i\nconst BONELESS = /(?:^|[^a-z])(?:bone|skin)less$/i\n\nlet patterns: ReturnType<typeof compilePatterns> | undefined\n\n// A bundler drops an unused regex literal but may keep a `new RegExp` call, so\n// the patterns built from strings compile on first use.\nfunction getPatterns() {\n return (patterns ??= compilePatterns())\n}\n\nfunction compilePatterns() {\n const word = String.raw`([a-z]+(?:-[a-z]+)?)\\.?`\n const end = String.raw`(?=[ ,;()/]|[-–—+&]\\d|$)`\n return {\n count: new RegExp(`(an?|${NUMBER_WORDS}) (?=\\\\S)`, 'iy'),\n unit: new RegExp(`${DASH}?${word}${end}`, 'iy'),\n twoWordUnit: new RegExp(`${DASH}?${word} ${word}${end}`, 'iy'),\n }\n}\n\n/**\n * Parses an ingredient line, such as `'1 ½ cups (190 g) flour, sifted'`, into\n * ingredient input. Never throws. Pass the result through `parseRecipe` before\n * resolving, or pass this function to `parseRecipe` as\n * `{ parseIngredientLine }` to put lines in recipe input.\n *\n * Reads a leading amount (`2`, `1–2`, `a`, `one`) and unit, and a second\n * measure in parentheses that hold nothing but measures, or after a slash or\n * semicolon, as `equivalents`. With no amount, a unit before \"of\" reads as 1\n * when it starts the line, after any leading \"optional\" and any space or\n * colon, or \"(optional)\" and any space, and after any size word such as\n * \"big\", so `'tsp. of salt'` gets `quantity: { amount: 1, unit: 'tsp' }` and\n * `'big pinch of salt'` gets `quantity: { amount: 1, unit: 'pinch' }` and\n * `note: 'big'`, but `'Optional - pinch of salt'` gets none. With no\n * leading amount or unit, the first measure in parentheses that hold nothing\n * but measures is the `quantity` instead, so `'Butter (100g)'` gets\n * `quantity: { amount: 100, unit: 'g' }`. Measures after a leading \"optional\"\n * in parentheses are read the same way, except straight after the amount,\n * where the first is dropped (`'2 (optional, 15 oz) cans beans'`). A measure\n * in the same dimension, mass or volume, after \"plus\", \"and\" or \"+\", or\n * straight after the first, is added to it (`'1 lb 2 oz flour'` is 1.125 lb).\n * In parentheses, any other sum (across dimensions, in count units such as\n * \"pinch\", or of three measures) makes every measure in them part of the note\n * (`'1 cup (8 oz + 1 tbsp) milk'`).\n * With no leading amount or unit, an amount and unit after the name and a\n * colon are read when nothing else follows but a note after a comma, so\n * `'nagaimo: 160 g, grated'` gets `name: 'nagaimo'`,\n * `quantity: { amount: 160, unit: 'g' }` and `note: 'grated'`. A name with a\n * digit, or that starts with the word \"for\", \"to\", \"a\", \"an\", \"yield\",\n * \"yields\" or \"total\", isn't read that way, and neither is one that is just\n * \"makes\", \"serves\", \"serving\", \"servings\", \"serving size\", \"protein\",\n * \"carbs\", \"carbohydrate\", \"carbohydrates\", \"fiber\", \"fibre\", \"sodium\",\n * \"cholesterol\", \"saturated fat\" or \"fat\". So `'For the sauce: 1 cup'` and\n * `'Protein: 12 g'` keep the whole line as the name, but\n * `'Protein powder: 30 g'` gets `name: 'Protein powder'`.\n * It also reads a note after the first comma outside parentheses that doesn't\n * follow \"boneless\" or \"skinless\", phrases such as \"to taste\", and \"optional\"\n * at the start of the line or of text in parentheses (the innermost when they\n * nest), or as its own comma-separated item after the first comma. After a\n * leading \"optional\" and any space or colon, or \"(optional)\" and any space,\n * the rest is read as if it were the whole line, so\n * `'Optional - 2 cups flour'` gets `optional: true` and\n * `name: '- 2 cups flour'` with no quantity. With a comma there, not even\n * \"optional\" is read: `'optional, 2 cups flour'` becomes\n * `{ name: 'optional, 2 cups flour' }` with a warning. Text in parentheses\n * before the first comma that isn't a measure or a leading \"optional\"\n * (only the innermost when they nest) is a note too, ahead of the text after\n * the comma, so `'1 onion (diced), peeled'` gets `note: 'diced, peeled'`. When\n * parentheses before the first comma nest, the outer ones stay in the name with\n * their text, so `'2 onions (diced (fine)), peeled'` gets\n * `name: 'onions (diced)'` and `note: 'fine, peeled'`, and\n * `'115 g butter ((1/2 cup))'` gets `name: 'butter ()'`.\n * A bare count gets a `plural` from the word before \"of\", a leading container\n * such as \"jars\", or else the last word. There's none when that word gets no\n * other form, as when it ends in parentheses (`'2 onions (diced (fine))'`),\n * has no plural ending after a count above 1 or a range (`'12 shrimp'`), or\n * ends in \"s\" after a count of 1 or less (`'1 eggs'`, `'½ onions'`). A\n * line it can't read becomes `{ name: line }` with a warning; only blank text\n * or a value that isn't a string is an error.\n *\n * @example\n * parseIngredientLine('1 ½ cups (190 g) flour, sifted').data\n * // { name: 'flour', quantity: { amount: 1.5, unit: 'cup' }, equivalents: [{ amount: 190, unit: 'g' }], note: 'sifted' }\n * parseIngredientLine('2 large eggs').data // { name: 'large egg', plural: 'large eggs', quantity: { amount: 2 } }\n */\nexport function parseIngredientLine(text: string): ParseResult<IngredientLine> {\n const issues: RecipeIssue[] = []\n\n if (typeof text !== 'string') {\n error(\n issues,\n 'invalid_type',\n [],\n `Expected ingredient text such as \"2 cups flour\", received ${describeValue(text)}.`,\n )\n return toResult<IngredientLine>('invalid_ingredient', undefined, issues)\n }\n\n // U+200B is invisible but isn't whitespace. U+200C and U+200D are kept,\n // because they join letters in some scripts and in emoji.\n const line = text.replace(/[\\s\\u200b]+/g, ' ').trim()\n\n if (line === '') {\n error(\n issues,\n 'missing_field',\n [],\n 'Add the ingredient text, e.g. \"2 cups flour\".',\n )\n return toResult<IngredientLine>(\n 'invalid_ingredient',\n undefined,\n issues,\n quote(text),\n )\n }\n\n let ingredient = readLine(line)\n\n if (isProblem(ingredient)) {\n warning(\n issues,\n ingredient.code,\n [],\n `${ingredient.message} The whole line is kept as the name.`,\n )\n ingredient = { name: line }\n }\n\n return toResult('invalid_ingredient', ingredient, issues)\n}\n\nfunction readLine(line: string): IngredientLine | Problem {\n const p = getPatterns()\n const notes: string[] = []\n const equivalents: Quantity[] = []\n let optional = false\n let quantity: Quantity | undefined\n let amount: Amount | undefined\n let unitText = ''\n let i = 0\n let match: RegExpExecArray | null\n\n const eat = (pattern: RegExp): RegExpExecArray | null => {\n const found = exec(pattern, line, i)\n if (found) {\n i = pattern.lastIndex\n }\n return found\n }\n\n const addNote = (text: string): void => {\n const note = /[^ ,](?:.*[^ ,])?/s.exec(text)\n if (note) {\n notes.push(note[0])\n }\n }\n\n const readParen = (content: string, keep: boolean): string => {\n const text = content.replace(OPTIONAL_PAREN, () => {\n optional = true\n return ''\n })\n const measures = text ? readMeasures(text) : undefined\n\n if (measures) {\n quantity ??= measures.shift()\n // A spread passes each measure as an argument, which overflows the stack\n // for a long list.\n for (const measure of measures) {\n equivalents.push(measure)\n }\n return ''\n }\n\n if (keep && text) {\n return ` (${text})`\n }\n\n addNote(text)\n return ''\n }\n\n if (eat(OPTIONAL)) {\n optional = true\n }\n\n match = exec(ABOUT, line, i)\n if (match && readAmount(line, ABOUT.lastIndex)) {\n addNote(match[1]!)\n i = ABOUT.lastIndex\n }\n\n if ((match = eat(PHRASE))) {\n addNote(match[1]!)\n } else if ((match = eat(p.count))) {\n amount = NUMBER_WORDS.split('|').indexOf(match[1]!.toLowerCase()) + 1 || 1\n } else {\n const found = readAmount(line, i, true)\n\n if (isProblem(found)) {\n return found\n }\n if (found) {\n ;[amount, i] = found\n } else {\n const modifier = exec(MODIFIER, line, i)\n const unit = readUnit(line, modifier ? MODIFIER.lastIndex : i)\n if (unit && exec(OF, line, unit[1])) {\n if (modifier) {\n addNote(modifier[1]!)\n }\n quantity = { amount: 1, unit: unit[0] }\n i = unit[1]\n } else if ((match = exec(NAMED, line, i))) {\n const measure = readMeasure(line, NAMED.lastIndex, false)\n if (measure && (line[measure[1]] ?? ',') === ',') {\n quantity = measure[0]\n line = match[1] + line.slice(measure[1])\n i = 0\n }\n }\n }\n }\n\n if (amount !== undefined) {\n let each: Quantity[] | undefined\n eat(TIMES)\n\n if ((match = eat(PAREN))) {\n each = readMeasures(match[1]!)\n if (!each) {\n readParen(match[1]!, false)\n }\n } else {\n const measure = readMeasure(line, i, false)\n if (measure) {\n each = [measure[0]]\n i = measure[1]\n }\n }\n\n const start = i\n const modifier = eat(MODIFIER)\n const unit = readUnit(line, i)\n\n if (unit) {\n if (modifier) {\n addNote(modifier[1]!)\n }\n unitText = line.slice(i, unit[1])\n quantity = { amount, unit: unit[0] }\n i = unit[1]\n\n const size =\n !each &&\n units[unit[0]].dimension === 'count' &&\n readMeasure(line, i, false)\n\n if (size) {\n equivalents.push(size[0])\n i = size[1]\n } else if (!each) {\n const extended = extend(line, [quantity, i])\n if (isProblem(extended)) {\n return extended\n }\n ;[quantity, i] = extended\n }\n\n while ((match = eat(SEPARATOR))) {\n const measure = readMeasure(line, i, true)\n if (!measure) {\n return noMeasure(quote(match[0].trim()), '100 g/3½ oz')\n }\n equivalents.push(measure[0])\n i = measure[1]\n }\n } else {\n i = start\n quantity = { amount }\n }\n\n if (exec(unit ? UNREAD_AFTER_UNIT : UNREAD, line, i)) {\n return {\n code: 'invalid_quantity',\n message: `Couldn't read the quantity in ${quote(line)}. Write one amount or range before the unit, e.g. \"2-3 eggs\" or \"2 ½ cups\".`,\n }\n }\n\n for (const item of each ?? []) {\n const total = combine(amount, item.amount, (a, b) => a * b)\n if (isProblem(total)) {\n return total\n }\n equivalents.push({ ...item, amount: total })\n }\n }\n\n const of = quantity !== undefined && eat(OF)\n\n const rest = line.slice(i).trim()\n const comma = topLevelComma(rest)\n let name = rest\n .slice(0, comma)\n .replace(PARENS, (_, content: string) => readParen(content, false))\n const phrase = PHRASE_NOTE.exec(name)\n\n if (phrase) {\n addNote(name.slice(phrase.index))\n name = name.slice(0, phrase.index)\n }\n\n addNote(\n rest\n .slice(comma + 1)\n .replace(PARENS, (_, content: string) => readParen(content, true))\n .replace(OPTIONAL_NOTE, () => {\n optional = true\n return ''\n }),\n )\n name = name.trim()\n\n if (!name) {\n if (\n of ||\n !unitText ||\n !quantity?.unit ||\n units[quantity.unit].dimension !== 'count'\n ) {\n return {\n code: 'missing_field',\n message: 'Add a name after the quantity, e.g. \"2 cups flour\".',\n }\n }\n name = unitText.replace(/^[ –—-]/, '')\n quantity = { amount: quantity.amount }\n }\n\n let plural: string | undefined\n if (quantity && !quantity.unit) {\n const many = typeof quantity.amount !== 'number' || quantity.amount > 1\n ;[name, plural] = countForms(name, many)\n }\n\n return {\n name,\n ...(plural !== undefined && { plural }),\n ...(quantity && { quantity }),\n ...(equivalents.length > 0 && { equivalents }),\n ...(notes.length > 0 && { note: notes.join(', ') }),\n ...(optional && { optional }),\n }\n}\n\nfunction exec(\n pattern: RegExp,\n text: string,\n start: number,\n): RegExpExecArray | null {\n pattern.lastIndex = start\n return pattern.exec(text)\n}\n\n/**\n * Reads the amount at `start` with `matchAmount`, ending where the text after\n * it starts. In a line, \"1-1/2\" is 1 ½, where `parseQuantity` reads a reversed\n * range. Unless `whole` is set, `matchAmount` sees only the amount and the\n * character after it, so a run of measures is read in linear time; a problem's\n * message then quotes only that.\n */\nfunction readAmount(\n text: string,\n start: number,\n whole?: boolean,\n): Read<Amount> | Problem | undefined {\n exec(AMOUNT_CHARS, text, start)\n const rest = text\n .slice(start, whole ? undefined : AMOUNT_CHARS.lastIndex + 1)\n .replace(\n HYPHENATED_MIXED,\n (dash, whole: string, numerator: string, denominator: string) =>\n Number(numerator) < Number(denominator) ? `${whole} ` : dash,\n )\n // matchAmount refuses text with a \"1,5\" anywhere in it, but in a line only\n // one in the amount counts.\n const comma = rest.search(/\\d,\\d/)\n const head = comma < 0 ? rest : rest.slice(0, comma).trimEnd()\n const match = matchAmount(head)\n\n if (comma >= 0 && (match ? !('code' in match) && !match.rest : !head)) {\n return matchAmount(rest) as Problem\n }\n if (!match || 'code' in match) {\n return match\n }\n\n return [match.amount, start + head.length - match.rest.length]\n}\n\nfunction isProblem(value: unknown): value is Problem {\n return typeof value === 'object' && value !== null && 'code' in value\n}\n\nfunction readUnit(text: string, start: number): Read<UnitId> | undefined {\n const { twoWordUnit, unit } = getPatterns()\n\n for (const pattern of [twoWordUnit, unit]) {\n const match = exec(pattern, text, start)\n const id = match && findUnit(match.slice(1).join(' '))\n if (id) {\n return [id, pattern.lastIndex]\n }\n }\n\n return undefined\n}\n\n/** A numeric amount and a unit, such as \"190 g\", \"14-ounce\", or \"1 lb 2 oz\". */\nfunction readMeasure(\n text: string,\n start: number,\n compound: boolean,\n): Read<Quantity> | undefined {\n const amount = readAmount(text, text[start] === ' ' ? start + 1 : start)\n\n if (!amount || isProblem(amount)) {\n return undefined\n }\n\n const unit = readUnit(text, amount[1])\n\n if (!unit) {\n return undefined\n }\n\n const measure: Read<Quantity> = [\n { amount: amount[0], unit: unit[0] },\n unit[1],\n ]\n const extended = compound ? extend(text, measure) : measure\n return isProblem(extended) ? undefined : extended\n}\n\n/**\n * Adds a second measure in the same dimension, as in \"1 cup plus 2 tbsp\" or\n * \"1 lb 2 oz\". After \"plus\", \"and\", or \"+\", the second measure is required.\n */\nfunction extend(\n text: string,\n measure: Read<Quantity>,\n): Read<Quantity> | Problem {\n const [{ amount, unit }, end] = measure\n const connector = exec(JOIN, text, end)![1]\n const extra = readMeasure(text, JOIN.lastIndex, false)\n const from = units[unit!]\n const to = extra && units[extra[0].unit!]\n\n if (!from.toBase || !to?.toBase || from.dimension !== to.dimension) {\n return connector\n ? noMeasure(\n `${quote(connector)} in the same kind of unit`,\n '1 cup plus 2 tbsp',\n )\n : measure\n }\n\n const ratio = to.toBase / from.toBase\n const total = combine(amount, extra![0].amount, (a, b) => a + b * ratio)\n return isProblem(total) ? total : [{ amount: total, unit }, extra![1]]\n}\n\nfunction noMeasure(after: string, example: string): Problem {\n return {\n code: 'invalid_quantity',\n message: `Expected a measure after ${after}, e.g. \"${example}\".`,\n }\n}\n\n/** Measures separated by \";\" or \"/\", as in \"12 Tbsp; 170g\", filling `text`. */\nfunction readMeasures(text: string): Quantity[] | undefined {\n const measures: Quantity[] = []\n let i = 0\n\n for (;;) {\n const measure = readMeasure(text, i, true)\n if (!measure) {\n return undefined\n }\n measures.push(measure[0])\n i = measure[1]\n\n if (i === text.length) {\n return measures\n }\n if (!exec(SEPARATOR, text, i)) {\n return undefined\n }\n i = SEPARATOR.lastIndex\n }\n}\n\n/** Multiplies or adds two amounts. A total that overflows or underflows is a problem. */\nfunction combine(\n a: Amount,\n b: Amount,\n op: (a: number, b: number) => number,\n): Amount | Problem {\n const [aMin, aMax] = typeof a === 'number' ? [a, a] : [a.min, a.max]\n const [bMin, bMax] = typeof b === 'number' ? [b, b] : [b.min, b.max]\n const min = op(aMin, bMin)\n const max = op(aMax, bMax)\n\n if (!isPositive(min) || !isPositive(max)) {\n return {\n code: 'invalid_amount',\n message:\n 'The total amount is out of range. Write it as one amount, e.g. \"800 g\".',\n }\n }\n\n return min < max ? { min, max } : min\n}\n\n/** The first comma outside parentheses that ends a name, skipping \"boneless, skinless\". */\nfunction topLevelComma(text: string): number {\n let depth = 0\n\n for (let j = 0; j < text.length; j++) {\n const char = text[j]\n if (char === '(') {\n depth++\n } else if (char === ')') {\n depth = Math.max(0, depth - 1)\n } else if (\n char === ',' &&\n depth === 0 &&\n !BONELESS.test(text.slice(0, j).slice(-9))\n ) {\n return j\n }\n }\n\n return text.length\n}\n\n/**\n * Returns the singular name and its plural. The counted word is the one before\n * \"of\", a leading container such as \"stalks\", or else the last word.\n */\nfunction countForms(\n name: string,\n many: boolean,\n): [name: string, plural: string | undefined] {\n const words = name.split(' ')\n let k = words.indexOf('of') - 1\n\n if (k < 0) {\n k = CONTAINER.test(words[0]!) ? 0 : words.length - 1\n }\n\n const word = words[k]!\n const other = many\n ? word.replace(\n /(?:(lea|hal|loa)ves|([rtdv])ies|(o|ch|sh|x|ss|chill?i)es|([^su])s)$/i,\n (_, f?: string, y?: string, es?: string, s?: string) =>\n f ? `${f}f` : y ? `${y}y` : (es ?? s)!,\n )\n : word.replace(\n /(?:(lea|hal|loa)f|([^aeiou])y|(ch|sh|x|z|tomato|potato|mango|chill?i)|[a-rt-z])$/i,\n (match, f?: string, y?: string, es?: string) =>\n f ? `${f}ves` : y ? `${y}ies` : es ? `${es}es` : `${match}s`,\n )\n\n if (other === word) {\n return [name, undefined]\n }\n\n words[k] = other\n const swapped = words.join(' ')\n return many ? [swapped, name] : [name, swapped]\n}\n"],"mappings":"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","names":["whole"]}
|
package/dist/issues.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/issues.ts"],"sourcesContent":["import type { RecipeIssue, RecipeIssueCode } from './errors'\n\nexport type Path = readonly (string | number)[]\n\nexport function warning(\n issues: RecipeIssue[],\n code: RecipeIssueCode,\n path: Path,\n message: string,\n suggestion?: string,\n): void {\n issues.push(issue('warning', code, path, message, suggestion))\n}\n\nexport function issue(\n severity: RecipeIssue['severity'],\n code: RecipeIssueCode,\n path: Path,\n message: string,\n suggestion?: string,\n): RecipeIssue {\n return {\n code,\n severity,\n path,\n message,\n ...(suggestion !== undefined && { suggestion }),\n }\n}\n\n/** A plain object: not `null`, not an array. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** `Object.hasOwn`, which the es2020 build target can't assume. */\nexport function hasOwn(record: object, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(record, key)\n}\n"],"mappings":";AAIO,SAAS,QACd,QACA,MACA,MACA,SACA,YACM;AACN,SAAO,KAAK,MAAM,WAAW,MAAM,MAAM,SAAS,UAAU,CAAC;AAC/D;AAEO,SAAS,MACd,UACA,MACA,MACA,SACA,YACa;AACb,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAI,eAAe,UAAa,EAAE,WAAW;AAAA,EAC/C;AACF;AAGO,SAAS,SAAS,OAAkD;AACzE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAGO,SAAS,OAAO,QAAgB,
|
|
1
|
+
{"version":3,"sources":["../src/issues.ts"],"sourcesContent":["import type { RecipeIssue, RecipeIssueCode } from './errors'\n\nexport type Path = readonly (string | number)[]\n\nexport function warning(\n issues: RecipeIssue[],\n code: RecipeIssueCode,\n path: Path,\n message: string,\n suggestion?: string,\n): void {\n issues.push(issue('warning', code, path, message, suggestion))\n}\n\nexport function issue(\n severity: RecipeIssue['severity'],\n code: RecipeIssueCode,\n path: Path,\n message: string,\n suggestion?: string,\n): RecipeIssue {\n return {\n code,\n severity,\n path,\n message,\n ...(suggestion !== undefined && { suggestion }),\n }\n}\n\n/** A plain object: not `null`, not an array. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** `Object.hasOwn`, which the es2020 build target can't assume. */\nexport function hasOwn(record: object, key: string | number): boolean {\n return Object.prototype.hasOwnProperty.call(record, key)\n}\n"],"mappings":";AAIO,SAAS,QACd,QACA,MACA,MACA,SACA,YACM;AACN,SAAO,KAAK,MAAM,WAAW,MAAM,MAAM,SAAS,UAAU,CAAC;AAC/D;AAEO,SAAS,MACd,UACA,MACA,MACA,SACA,YACa;AACb,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAI,eAAe,UAAa,EAAE,WAAW;AAAA,EAC/C;AACF;AAGO,SAAS,SAAS,OAAkD;AACzE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAGO,SAAS,OAAO,QAAgB,KAA+B;AACpE,SAAO,OAAO,UAAU,eAAe,KAAK,QAAQ,GAAG;AACzD;","names":[]}
|
package/dist/quantity/amount.js
CHANGED
|
@@ -44,6 +44,16 @@ function checkQuantity(fn, quantity) {
|
|
|
44
44
|
`${fn}: amount must be a number or a { min, max } range, received ${describeValue(amount)}.`
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
|
+
function checkResult(fn, amount) {
|
|
48
|
+
for (const value of amountEnds(amount)) {
|
|
49
|
+
if (!isPositive(value)) {
|
|
50
|
+
throw invalidArgument(
|
|
51
|
+
`${fn}: the result must be a finite number greater than 0, but it is ${describeValue(value)}.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return amount;
|
|
56
|
+
}
|
|
47
57
|
function checkFactor(fn, name, value) {
|
|
48
58
|
if (!isPositive(value)) {
|
|
49
59
|
throw invalidArgument(
|
|
@@ -61,6 +71,7 @@ export {
|
|
|
61
71
|
amountEnds,
|
|
62
72
|
checkFactor,
|
|
63
73
|
checkQuantity,
|
|
74
|
+
checkResult,
|
|
64
75
|
isPositive,
|
|
65
76
|
mapAmount
|
|
66
77
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/quantity/amount.ts"],"sourcesContent":["import { describeValue, RecipeBookError } from '../errors'\nimport type { Amount, Range } from './types'\n\n/** The amount type an operation returns for an input amount type. */\nexport type SameAmount<A extends Amount> = A extends number ? number : Range\n\nexport function mapAmount<A extends Amount>(\n amount: A,\n fn: (value: number) => number,\n): SameAmount<A> {\n return (\n typeof amount === 'number'\n ? fn(amount)\n : { min: fn(amount.min), max: fn(amount.max) }\n ) as SameAmount<A>\n}\n\nexport function amountEnds(amount: Amount): readonly number[] {\n return typeof amount === 'number' ? [amount] : [amount.min, amount.max]\n}\n\n/**\n * Validates a quantity passed to a public function.\n *\n * @throws {RecipeBookError} `invalid_argument`, naming `fn` and the fix.\n */\nexport function checkQuantity(fn: string, quantity: unknown): Amount {\n if (\n typeof quantity !== 'object' ||\n quantity === null ||\n !('amount' in quantity)\n ) {\n throw invalidArgument(\n `${fn}: expected a quantity such as { amount: 2, unit: \"cup\" }, received ${describeValue(quantity)}.`,\n )\n }\n\n if ((quantity as { max?: unknown }).max !== undefined) {\n throw invalidArgument(\n `${fn}: a quantity has no max; use amount: { min, max }, or formatServings or formatYield.`,\n )\n }\n\n const { amount } = quantity\n\n if (typeof amount === 'number') {\n if (!isPositive(amount)) {\n throw invalidArgument(\n `${fn}: amount must be a finite number greater than 0, received ${describeValue(amount)}.`,\n )\n }\n return amount\n }\n\n if (\n typeof amount === 'object' &&\n amount !== null &&\n 'min' in amount &&\n 'max' in amount\n ) {\n const { min, max } = amount\n if (!isPositive(min) || !isPositive(max)) {\n throw invalidArgument(\n `${fn}: range ends must be finite numbers greater than 0, received { min: ${describeValue(min)}, max: ${describeValue(max)} }.`,\n )\n }\n if (min >= max) {\n throw invalidArgument(\n `${fn}: range minimum (${min}) must be less than maximum (${max}). Use a plain number when both ends are equal.`,\n )\n }\n return { min, max }\n }\n\n throw invalidArgument(\n `${fn}: amount must be a number or a { min, max } range, received ${describeValue(amount)}.`,\n )\n}\n\n/**\n * Validates a multiplier passed to a public function.\n *\n * @throws {RecipeBookError} `invalid_argument`, naming `fn` and the parameter.\n */\nexport function checkFactor(fn: string, name: string, value: unknown): void {\n if (!isPositive(value)) {\n throw invalidArgument(\n `${fn}: ${name} must be a finite number greater than 0, received ${describeValue(value)}.`,\n )\n }\n}\n\nexport function isPositive(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0\n}\n\nfunction invalidArgument(message: string): RecipeBookError {\n return new RecipeBookError('invalid_argument', message)\n}\n"],"mappings":";AAAA,SAAS,eAAe,uBAAuB;AAMxC,SAAS,UACd,QACA,IACe;AACf,SACE,OAAO,WAAW,WACd,GAAG,MAAM,IACT,EAAE,KAAK,GAAG,OAAO,GAAG,GAAG,KAAK,GAAG,OAAO,GAAG,EAAE;AAEnD;AAEO,SAAS,WAAW,QAAmC;AAC5D,SAAO,OAAO,WAAW,WAAW,CAAC,MAAM,IAAI,CAAC,OAAO,KAAK,OAAO,GAAG;AACxE;AAOO,SAAS,cAAc,IAAY,UAA2B;AACnE,MACE,OAAO,aAAa,YACpB,aAAa,QACb,EAAE,YAAY,WACd;AACA,UAAM;AAAA,MACJ,GAAG,EAAE,sEAAsE,cAAc,QAAQ,CAAC;AAAA,IACpG;AAAA,EACF;AAEA,MAAK,SAA+B,QAAQ,QAAW;AACrD,UAAM;AAAA,MACJ,GAAG,EAAE;AAAA,IACP;AAAA,EACF;AAEA,QAAM,EAAE,OAAO,IAAI;AAEnB,MAAI,OAAO,WAAW,UAAU;AAC9B,QAAI,CAAC,WAAW,MAAM,GAAG;AACvB,YAAM;AAAA,QACJ,GAAG,EAAE,6DAA6D,cAAc,MAAM,CAAC;AAAA,MACzF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,MACE,OAAO,WAAW,YAClB,WAAW,QACX,SAAS,UACT,SAAS,QACT;AACA,UAAM,EAAE,KAAK,IAAI,IAAI;AACrB,QAAI,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,GAAG,GAAG;AACxC,YAAM;AAAA,QACJ,GAAG,EAAE,uEAAuE,cAAc,GAAG,CAAC,UAAU,cAAc,GAAG,CAAC;AAAA,MAC5H;AAAA,IACF;AACA,QAAI,OAAO,KAAK;AACd,YAAM;AAAA,QACJ,GAAG,EAAE,oBAAoB,GAAG,gCAAgC,GAAG;AAAA,MACjE;AAAA,IACF;AACA,WAAO,EAAE,KAAK,IAAI;AAAA,EACpB;AAEA,QAAM;AAAA,IACJ,GAAG,EAAE,+DAA+D,cAAc,MAAM,CAAC;AAAA,EAC3F;AACF;AAOO,SAAS,YAAY,IAAY,MAAc,OAAsB;AAC1E,MAAI,CAAC,WAAW,KAAK,GAAG;AACtB,UAAM;AAAA,MACJ,GAAG,EAAE,KAAK,IAAI,qDAAqD,cAAc,KAAK,CAAC;AAAA,IACzF;AAAA,EACF;AACF;AAEO,SAAS,WAAW,OAAiC;AAC1D,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,QAAQ;AACxE;AAEA,SAAS,gBAAgB,SAAkC;AACzD,SAAO,IAAI,gBAAgB,oBAAoB,OAAO;AACxD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/quantity/amount.ts"],"sourcesContent":["import { describeValue, RecipeBookError } from '../errors'\nimport type { Amount, Range } from './types'\n\n/** The amount type an operation returns for an input amount type. */\nexport type SameAmount<A extends Amount> = A extends number ? number : Range\n\nexport function mapAmount<A extends Amount>(\n amount: A,\n fn: (value: number) => number,\n): SameAmount<A> {\n return (\n typeof amount === 'number'\n ? fn(amount)\n : { min: fn(amount.min), max: fn(amount.max) }\n ) as SameAmount<A>\n}\n\nexport function amountEnds(amount: Amount): readonly number[] {\n return typeof amount === 'number' ? [amount] : [amount.min, amount.max]\n}\n\n/**\n * Validates a quantity passed to a public function.\n *\n * @throws {RecipeBookError} `invalid_argument`, naming `fn` and the fix.\n */\nexport function checkQuantity(fn: string, quantity: unknown): Amount {\n if (\n typeof quantity !== 'object' ||\n quantity === null ||\n !('amount' in quantity)\n ) {\n throw invalidArgument(\n `${fn}: expected a quantity such as { amount: 2, unit: \"cup\" }, received ${describeValue(quantity)}.`,\n )\n }\n\n if ((quantity as { max?: unknown }).max !== undefined) {\n throw invalidArgument(\n `${fn}: a quantity has no max; use amount: { min, max }, or formatServings or formatYield.`,\n )\n }\n\n const { amount } = quantity\n\n if (typeof amount === 'number') {\n if (!isPositive(amount)) {\n throw invalidArgument(\n `${fn}: amount must be a finite number greater than 0, received ${describeValue(amount)}.`,\n )\n }\n return amount\n }\n\n if (\n typeof amount === 'object' &&\n amount !== null &&\n 'min' in amount &&\n 'max' in amount\n ) {\n const { min, max } = amount\n if (!isPositive(min) || !isPositive(max)) {\n throw invalidArgument(\n `${fn}: range ends must be finite numbers greater than 0, received { min: ${describeValue(min)}, max: ${describeValue(max)} }.`,\n )\n }\n if (min >= max) {\n throw invalidArgument(\n `${fn}: range minimum (${min}) must be less than maximum (${max}). Use a plain number when both ends are equal.`,\n )\n }\n return { min, max }\n }\n\n throw invalidArgument(\n `${fn}: amount must be a number or a { min, max } range, received ${describeValue(amount)}.`,\n )\n}\n\n/**\n * Returns `amount`, a result of `fn`, unless an end overflowed to `Infinity`\n * or underflowed to 0.\n *\n * @throws {RecipeBookError} `invalid_argument`, naming `fn`.\n */\nexport function checkResult<A extends Amount>(fn: string, amount: A): A {\n for (const value of amountEnds(amount)) {\n if (!isPositive(value)) {\n throw invalidArgument(\n `${fn}: the result must be a finite number greater than 0, but it is ${describeValue(value)}.`,\n )\n }\n }\n return amount\n}\n\n/**\n * Validates a multiplier passed to a public function.\n *\n * @throws {RecipeBookError} `invalid_argument`, naming `fn` and the parameter.\n */\nexport function checkFactor(fn: string, name: string, value: unknown): void {\n if (!isPositive(value)) {\n throw invalidArgument(\n `${fn}: ${name} must be a finite number greater than 0, received ${describeValue(value)}.`,\n )\n }\n}\n\nexport function isPositive(value: unknown): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0\n}\n\nfunction invalidArgument(message: string): RecipeBookError {\n return new RecipeBookError('invalid_argument', message)\n}\n"],"mappings":";AAAA,SAAS,eAAe,uBAAuB;AAMxC,SAAS,UACd,QACA,IACe;AACf,SACE,OAAO,WAAW,WACd,GAAG,MAAM,IACT,EAAE,KAAK,GAAG,OAAO,GAAG,GAAG,KAAK,GAAG,OAAO,GAAG,EAAE;AAEnD;AAEO,SAAS,WAAW,QAAmC;AAC5D,SAAO,OAAO,WAAW,WAAW,CAAC,MAAM,IAAI,CAAC,OAAO,KAAK,OAAO,GAAG;AACxE;AAOO,SAAS,cAAc,IAAY,UAA2B;AACnE,MACE,OAAO,aAAa,YACpB,aAAa,QACb,EAAE,YAAY,WACd;AACA,UAAM;AAAA,MACJ,GAAG,EAAE,sEAAsE,cAAc,QAAQ,CAAC;AAAA,IACpG;AAAA,EACF;AAEA,MAAK,SAA+B,QAAQ,QAAW;AACrD,UAAM;AAAA,MACJ,GAAG,EAAE;AAAA,IACP;AAAA,EACF;AAEA,QAAM,EAAE,OAAO,IAAI;AAEnB,MAAI,OAAO,WAAW,UAAU;AAC9B,QAAI,CAAC,WAAW,MAAM,GAAG;AACvB,YAAM;AAAA,QACJ,GAAG,EAAE,6DAA6D,cAAc,MAAM,CAAC;AAAA,MACzF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,MACE,OAAO,WAAW,YAClB,WAAW,QACX,SAAS,UACT,SAAS,QACT;AACA,UAAM,EAAE,KAAK,IAAI,IAAI;AACrB,QAAI,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,GAAG,GAAG;AACxC,YAAM;AAAA,QACJ,GAAG,EAAE,uEAAuE,cAAc,GAAG,CAAC,UAAU,cAAc,GAAG,CAAC;AAAA,MAC5H;AAAA,IACF;AACA,QAAI,OAAO,KAAK;AACd,YAAM;AAAA,QACJ,GAAG,EAAE,oBAAoB,GAAG,gCAAgC,GAAG;AAAA,MACjE;AAAA,IACF;AACA,WAAO,EAAE,KAAK,IAAI;AAAA,EACpB;AAEA,QAAM;AAAA,IACJ,GAAG,EAAE,+DAA+D,cAAc,MAAM,CAAC;AAAA,EAC3F;AACF;AAQO,SAAS,YAA8B,IAAY,QAAc;AACtE,aAAW,SAAS,WAAW,MAAM,GAAG;AACtC,QAAI,CAAC,WAAW,KAAK,GAAG;AACtB,YAAM;AAAA,QACJ,GAAG,EAAE,kEAAkE,cAAc,KAAK,CAAC;AAAA,MAC7F;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,YAAY,IAAY,MAAc,OAAsB;AAC1E,MAAI,CAAC,WAAW,KAAK,GAAG;AACtB,UAAM;AAAA,MACJ,GAAG,EAAE,KAAK,IAAI,qDAAqD,cAAc,KAAK,CAAC;AAAA,IACzF;AAAA,EACF;AACF;AAEO,SAAS,WAAW,OAAiC;AAC1D,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,QAAQ;AACxE;AAEA,SAAS,gBAAgB,SAAkC;AACzD,SAAO,IAAI,gBAAgB,oBAAoB,OAAO;AACxD;","names":[]}
|
package/dist/quantity/parse.js
CHANGED
|
@@ -8,45 +8,9 @@ import { findUnit, suggestUnit } from "../units/lookup.js";
|
|
|
8
8
|
import { units } from "../units/registry.js";
|
|
9
9
|
import { asRecord, checkFields, error, toResult } from "../validation.js";
|
|
10
10
|
import { isPositive } from "./amount.js";
|
|
11
|
-
var
|
|
12
|
-
"\xBC": [1, 4],
|
|
13
|
-
"\xBD": [1, 2],
|
|
14
|
-
"\xBE": [3, 4],
|
|
15
|
-
"\u2150": [1, 7],
|
|
16
|
-
"\u2151": [1, 9],
|
|
17
|
-
"\u2152": [1, 10],
|
|
18
|
-
"\u2153": [1, 3],
|
|
19
|
-
"\u2154": [2, 3],
|
|
20
|
-
"\u2155": [1, 5],
|
|
21
|
-
"\u2156": [2, 5],
|
|
22
|
-
"\u2157": [3, 5],
|
|
23
|
-
"\u2158": [4, 5],
|
|
24
|
-
"\u2159": [1, 6],
|
|
25
|
-
"\u215A": [5, 6],
|
|
26
|
-
"\u215B": [1, 8],
|
|
27
|
-
"\u215C": [3, 8],
|
|
28
|
-
"\u215D": [5, 8],
|
|
29
|
-
"\u215E": [7, 8]
|
|
30
|
-
};
|
|
11
|
+
var VULGAR = "[\xBC-\xBE\u2150-\u215E]";
|
|
31
12
|
var ARTICLE = /^an?\s+(?=\D)/i;
|
|
32
|
-
var
|
|
33
|
-
function getPatterns() {
|
|
34
|
-
return patterns ?? (patterns = compilePatterns());
|
|
35
|
-
}
|
|
36
|
-
function compilePatterns() {
|
|
37
|
-
const vulgar = `[${Object.keys(VULGAR_FRACTIONS).join("")}]`;
|
|
38
|
-
const slash = "[/\u2044]";
|
|
39
|
-
const number = String.raw`(?:\d+\s+)?\d+\s*${slash}\s*\d+|(?:\d+\s*)?${vulgar}|\d+(?:\.\d+)?|\.\d+`;
|
|
40
|
-
return {
|
|
41
|
-
amount: new RegExp(
|
|
42
|
-
String.raw`^(${number})(?:\s*[-–—]\s*(${number})|\s+to\s+(${number}))?`,
|
|
43
|
-
"i"
|
|
44
|
-
),
|
|
45
|
-
asciiFraction: new RegExp(
|
|
46
|
-
String.raw`^(?:(\d+)\s+)?(\d+)\s*${slash}\s*(\d+)$`
|
|
47
|
-
)
|
|
48
|
-
};
|
|
49
|
-
}
|
|
13
|
+
var amountPattern;
|
|
50
14
|
function parseQuantity(text) {
|
|
51
15
|
const issues = [];
|
|
52
16
|
if (typeof text !== "string") {
|
|
@@ -63,7 +27,8 @@ function parseQuantity(text) {
|
|
|
63
27
|
}
|
|
64
28
|
function readQuantity(issues, value, path) {
|
|
65
29
|
if (typeof value === "string") {
|
|
66
|
-
|
|
30
|
+
const quantity = readQuantityText(issues, value, path);
|
|
31
|
+
return quantity && readTextAmount(issues, quantity.amount, path) ? quantity : void 0;
|
|
67
32
|
}
|
|
68
33
|
if (typeof value === "number") {
|
|
69
34
|
const amount = readPositive(issues, value, path);
|
|
@@ -71,7 +36,35 @@ function readQuantity(issues, value, path) {
|
|
|
71
36
|
}
|
|
72
37
|
const record = asRecord(value);
|
|
73
38
|
if (record) {
|
|
74
|
-
|
|
39
|
+
checkFields(issues, record, { amount: true, unit: true }, path);
|
|
40
|
+
const amount = readAmount(issues, record.amount, [...path, "amount"]);
|
|
41
|
+
const { unit } = record;
|
|
42
|
+
if (unit === void 0) {
|
|
43
|
+
return amount === void 0 ? void 0 : { amount };
|
|
44
|
+
}
|
|
45
|
+
if (typeof unit !== "string") {
|
|
46
|
+
return error(
|
|
47
|
+
issues,
|
|
48
|
+
"invalid_type",
|
|
49
|
+
[...path, "unit"],
|
|
50
|
+
`Expected a unit such as "g" or "cup", received ${describeValue(unit)}.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const id = findUnit(unit);
|
|
54
|
+
if (!id) {
|
|
55
|
+
const problem = unknownUnit(
|
|
56
|
+
unit,
|
|
57
|
+
amount === void 0 ? void 0 : typeof amount === "number" ? String(amount) : `{ amount: { min: ${amount.min}, max: ${amount.max} } }`
|
|
58
|
+
);
|
|
59
|
+
return error(
|
|
60
|
+
issues,
|
|
61
|
+
problem.code,
|
|
62
|
+
[...path, "unit"],
|
|
63
|
+
problem.message,
|
|
64
|
+
problem.suggestion
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return amount === void 0 ? void 0 : { amount, unit: id };
|
|
75
68
|
}
|
|
76
69
|
return error(
|
|
77
70
|
issues,
|
|
@@ -126,12 +119,18 @@ function matchAmount(text, allowZero) {
|
|
|
126
119
|
if (article) {
|
|
127
120
|
return { amount: 1, amountText: "1", rest: text.slice(article[0].length) };
|
|
128
121
|
}
|
|
129
|
-
|
|
122
|
+
if (!amountPattern) {
|
|
123
|
+
const number = String.raw`(?:\d+\s+)?\d+\s*[/⁄]\s*\d+|(?:\d+\s*)?${VULGAR}|\d+(?:\.\d+)?|\.\d+`;
|
|
124
|
+
amountPattern = new RegExp(
|
|
125
|
+
String.raw`^(${number})(?:(?:\s*[-–—]\s*|\s+to\s+)(${number}))?`,
|
|
126
|
+
"i"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const match = amountPattern.exec(text);
|
|
130
130
|
if (!match) {
|
|
131
131
|
return void 0;
|
|
132
132
|
}
|
|
133
|
-
const [matched, lowText = "",
|
|
134
|
-
const highText = highTextDash ?? highTextTo;
|
|
133
|
+
const [matched, lowText = "", highText] = match;
|
|
135
134
|
const low = toNumber(lowText);
|
|
136
135
|
const high = highText === void 0 ? low : toNumber(highText);
|
|
137
136
|
if (low === Infinity || high === Infinity) {
|
|
@@ -177,50 +176,25 @@ function unreadable(text) {
|
|
|
177
176
|
message: text === "" ? 'Expected a quantity such as "1 \xBD cups", received an empty string.' : `Couldn't read ${quote(text)}. Use a number or fraction, e.g. "1 \xBD cups".`
|
|
178
177
|
};
|
|
179
178
|
}
|
|
179
|
+
var fractions = /* @__PURE__ */ new Map();
|
|
180
180
|
function toNumber(text) {
|
|
181
|
-
const ascii =
|
|
181
|
+
const ascii = /^(?:(\d+)\s+)?(\d+)\s*[/⁄]\s*(\d+)$/.exec(text);
|
|
182
182
|
if (ascii) {
|
|
183
183
|
const [, whole = "0", numerator, denominator] = ascii;
|
|
184
184
|
return Number(whole) + Number(numerator) / (Number(denominator) || NaN);
|
|
185
185
|
}
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
const last = text.slice(-1);
|
|
187
|
+
if (last > "9") {
|
|
188
|
+
let fraction = fractions.get(last);
|
|
189
|
+
if (fraction === void 0) {
|
|
190
|
+
const [numerator, denominator] = last.normalize("NFKD").split("\u2044");
|
|
191
|
+
fraction = Number(numerator) / Number(denominator);
|
|
192
|
+
fractions.set(last, fraction);
|
|
193
|
+
}
|
|
194
|
+
return Number(text.slice(0, -1)) + fraction;
|
|
190
195
|
}
|
|
191
196
|
return Number(text);
|
|
192
197
|
}
|
|
193
|
-
function readQuantityObject(issues, record, path) {
|
|
194
|
-
checkFields(issues, record, { amount: true, unit: true }, path);
|
|
195
|
-
const amount = readAmount(issues, record.amount, [...path, "amount"]);
|
|
196
|
-
const { unit } = record;
|
|
197
|
-
if (unit === void 0) {
|
|
198
|
-
return amount === void 0 ? void 0 : { amount };
|
|
199
|
-
}
|
|
200
|
-
if (typeof unit !== "string") {
|
|
201
|
-
return error(
|
|
202
|
-
issues,
|
|
203
|
-
"invalid_type",
|
|
204
|
-
[...path, "unit"],
|
|
205
|
-
`Expected a unit such as "g" or "cup", received ${describeValue(unit)}.`
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
const id = findUnit(unit);
|
|
209
|
-
if (!id) {
|
|
210
|
-
const problem = unknownUnit(
|
|
211
|
-
unit,
|
|
212
|
-
amount === void 0 ? void 0 : typeof amount === "number" ? String(amount) : `{ amount: { min: ${amount.min}, max: ${amount.max} } }`
|
|
213
|
-
);
|
|
214
|
-
return error(
|
|
215
|
-
issues,
|
|
216
|
-
problem.code,
|
|
217
|
-
[...path, "unit"],
|
|
218
|
-
problem.message,
|
|
219
|
-
problem.suggestion
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
return amount === void 0 ? void 0 : { amount, unit: id };
|
|
223
|
-
}
|
|
224
198
|
function readAmount(issues, value, path) {
|
|
225
199
|
if (value === void 0) {
|
|
226
200
|
return error(
|
|
@@ -259,16 +233,19 @@ function readAmount(issues, value, path) {
|
|
|
259
233
|
);
|
|
260
234
|
}
|
|
261
235
|
function readPositive(issues, value, path) {
|
|
262
|
-
if (isPositive(value)) {
|
|
236
|
+
if (isPositive(value) && value >= 1e-300 && value <= 1e290) {
|
|
263
237
|
return value;
|
|
264
238
|
}
|
|
265
239
|
return error(
|
|
266
240
|
issues,
|
|
267
241
|
typeof value === "number" ? "invalid_amount" : "invalid_type",
|
|
268
242
|
path,
|
|
269
|
-
`Expected a positive number, received ${describeValue(value)}.`
|
|
243
|
+
isPositive(value) ? `Expected a number from 1e-300 to 1e290, received ${describeValue(value)}.` : `Expected a positive number, received ${describeValue(value)}.`
|
|
270
244
|
);
|
|
271
245
|
}
|
|
246
|
+
function readTextAmount(issues, amount, path) {
|
|
247
|
+
return (typeof amount === "number" ? [amount] : [amount.min, amount.max]).every((end) => readPositive(issues, end, path) !== void 0);
|
|
248
|
+
}
|
|
272
249
|
export {
|
|
273
250
|
matchAmount,
|
|
274
251
|
parseQuantity,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/quantity/parse.ts"],"sourcesContent":["import {\n describeValue,\n MAX_ECHOED,\n type ParseResult,\n quote,\n type RecipeIssue,\n type RecipeIssueCode,\n} from '../errors'\nimport type { Path } from '../issues'\nimport { findUnit, suggestUnit } from '../units/lookup'\nimport { units } from '../units/registry'\nimport { asRecord, checkFields, error, toResult } from '../validation'\nimport { isPositive } from './amount'\nimport type { Amount, Quantity } from './types'\n\nconst VULGAR_FRACTIONS: Readonly<Record<string, readonly [number, number]>> = {\n '¼': [1, 4],\n '½': [1, 2],\n '¾': [3, 4],\n '⅐': [1, 7],\n '⅑': [1, 9],\n '⅒': [1, 10],\n '⅓': [1, 3],\n '⅔': [2, 3],\n '⅕': [1, 5],\n '⅖': [2, 5],\n '⅗': [3, 5],\n '⅘': [4, 5],\n '⅙': [1, 6],\n '⅚': [5, 6],\n '⅛': [1, 8],\n '⅜': [3, 8],\n '⅝': [5, 8],\n '⅞': [7, 8],\n}\n\nconst ARTICLE = /^an?\\s+(?=\\D)/i\n\nlet patterns: ReturnType<typeof compilePatterns> | undefined\n\nfunction getPatterns() {\n return (patterns ??= compilePatterns())\n}\n\nfunction compilePatterns() {\n const vulgar = `[${Object.keys(VULGAR_FRACTIONS).join('')}]`\n const slash = '[/⁄]'\n // Alternatives are tried in order, so a fraction with an optional whole\n // number, as in \"1 1/2\", \"1/2\", \"1½\" or \"½\", comes before decimals and\n // integers.\n const number = String.raw`(?:\\d+\\s+)?\\d+\\s*${slash}\\s*\\d+|(?:\\d+\\s*)?${vulgar}|\\d+(?:\\.\\d+)?|\\.\\d+`\n\n return {\n amount: new RegExp(\n String.raw`^(${number})(?:\\s*[-–—]\\s*(${number})|\\s+to\\s+(${number}))?`,\n 'i',\n ),\n asciiFraction: new RegExp(\n String.raw`^(?:(\\d+)\\s+)?(\\d+)\\s*${slash}\\s*(\\d+)$`,\n ),\n }\n}\n\nexport type Problem = {\n code: RecipeIssueCode\n message: string\n suggestion?: string\n}\n\n/** An amount read from the start of text, as written, and the text after it. */\ntype AmountMatch = { amount: Amount; amountText: string; rest: string }\n\n/**\n * Parses quantity text, such as `'1 ½ cups'`, `'2-3 tbsp'`, `'a pinch'`, or\n * `'3'` for a bare count. Never throws.\n *\n * Accepts whole numbers, decimals, fractions (`1/2`, `½`, also with the\n * fraction slash `⁄`), mixed numbers (`1 1/2`, `1½`) and ranges (`2-3`, `2–3`,\n * `2 to 3`), each followed by an optional unit alias, and `a` or `an` for 1\n * before a unit alias, as in `'a pinch'`.\n *\n * @example\n * parseQuantity('1 ½ cups') // { success: true, data: { amount: 1.5, unit: 'cup' }, warnings: [] }\n *\n * const result = parseQuantity('2 cupz')\n * if (!result.success) {\n * result.error.message // '\"2 cupz\" has 1 problem:\\n ✖ (root) Unknown unit \"cupz\". Did you mean \"cups\"?\\nSee https://recipe-book.siraj.page/errors/invalid_quantity'\n * }\n */\nexport function parseQuantity(text: string): ParseResult<Quantity> {\n const issues: RecipeIssue[] = []\n\n if (typeof text !== 'string') {\n error(\n issues,\n 'invalid_type',\n [],\n `Expected quantity text such as \"1 ½ cups\", received ${describeValue(text)}.`,\n )\n return toResult<Quantity>('invalid_quantity', undefined, issues)\n }\n\n const quantity = readQuantityText(issues, text, [])\n return toResult('invalid_quantity', quantity, issues, quote(text))\n}\n\n/** Reads a quantity in any input form: text, a number, or `{ amount, unit }`. */\nexport function readQuantity(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Quantity | undefined {\n if (typeof value === 'string') {\n return readQuantityText(issues, value, path)\n }\n\n if (typeof value === 'number') {\n const amount = readPositive(issues, value, path)\n return amount === undefined ? undefined : { amount }\n }\n\n const record = asRecord(value)\n if (record) {\n return readQuantityObject(issues, record, path)\n }\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a quantity such as \"2 cups\", 2, or { amount: 2, unit: \"cup\" }, received ${describeValue(value)}.`,\n )\n}\n\nfunction readQuantityText(\n issues: RecipeIssue[],\n text: string,\n path: Path,\n): Quantity | undefined {\n const result = parseText(text.replace(/\\s+/g, ' ').trim())\n\n if ('code' in result) {\n return error(issues, result.code, path, result.message, result.suggestion)\n }\n\n return result\n}\n\nfunction parseText(text: string): Quantity | Problem {\n const match = matchAmount(text)\n\n if (!match) {\n const unit = findUnit(text)\n return unit\n ? {\n code: 'invalid_quantity',\n message: `${quote(text)} has no amount. Write a full quantity, e.g. \"1 ${units[unit].labels.short.one}\".`,\n }\n : unreadable(text)\n }\n\n if ('code' in match) {\n return match\n }\n\n const { amount, amountText, rest: unitText } = match\n\n if (unitText === '') {\n return { amount }\n }\n\n if (/\\d/.test(unitText) || !/\\p{L}/u.test(unitText)) {\n return unreadable(text)\n }\n\n const unit = findUnit(unitText)\n if (unit) {\n return { amount, unit }\n }\n\n return unknownUnit(\n unitText,\n amountText.length <= MAX_ECHOED && /^\\d+(?:\\.\\d+)?$/.test(amountText)\n ? amountText\n : quote(amountText),\n )\n}\n\n/**\n * Reads the amount that starts `text`, which is trimmed with single spaces: a\n * number, fraction, mixed number, range, or \"a\" or \"an\" for 1. A comma between\n * digits anywhere in the text, as in \"1,5\", is a problem. Otherwise returns\n * `undefined` when the text doesn't start with an amount, and a problem when\n * the amount is too large to read, isn't greater than 0 (0 is allowed with\n * `allowZero`), or is a range whose low end is above its high end.\n */\nexport function matchAmount(\n text: string,\n allowZero?: boolean,\n): AmountMatch | Problem | undefined {\n // \"1,5\" is a decimal in much of Europe and \"1,500\" a thousands separator in\n // the US, so neither is guessed.\n if (/\\d,\\d/.test(text)) {\n return {\n code: 'invalid_quantity',\n message: `Couldn't read ${quote(text)}. Write decimals with a period, e.g. \"1.5\", and leave out thousands separators.`,\n }\n }\n\n const article = ARTICLE.exec(text)\n if (article) {\n return { amount: 1, amountText: '1', rest: text.slice(article[0].length) }\n }\n\n const match = getPatterns().amount.exec(text)\n if (!match) {\n return undefined\n }\n\n const [matched, lowText = '', highTextDash, highTextTo] = match\n const highText = highTextDash ?? highTextTo\n const low = toNumber(lowText)\n const high = highText === undefined ? low : toNumber(highText)\n\n if (low === Infinity || high === Infinity) {\n return {\n code: 'invalid_amount',\n message: `${quote(text)} has an amount too large to read. Check it for extra digits.`,\n }\n }\n\n if (\n !(isPositive(low) || (allowZero && low === 0)) ||\n !(isPositive(high) || (allowZero && high === 0))\n ) {\n return {\n code: 'invalid_amount',\n message: `Expected a positive amount, received ${quote(text)}.`,\n }\n }\n\n if (low > high) {\n const mixedHint =\n /^\\d+$/.test(lowText) && high < 1\n ? ` To write a mixed number, use ${quote(`${lowText} ${highText}`)}.`\n : ''\n return {\n code: 'invalid_range',\n message: `Range minimum (${truncateNumber(lowText)}) must be less than maximum (${truncateNumber(highText!)}).${mixedHint}`,\n }\n }\n\n return {\n amount: low === high ? low : { min: low, max: high },\n amountText: matched,\n rest: text.slice(matched.length).trim(),\n }\n}\n\n/**\n * The `unknown_unit` problem. `quantity` is the line's quantity without the\n * unit, as it would be written in `RecipeInput`, for the \"move the word into\n * the name\" fix.\n */\nfunction unknownUnit(unitText: string, quantity: string | undefined): Problem {\n const suggestion = suggestUnit(unitText)\n const move =\n quantity === undefined\n ? 'move the word into the name'\n : `move the word into the name: { name: ${quote(unitText)}, quantity: ${quantity} }`\n\n return {\n code: 'unknown_unit',\n message: suggestion\n ? `Unknown unit ${quote(unitText)}. Did you mean \"${suggestion}\"?`\n : `Unknown unit ${quote(unitText)}. Use a known unit, or ${move}.`,\n ...(suggestion && { suggestion }),\n }\n}\n\nfunction truncateNumber(text: string): string {\n return text.length > 20 ? `${text.slice(0, 19)}…` : text\n}\n\nfunction unreadable(text: string): Problem {\n return {\n code: 'invalid_quantity',\n message:\n text === ''\n ? 'Expected a quantity such as \"1 ½ cups\", received an empty string.'\n : `Couldn't read ${quote(text)}. Use a number or fraction, e.g. \"1 ½ cups\".`,\n }\n}\n\nfunction toNumber(text: string): number {\n const ascii = getPatterns().asciiFraction.exec(text)\n if (ascii) {\n const [, whole = '0', numerator, denominator] = ascii\n // A zero denominator reads as NaN, not Infinity, so \"1/0\" isn't too large.\n return Number(whole) + Number(numerator) / (Number(denominator) || NaN)\n }\n\n const vulgar = VULGAR_FRACTIONS[text.slice(-1)]\n if (vulgar) {\n const [numerator, denominator] = vulgar\n // Before a glyph is a whole number and any spaces, or nothing, which\n // Number reads as 0.\n return Number(text.slice(0, -1)) + numerator / denominator\n }\n\n return Number(text)\n}\n\nfunction readQuantityObject(\n issues: RecipeIssue[],\n record: Record<string, unknown>,\n path: Path,\n): Quantity | undefined {\n checkFields(issues, record, { amount: true, unit: true }, path)\n\n const amount = readAmount(issues, record.amount, [...path, 'amount'])\n const { unit } = record\n\n if (unit === undefined) {\n return amount === undefined ? undefined : { amount }\n }\n\n if (typeof unit !== 'string') {\n return error(\n issues,\n 'invalid_type',\n [...path, 'unit'],\n `Expected a unit such as \"g\" or \"cup\", received ${describeValue(unit)}.`,\n )\n }\n\n const id = findUnit(unit)\n\n if (!id) {\n const problem = unknownUnit(\n unit,\n amount === undefined\n ? undefined\n : typeof amount === 'number'\n ? String(amount)\n : `{ amount: { min: ${amount.min}, max: ${amount.max} } }`,\n )\n return error(\n issues,\n problem.code,\n [...path, 'unit'],\n problem.message,\n problem.suggestion,\n )\n }\n\n return amount === undefined ? undefined : { amount, unit: id }\n}\n\nfunction readAmount(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Amount | undefined {\n if (value === undefined) {\n return error(\n issues,\n 'missing_field',\n path,\n 'Add an amount, e.g. { amount: 2, unit: \"cup\" }.',\n )\n }\n\n if (typeof value === 'number') {\n return readPositive(issues, value, path)\n }\n\n const range = asRecord(value)\n if (range && 'min' in range && 'max' in range) {\n checkFields(issues, range, { min: true, max: true }, path)\n\n const min = readPositive(issues, range.min, [...path, 'min'])\n const max = readPositive(issues, range.max, [...path, 'max'])\n\n if (min === undefined || max === undefined) {\n return undefined\n }\n\n if (min > max) {\n return error(\n issues,\n 'invalid_range',\n path,\n `Range minimum (${min}) must be less than maximum (${max}).`,\n )\n }\n\n return min === max ? min : { min, max }\n }\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a number or { min, max }, received ${describeValue(value)}.`,\n )\n}\n\nexport function readPositive(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): number | undefined {\n if (isPositive(value)) {\n return value\n }\n\n return error(\n issues,\n typeof value === 'number' ? 'invalid_amount' : 'invalid_type',\n path,\n `Expected a positive number, received ${describeValue(value)}.`,\n )\n}\n"],"mappings":"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","names":["unit"]}
|
|
1
|
+
{"version":3,"sources":["../../src/quantity/parse.ts"],"sourcesContent":["import {\n describeValue,\n MAX_ECHOED,\n type ParseResult,\n quote,\n type RecipeIssue,\n type RecipeIssueCode,\n} from '../errors'\nimport type { Path } from '../issues'\nimport { findUnit, suggestUnit } from '../units/lookup'\nimport { units } from '../units/registry'\nimport { asRecord, checkFields, error, toResult } from '../validation'\nimport { isPositive } from './amount'\nimport type { Amount, Quantity } from './types'\n\n// ¼, ½, ¾ and ⅐ to ⅞, which decompose to their fractions, such as '½' to\n// '1⁄2'. ⅟ has no denominator, and ↉ is zero.\nconst VULGAR = '[¼-¾⅐-⅞]'\n\nconst ARTICLE = /^an?\\s+(?=\\D)/i\n\nlet amountPattern: RegExp | undefined\n\nexport type Problem = {\n code: RecipeIssueCode\n message: string\n suggestion?: string\n}\n\n/**\n * Parses quantity text, such as `'1 ½ cups'`, `'2-3 tbsp'`, `'a pinch'`, or\n * `'3'` for a bare count. Never throws.\n *\n * Accepts whole numbers, decimals, fractions (`1/2`, `½`, also with the\n * fraction slash `⁄`), mixed numbers (`1 1/2`, `1½`) and ranges (`2-3`, `2–3`,\n * `2 to 3`), each followed by an optional unit alias, and `a` or `an` for 1\n * before a unit alias, as in `'a pinch'`.\n *\n * @example\n * parseQuantity('1 ½ cups') // { success: true, data: { amount: 1.5, unit: 'cup' }, warnings: [] }\n *\n * const result = parseQuantity('2 cupz')\n * if (!result.success) {\n * result.error.message // '\"2 cupz\" has 1 problem:\\n ✖ (root) Unknown unit \"cupz\". Did you mean \"cups\"?\\nSee https://recipe-book.siraj.page/errors/invalid_quantity'\n * }\n */\nexport function parseQuantity(text: string): ParseResult<Quantity> {\n const issues: RecipeIssue[] = []\n\n if (typeof text !== 'string') {\n error(\n issues,\n 'invalid_type',\n [],\n `Expected quantity text such as \"1 ½ cups\", received ${describeValue(text)}.`,\n )\n return toResult<Quantity>('invalid_quantity', undefined, issues)\n }\n\n const quantity = readQuantityText(issues, text, [])\n return toResult('invalid_quantity', quantity, issues, quote(text))\n}\n\n/** Reads a quantity in any input form: text, a number, or `{ amount, unit }`. */\nexport function readQuantity(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Quantity | undefined {\n if (typeof value === 'string') {\n const quantity = readQuantityText(issues, value, path)\n return quantity && readTextAmount(issues, quantity.amount, path)\n ? quantity\n : undefined\n }\n\n if (typeof value === 'number') {\n const amount = readPositive(issues, value, path)\n return amount === undefined ? undefined : { amount }\n }\n\n const record = asRecord(value)\n if (record) {\n checkFields(issues, record, { amount: true, unit: true }, path)\n\n const amount = readAmount(issues, record.amount, [...path, 'amount'])\n const { unit } = record\n\n if (unit === undefined) {\n return amount === undefined ? undefined : { amount }\n }\n\n if (typeof unit !== 'string') {\n return error(\n issues,\n 'invalid_type',\n [...path, 'unit'],\n `Expected a unit such as \"g\" or \"cup\", received ${describeValue(unit)}.`,\n )\n }\n\n const id = findUnit(unit)\n\n if (!id) {\n const problem = unknownUnit(\n unit,\n amount === undefined\n ? undefined\n : typeof amount === 'number'\n ? String(amount)\n : `{ amount: { min: ${amount.min}, max: ${amount.max} } }`,\n )\n return error(\n issues,\n problem.code,\n [...path, 'unit'],\n problem.message,\n problem.suggestion,\n )\n }\n\n return amount === undefined ? undefined : { amount, unit: id }\n }\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a quantity such as \"2 cups\", 2, or { amount: 2, unit: \"cup\" }, received ${describeValue(value)}.`,\n )\n}\n\nfunction readQuantityText(\n issues: RecipeIssue[],\n text: string,\n path: Path,\n): Quantity | undefined {\n const result = parseText(text.replace(/\\s+/g, ' ').trim())\n\n if ('code' in result) {\n return error(issues, result.code, path, result.message, result.suggestion)\n }\n\n return result\n}\n\nfunction parseText(text: string): Quantity | Problem {\n const match = matchAmount(text)\n\n if (!match) {\n const unit = findUnit(text)\n return unit\n ? {\n code: 'invalid_quantity',\n message: `${quote(text)} has no amount. Write a full quantity, e.g. \"1 ${units[unit].labels.short.one}\".`,\n }\n : unreadable(text)\n }\n\n if ('code' in match) {\n return match\n }\n\n const { amount, amountText, rest: unitText } = match\n\n if (unitText === '') {\n return { amount }\n }\n\n if (/\\d/.test(unitText) || !/\\p{L}/u.test(unitText)) {\n return unreadable(text)\n }\n\n const unit = findUnit(unitText)\n if (unit) {\n return { amount, unit }\n }\n\n return unknownUnit(\n unitText,\n amountText.length <= MAX_ECHOED && /^\\d+(?:\\.\\d+)?$/.test(amountText)\n ? amountText\n : quote(amountText),\n )\n}\n\n/**\n * Reads the amount that starts `text`, which is trimmed with single spaces: a\n * number, fraction, mixed number, range, or \"a\" or \"an\" for 1. A comma between\n * digits anywhere in the text, as in \"1,5\", is a problem. Otherwise returns\n * `undefined` when the text doesn't start with an amount, and a problem when\n * the amount is too large to read, isn't greater than 0 (0 is allowed with\n * `allowZero`), or is a range whose low end is above its high end. When it\n * reads one, it returns the amount, its text as written, and the text after it.\n */\nexport function matchAmount(\n text: string,\n allowZero?: boolean,\n): { amount: Amount; amountText: string; rest: string } | Problem | undefined {\n // \"1,5\" is a decimal in much of Europe and \"1,500\" a thousands separator in\n // the US, so neither is guessed.\n if (/\\d,\\d/.test(text)) {\n return {\n code: 'invalid_quantity',\n message: `Couldn't read ${quote(text)}. Write decimals with a period, e.g. \"1.5\", and leave out thousands separators.`,\n }\n }\n\n const article = ARTICLE.exec(text)\n if (article) {\n return { amount: 1, amountText: '1', rest: text.slice(article[0].length) }\n }\n\n if (!amountPattern) {\n // Alternatives are tried in order, so a fraction with an optional whole\n // number, as in \"1 1/2\", \"1/2\", \"1½\" or \"½\", comes before decimals and\n // integers.\n const number = String.raw`(?:\\d+\\s+)?\\d+\\s*[/⁄]\\s*\\d+|(?:\\d+\\s*)?${VULGAR}|\\d+(?:\\.\\d+)?|\\.\\d+`\n amountPattern = new RegExp(\n String.raw`^(${number})(?:(?:\\s*[-–—]\\s*|\\s+to\\s+)(${number}))?`,\n 'i',\n )\n }\n\n const match = amountPattern.exec(text)\n if (!match) {\n return undefined\n }\n\n const [matched, lowText = '', highText] = match\n const low = toNumber(lowText)\n const high = highText === undefined ? low : toNumber(highText)\n\n if (low === Infinity || high === Infinity) {\n return {\n code: 'invalid_amount',\n message: `${quote(text)} has an amount too large to read. Check it for extra digits.`,\n }\n }\n\n if (\n !(isPositive(low) || (allowZero && low === 0)) ||\n !(isPositive(high) || (allowZero && high === 0))\n ) {\n return {\n code: 'invalid_amount',\n message: `Expected a positive amount, received ${quote(text)}.`,\n }\n }\n\n if (low > high) {\n const mixedHint =\n /^\\d+$/.test(lowText) && high < 1\n ? ` To write a mixed number, use ${quote(`${lowText} ${highText}`)}.`\n : ''\n return {\n code: 'invalid_range',\n message: `Range minimum (${truncateNumber(lowText)}) must be less than maximum (${truncateNumber(highText!)}).${mixedHint}`,\n }\n }\n\n return {\n amount: low === high ? low : { min: low, max: high },\n amountText: matched,\n rest: text.slice(matched.length).trim(),\n }\n}\n\n/**\n * The `unknown_unit` problem. `quantity` is the line's quantity without the\n * unit, as it would be written in `RecipeInput`, for the \"move the word into\n * the name\" fix.\n */\nfunction unknownUnit(unitText: string, quantity: string | undefined): Problem {\n const suggestion = suggestUnit(unitText)\n const move =\n quantity === undefined\n ? 'move the word into the name'\n : `move the word into the name: { name: ${quote(unitText)}, quantity: ${quantity} }`\n\n return {\n code: 'unknown_unit',\n message: suggestion\n ? `Unknown unit ${quote(unitText)}. Did you mean \"${suggestion}\"?`\n : `Unknown unit ${quote(unitText)}. Use a known unit, or ${move}.`,\n ...(suggestion && { suggestion }),\n }\n}\n\nfunction truncateNumber(text: string): string {\n return text.length > 20 ? `${text.slice(0, 19)}…` : text\n}\n\nfunction unreadable(text: string): Problem {\n return {\n code: 'invalid_quantity',\n message:\n text === ''\n ? 'Expected a quantity such as \"1 ½ cups\", received an empty string.'\n : `Couldn't read ${quote(text)}. Use a number or fraction, e.g. \"1 ½ cups\".`,\n }\n}\n\n// Decomposing a glyph is slow, so each glyph's value is read once.\nconst fractions = new Map<string, number>()\n\nfunction toNumber(text: string): number {\n // The `[/⁄]` must match the one in `amountPattern`.\n const ascii = /^(?:(\\d+)\\s+)?(\\d+)\\s*[/⁄]\\s*(\\d+)$/.exec(text)\n if (ascii) {\n const [, whole = '0', numerator, denominator] = ascii\n // A zero denominator reads as NaN, not Infinity, so \"1/0\" isn't too large.\n return Number(whole) + Number(numerator) / (Number(denominator) || NaN)\n }\n\n // Amounts end in a digit or a glyph.\n const last = text.slice(-1)\n if (last > '9') {\n let fraction = fractions.get(last)\n if (fraction === undefined) {\n const [numerator, denominator] = last.normalize('NFKD').split('⁄')\n fraction = Number(numerator) / Number(denominator)\n fractions.set(last, fraction)\n }\n // Before a glyph is a whole number and any spaces, or nothing, which\n // Number reads as 0.\n return Number(text.slice(0, -1)) + fraction\n }\n\n return Number(text)\n}\n\nfunction readAmount(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Amount | undefined {\n if (value === undefined) {\n return error(\n issues,\n 'missing_field',\n path,\n 'Add an amount, e.g. { amount: 2, unit: \"cup\" }.',\n )\n }\n\n if (typeof value === 'number') {\n return readPositive(issues, value, path)\n }\n\n const range = asRecord(value)\n if (range && 'min' in range && 'max' in range) {\n checkFields(issues, range, { min: true, max: true }, path)\n\n const min = readPositive(issues, range.min, [...path, 'min'])\n const max = readPositive(issues, range.max, [...path, 'max'])\n\n if (min === undefined || max === undefined) {\n return undefined\n }\n\n if (min > max) {\n return error(\n issues,\n 'invalid_range',\n path,\n `Range minimum (${min}) must be less than maximum (${max}).`,\n )\n }\n\n return min === max ? min : { min, max }\n }\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a number or { min, max }, received ${describeValue(value)}.`,\n )\n}\n\n/**\n * Reads a number from 1e-300 to 1e290. Scaling by 1e-6 to 1e6 and converting\n * between any two units in the registry keeps an amount in that range finite\n * and greater than 0.\n */\nexport function readPositive(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): number | undefined {\n if (isPositive(value) && value >= 1e-300 && value <= 1e290) {\n return value\n }\n\n return error(\n issues,\n typeof value === 'number' ? 'invalid_amount' : 'invalid_type',\n path,\n isPositive(value)\n ? `Expected a number from 1e-300 to 1e290, received ${describeValue(value)}.`\n : `Expected a positive number, received ${describeValue(value)}.`,\n )\n}\n\n/** Checks each end of an amount read from text with `readPositive`. */\nfunction readTextAmount(\n issues: RecipeIssue[],\n amount: Amount,\n path: Path,\n): boolean {\n return (\n typeof amount === 'number' ? [amount] : [amount.min, amount.max]\n ).every((end) => readPositive(issues, end, path) !== undefined)\n}\n"],"mappings":"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","names":["unit"]}
|
package/dist/quantity/scale.js
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
import {
|
|
3
3
|
checkFactor,
|
|
4
4
|
checkQuantity,
|
|
5
|
+
checkResult,
|
|
5
6
|
mapAmount
|
|
6
7
|
} from "./amount.js";
|
|
7
8
|
function scaleQuantity(quantity, factor) {
|
|
8
|
-
checkQuantity("scaleQuantity", quantity);
|
|
9
|
+
const amount = checkQuantity("scaleQuantity", quantity);
|
|
9
10
|
checkFactor("scaleQuantity", "factor", factor);
|
|
10
|
-
|
|
11
|
+
const result = multiply({ amount, unit: quantity.unit }, factor);
|
|
12
|
+
checkResult("scaleQuantity", result.amount);
|
|
13
|
+
return result;
|
|
11
14
|
}
|
|
12
15
|
function multiply(quantity, factor) {
|
|
13
16
|
const amount = mapAmount(quantity.amount, (value) => value * factor);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/quantity/scale.ts"],"sourcesContent":["import type { UnitId } from '../units/registry'\nimport {\n checkFactor,\n checkQuantity,\n mapAmount,\n type SameAmount,\n} from './amount'\nimport type { Amount, Quantity } from './types'\n\n/**\n * Multiplies a quantity by `factor`. Both ends of a range scale, the unit is\n * kept, and nothing is rounded.\n *\n * @throws {RecipeBookError} `invalid_argument` when `factor` is not a finite\n * number greater than 0, `quantity` isn't a quantity object, the amount is\n * not a finite number greater than 0 or a range of them, a range's `min`\n * isn't less than its `max`, or the quantity has `max` set, as a `Servings`\n * or `Yield` has. `resolveRecipe` scales those. The unit isn't checked.\n *\n * @example\n * scaleQuantity({ amount: 1.5, unit: 'cup' }, 2) // { amount: 3, unit: 'cup' }\n * scaleQuantity({ amount: { min: 2, max: 3 }, unit: 'tbsp' }, 2) // { amount: { min: 4, max: 6 }, unit: 'tbsp' }\n */\nexport function scaleQuantity<A extends Amount>(\n quantity: Quantity & { amount: A },\n factor: number,\n): { amount: SameAmount<A>; unit?: UnitId | undefined } {\n checkQuantity('scaleQuantity', quantity)\n checkFactor('scaleQuantity', 'factor', factor)\n\n
|
|
1
|
+
{"version":3,"sources":["../../src/quantity/scale.ts"],"sourcesContent":["import type { UnitId } from '../units/registry'\nimport {\n checkFactor,\n checkQuantity,\n checkResult,\n mapAmount,\n type SameAmount,\n} from './amount'\nimport type { Amount, Quantity } from './types'\n\n/**\n * Multiplies a quantity by `factor`. Both ends of a range scale, the unit is\n * kept, and nothing is rounded.\n *\n * @throws {RecipeBookError} `invalid_argument` when `factor` is not a finite\n * number greater than 0, `quantity` isn't a quantity object, the amount is\n * not a finite number greater than 0 or a range of them, a range's `min`\n * isn't less than its `max`, or the quantity has `max` set, as a `Servings`\n * or `Yield` has. `resolveRecipe` scales those. The unit isn't checked.\n * Also thrown when an end times `factor` overflows to `Infinity` or\n * underflows to 0: past about 1.8e308, or below about 5e-324.\n *\n * @example\n * scaleQuantity({ amount: 1.5, unit: 'cup' }, 2) // { amount: 3, unit: 'cup' }\n * scaleQuantity({ amount: { min: 2, max: 3 }, unit: 'tbsp' }, 2) // { amount: { min: 4, max: 6 }, unit: 'tbsp' }\n */\nexport function scaleQuantity<A extends Amount>(\n quantity: Quantity & { amount: A },\n factor: number,\n): { amount: SameAmount<A>; unit?: UnitId | undefined } {\n const amount = checkQuantity('scaleQuantity', quantity) as A\n checkFactor('scaleQuantity', 'factor', factor)\n\n const result = multiply({ amount, unit: quantity.unit }, factor)\n checkResult('scaleQuantity', result.amount)\n return result\n}\n\n/** {@link scaleQuantity} without argument checks. */\nexport function multiply<A extends Amount>(\n quantity: Quantity & { amount: A },\n factor: number,\n): { amount: SameAmount<A>; unit?: UnitId | undefined } {\n const amount = mapAmount(quantity.amount, (value) => value * factor)\n return quantity.unit === undefined\n ? { amount }\n : { amount, unit: quantity.unit }\n}\n"],"mappings":";AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAmBA,SAAS,cACd,UACA,QACsD;AACtD,QAAM,SAAS,cAAc,iBAAiB,QAAQ;AACtD,cAAY,iBAAiB,UAAU,MAAM;AAE7C,QAAM,SAAS,SAAS,EAAE,QAAQ,MAAM,SAAS,KAAK,GAAG,MAAM;AAC/D,cAAY,iBAAiB,OAAO,MAAM;AAC1C,SAAO;AACT;AAGO,SAAS,SACd,UACA,QACsD;AACtD,QAAM,SAAS,UAAU,SAAS,QAAQ,CAAC,UAAU,QAAQ,MAAM;AACnE,SAAO,SAAS,SAAS,SACrB,EAAE,OAAO,IACT,EAAE,QAAQ,MAAM,SAAS,KAAK;AACpC;","names":[]}
|