Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/e/codes/COOKLANG_E9401.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
---
code: COOKLANG_E9401
level: error(fatal)
title: Text quantity cannot be added
title: Ratio cannot be computed
---

## What this error means

A quantity with a text value (e.g. `a pinch`) cannot be added to another quantity. This aborts the whole `parse()`/`scaleBy()`/`convertTo()` call; it is not collected alongside other diagnostics.
The ratio between two quantities cannot be computed because one of them is not numeric. This aborts the whole `parse()`/`scaleBy()`/`convertTo()` call; it is not collected alongside other diagnostics.

## Common causes

- The same ingredient (without alternatives) appears more than once, once with a text quantity and once with a numeric quantity of a compatible unit, e.g. `@salt{a pinch%tsp}` then `@&salt{2%tsp}`
- Merging equivalents where one of the equivalent quantities is text-only, e.g. `@flour{1%cup|a bit}` then `@&flour{2%cup|a lot}`

## How to fix it

Use numeric quantities throughout when the same ingredient's quantities need to be summed, or use `&`-less separate entries / alternatives if they are not meant to be added together.
Use numeric quantities for every equivalent that needs to be merged with another occurrence of the same ingredient.
22 changes: 18 additions & 4 deletions docs/e/codes/COOKLANG_E9402.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
---
code: COOKLANG_E9402
level: error(fatal)
title: Ratio cannot be computed
title: Division by zero
---

## What this error means

The ratio between two quantities cannot be computed because one of them is not numeric. This aborts the whole `parse()`/`scaleBy()`/`convertTo()` call; it is not collected alongside other diagnostics.
A fraction in the recipe has a zero denominator, so its value is undefined.
The parser stops immediately rather than continuing with a partial parse.

## Common causes

- Merging equivalents where one of the equivalent quantities is text-only, e.g. `@flour{1%cup|a bit}` then `@&flour{2%cup|a lot}`
- Writing a fraction such as `{1/0}`

## How to fix it

Use numeric quantities for every equivalent that needs to be merged with another occurrence of the same ingredient.
```text
-- Wrong
Add @flour{1/0}
-- Correct
Add @flour{1/2}
```

Use a nonzero denominator.

## Example output

```
error[COOKLANG_E1003]: A fraction cannot have a zero denominator.
```
31 changes: 0 additions & 31 deletions docs/e/codes/COOKLANG_E9403.md

This file was deleted.

17 changes: 17 additions & 0 deletions docs/e/codes/COOKLANG_W0401.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
code: COOKLANG_W0401
level: warning
title: Text quantities kept separate
---

## What this warning means

A numeric quantity and a text-valued quantity were found for the same ingredient. They cannot be added together, so they are kept separate.

## Common causes

- Referencing an ingredient with both a numeric quantity and a descriptive quantity, such as `@salt{2}` and `@&salt{a pinch}`, or `@salt{2%g}` and `@&salt{a pinch}`

## How to fix it

No change is needed if both quantities are intentional; they remain available separately. If they should be summed, use numeric quantities for both.
6 changes: 3 additions & 3 deletions docs/guide-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ Parse-time diagnostics, reported by `Recipe.parse()`:
| [`COOKLANG_E0301`](/e/cooklang_e0301) | error | Metadata indentation contains a tab. |
| [`COOKLANG_E0302`](/e/cooklang_e0302) | error | Metadata indentation levels are inconsistent. |
| [`COOKLANG_W0301`](/e/cooklang_w0301) | warning | `servings` or `serves` is not a number. |
| [`COOKLANG_E9401`](/e/cooklang_e9401) | error (fatal) | A text quantity (e.g. "a pinch") could not be merged with another occurrence of the same ingredient. |
| [`COOKLANG_E9402`](/e/cooklang_e9402) | error (fatal) | A text equivalent could not be merged with another occurrence of the same ingredient. |
| [`COOKLANG_E9403`](/e/cooklang_e9403) | error (fatal) | A fraction has a zero denominator; parsing stops immediately. |
| [`COOKLANG_E9401`](/e/cooklang_e9401) | error (fatal) | A text equivalent could not be merged with another occurrence of the same ingredient. |
| [`COOKLANG_E9402`](/e/cooklang_e9402) | error (fatal) | A fraction has a zero denominator; parsing stops immediately. |
| [`COOKLANG_W0401`](/e/cooklang_w0401) | warning | A text quantity could not be added to a numeric quantity and was kept separate. |

Warnings never make `parse()` throw. Non-fatal errors (`E0…`) are collected
alongside everything else so that the whole recipe gets reported in one
Expand Down
43 changes: 36 additions & 7 deletions src/classes/recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,13 +436,15 @@ export class Recipe {
try {
return parseQuantityValue(quantity);
} catch (error) {
// v8 ignore else -- @preserve: DivisionByZeroError is the only possible error
if (error instanceof DivisionByZeroError) {
throw this._toParseStoppedError(
error,
makeSpan(this._lineOffsets, lineIdx, column, length),
);
} else {
throw error;
}
throw error;
}
}

Expand Down Expand Up @@ -878,15 +880,21 @@ export class Recipe {
*
* @internal
*/
private _populateIngredientQuantities(): void {
private _populateIngredientQuantities(collectDiagnostics = false): void {
// Reset quantities and usedAsPrimary flag
for (const ing of this.ingredients) {
delete ing.quantities;
delete ing.usedAsPrimary;
}

// Get ingredients with quantities using default (no explicit choice = primary with alternatives)
const ingredientsWithQuantities = this.getIngredientQuantities();
const { ingredients: ingredientsWithQuantities, textMixCount } =
this._computeIngredientQuantities({});
if (collectDiagnostics) {
for (let i = 0; i < textMixCount; i++) {
this.diagnostics.push(createDiagnostic("COOKLANG_W0401", {}));
}
}

// Track which indices have been matched (for handling duplicate names)
const matchedIndices = new Set<number>();
Expand Down Expand Up @@ -1414,6 +1422,19 @@ export class Recipe {
getIngredientQuantities(
options?: GetIngredientQuantitiesOptions,
): Ingredient[] {
return this._computeIngredientQuantities(options).ingredients;
}

private _computeIngredientQuantities(
options?: GetIngredientQuantitiesOptions,
): {
ingredients: Ingredient[];
textMixCount: number;
} {
let textMixCount = 0;
const onTextQuantityMix = () => {
textMixCount++;
};
const {
ingredientGroups,
selectedIndices,
Expand Down Expand Up @@ -1456,7 +1477,11 @@ export class Recipe {
for (const [, group] of groupsForIng) {
const summed =
group.quantities.length > 0
? addEquivalentsAndSimplify(group.quantities, this.unitSystem)
? addEquivalentsAndSimplify(
group.quantities,
this.unitSystem,
onTextQuantityMix,
)
: undefined;
const flattened = summed ? flattenPlainUnitGroup(summed) : [];

Expand All @@ -1470,7 +1495,11 @@ export class Recipe {
index: altIdx,
...(altQtys.length > 0 && {
quantities: flattenPlainUnitGroup(
addEquivalentsAndSimplify(altQtys, this.unitSystem),
addEquivalentsAndSimplify(
altQtys,
this.unitSystem,
onTextQuantityMix,
),
).flatMap(
/* v8 ignore next -- item.and branch requires complex nested AND-with-equivalents structure */
(item) => ("quantity" in item ? [item] : item.and),
Expand Down Expand Up @@ -1513,7 +1542,7 @@ export class Recipe {
result.push(ing);
}

return result;
return { ingredients: result, textMixCount };
}

/**
Expand Down Expand Up @@ -2043,7 +2072,7 @@ export class Recipe {
this.choices.variants = [...allVariants];
}

this._populateIngredientQuantities();
this._populateIngredientQuantities(true);

// Diagnostics are reported in source order (sort is stable)
this.diagnostics.sort(
Expand Down
8 changes: 4 additions & 4 deletions src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ export const diagnosticCatalog = defineDiagnostics({
`Cannot add quantities with incompatible or unknown units: ${p.unit1} and ${p.unit2}`,
fix: `This should not be reachable from normal library usage. Please report it with a reproduction.`,
},
COOKLANG_E9401: {
why: `Cannot add a quantity with a text value.`,
COOKLANG_W0401: {
why: `Text-valued quantities could not be combined and were kept separate.`,
fix: `Use numeric quantities if they need to be added together.`,
},
COOKLANG_E9402: {
COOKLANG_E9401: {
why: `One of both values is not a number, so a ratio cannot be computed`,
fix: `Use numeric quantities.`,
},
COOKLANG_E9403: {
COOKLANG_E9402: {
why: `A fraction cannot have a zero denominator.`,
fix: `Use a nonzero denominator, e.g. 1/2.`,
},
Expand Down
6 changes: 3 additions & 3 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class DivisionByZeroError extends SingleDiagnosticError {
readonly fraction: string;

constructor(fraction: string) {
super(createDiagnostic("COOKLANG_E9403", {}));
super(createDiagnostic("COOKLANG_E9402", {}));
this.name = "DivisionByZeroError";
this.fraction = fraction;
}
Expand Down Expand Up @@ -213,7 +213,7 @@ export class BadIndentationError extends SingleDiagnosticError {
*/
export class CannotAddTextValueError extends SingleDiagnosticError {
constructor() {
super(createDiagnostic("COOKLANG_E9401", {}));
super(createDiagnostic("COOKLANG_W0401", {}));
this.name = "CannotAddTextValueError";
}
}
Expand All @@ -239,7 +239,7 @@ export class IncompatibleUnitsError extends SingleDiagnosticError {
*/
export class RatioNotComputableError extends SingleDiagnosticError {
constructor() {
super(createDiagnostic("COOKLANG_E9402", {}));
super(createDiagnostic("COOKLANG_E9401", {}));
this.name = "RatioNotComputableError";
}
}
Expand Down
33 changes: 31 additions & 2 deletions src/quantities/alternatives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export function addQuantitiesOrGroups(
QuantityWithExtendedUnit | FlatOrGroup<QuantityWithExtendedUnit>
)[],
system?: SpecificUnitSystem,
onTextQuantityMix?: () => void,
): {
sum: QuantityWithUnitDef | FlatAndGroup<QuantityWithUnitDef>;
unitsLists: QuantityWithUnitDef[][];
Expand Down Expand Up @@ -283,7 +284,30 @@ export function addQuantitiesOrGroups(
// Step 3: calculate the sum
const sum: QuantityWithUnitDef[] = [];
for (const nextQ of reducedQuantities) {
const existingQ = findCompatibleQuantityWithinList(sum, nextQ);
const nextIsText =
nextQ.quantity.type === "fixed" && nextQ.quantity.value.type === "text";
// Any numeric/text mix is reported, whatever the units
if (
sum.some(
(q) =>
(q.quantity.type === "fixed" && q.quantity.value.type === "text") !==
nextIsText,
)
) {
onTextQuantityMix?.();
}

// Text quantities are never merged, so only numeric entries are candidates
const existingQ = nextIsText
? undefined
: findCompatibleQuantityWithinList(
sum.filter(
(q) =>
!(q.quantity.type === "fixed" && q.quantity.value.type === "text"),
),
nextQ,
);

if (existingQ === undefined) {
sum.push({
...nextQ,
Expand Down Expand Up @@ -400,12 +424,17 @@ export function addEquivalentsAndSimplify(
QuantityWithExtendedUnit | FlatOrGroup<QuantityWithExtendedUnit>
)[],
system?: SpecificUnitSystem,
onTextQuantityMix?: () => void,
): QuantityWithPlainUnit | MaybeNestedGroup<QuantityWithPlainUnit> {
if (quantities.length === 1) {
return toPlainUnit(quantities[0]!);
}
// Step 1+2+3: find equivalents, reduce groups and add quantities
const { sum, unitsLists } = addQuantitiesOrGroups(quantities, system);
const { sum, unitsLists } = addQuantitiesOrGroups(
quantities,
system,
onTextQuantityMix,
);
// Step 4: regroup and expand equivalents per group
const regrouped = regroupQuantitiesAndExpandEquivalents(
sum,
Expand Down
Loading
Loading