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
12 changes: 10 additions & 2 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,22 @@ export default defineConfig({
{ text: "Quick start", link: "/api/#quick-start" },
{ text: "Cooklang specs", link: "/guide-cooklang-specs" },
{ text: "Extensions", link: "/guide-extensions" },
{ text: "Units and conversions", link: "/guide-units" },
{ text: "Unit conversion", link: "/guide-unit-conversion" },
],
collapsed: true
},
{
text: "API",
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
items: typedocSidebar,
items: [
{
text: "Reference",
items: [
{
text: "Units definition", link: "/reference-units"
}
],
}, ...typedocSidebar],
collapsed: true
},
{
Expand Down
2 changes: 1 addition & 1 deletion docs/guide-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ One of more prefixes can be added before the ingredient name (`@<modifiers>name{

Use case: `Add @water{1%L} first, and than again @&water{100%mL}` will create one "water" ingredient with a quantity of 1.1L

- The quantities will be added to the ingredient having the same name existing in the `ingredients` list, according to the rules explained in the [units guide](/guide-units)
- The quantities will be added to the ingredient having the same name existing in the `ingredients` list, according to the rules explained in the [unit conversion guide](/guide-unit-conversion)
- The quantity for this specific instance will be saved as part of the item
- If the referenced ingredient is not found or if the quantities cannot be added, a new ingredient will be created

Expand Down
88 changes: 88 additions & 0 deletions docs/guide-unit-conversion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
outline: deep
---

# Guide: unit conversion

For units definition, see [Units Reference](/reference-units).

## Automatic unit selection and conversion

When quantities are added together (e.g., from [referenced ingredients](/guide-extensions.html#reference-to-an-existing-ingredient)), the parser selects the most appropriate unit for the result. This does **not** apply to individual quantities—`@flour{500%g}` will always parse as `500 g`.

### Specifying a unit system

You can specify a unit system in your recipe metadata to control how ambiguous units are resolved:

```cooklang
---
unit system: UK
---
Add @water{1%cup} and some more @&water{1%fl-oz}
```

Valid values (case insensitive) are: `metric`, `US`, `UK`, `JP` (see [Unit Reference Table](#unit-reference-table) above)

When no `unit system` is specified:
- Units with a **metric** definition (like `tsp`, `tbsp`) default to metric
- Units without a metric definition (like `cup`, `pint`) default to US

### System selection

The target system depends on the input units and recipe metadata:

1. **Recipe has `unit system` metadata** → Use the specified system. Example with `unit system: UK`: `1%cup` + `1%fl-oz` becomes `11%fl-oz`

Otherwise:

2. **One unit is metric** → Convert to metric. Example: `1%lb` + `500%g` becomes `954%g`

3. **Both units are ambiguous and US-compatible** → Use US system. Example: `1%cup` + `1%fl-oz` becomes `9%fl-oz`

4. **Different non-metric systems** → Convert to metric. Example: `1%go` + `1%cup` becomes `417%ml`

5. **Incompatible units** (e.g., text values, or volume and mass) → Quantities won't be added and will be kept separate.

### Unit selection algorithm

Once the system is determined, the best unit is selected based on:

1. **Candidates units**:
- Units that belong to that system are considered potential candidates for best unit. The JP system also includes all the metric units. Certain units are disabled as not commonly used, by setting `isBestUnit` to false (default: true)
- The units of the input quantities are restored into that list, as they are actually already used in the recipe.

2. **Valid range**: A value is considered "in range" for a unit if:
- It's between 1 and the unit's `maxValue` (default: 999), OR
- It's less than 1 but can be approximated as a fraction (for units with fractions enabled)

::: info Example: fraction-aware selection
With US units, a value of 1.7 ml (~0.345 tsp) will select `tsp` because:
- 0.345 ≈ 1/3, which is a valid fraction (denominator 3 is allowed)
- `tsp` has `fractions.enabled: true`
- Therefore 0.345 tsp is considered "in range" and is the smallest valid option
:::

3. **Selection priority** (among in-range candidates):
- Smallest integer in the input unit family. Examples:
- `1 cup + 1 cup` -> `2 cup` and not 1 pint
- `0.5 pint + 0.5 pint` -> `1 pint` and not 2 cup
- `2 cup + 1 pint` -> `2 pint` and not 4 cup
- Smallest integers in any compatible family
- Smallest non-integer value in range

4. **Fallback**: If no candidate is in range, the unit closest to the valid range is selected. This is in particular used for potential edge cases with values above 999 liters or 999 gallons.

The per-unit configuration is detailed in the [Units Reference](/reference-units#units-configuration)

## Full-recipe unit conversion

It is also possible to convert an entire recipe into a specific unit system, using the [`convertTo()`](/api/classes/Recipe.html#convertto) method of the Recipe instance which returns a new Recipe with the specific conversion applied.

```typescript
function convertTo(unit: SpecificUnitSystem, method: method: "keep" | "replace" | "remove"): Recipe
```

There are three modes for full-recipe unit conversion:
- `keep` will keep existing equivalents, and add the equivalent in the specified system
- `replace` will replace whichever equivalent was used for conversion, and keep the other equivalents
- `remove` will only leave the equivalent in the specified system and remove all others
103 changes: 10 additions & 93 deletions docs/guide-units.md → docs/reference-units.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@
outline: deep
---

# Guide: units and conversion
# Reference: Units

When adding quantities of [referenced ingredients](/guide-extensions.html#reference-to-an-existing-ingredient) together for the ingredients list (i.e the [ingredients](/api/classes/Recipe.html#ingredients) properties of a `Recipe`), the parser tries its best to add apples to apples.
## Units definitions


## Unit reference table

The following table shows all recognized units:
The following table shows all recognized units. Some units like `cup`, `tsp`, and `tbsp` have different sizes depending on the measurement system. These are marked as **ambiguous** and have system-specific conversion factors in the `toBaseBySystem` column.

### Mass

Expand All @@ -20,7 +17,9 @@ The following table shows all recognized units:
| oz | mass | ambiguous | ounce, ounces | 28.3495 | US: 28.3495, UK: 28.3495 |
| lb | mass | ambiguous | pound, pounds | 453.592 | US: 453.592, UK: 453.592 |

### Volume (Metric)
### Volume

#### Metric

| Name | Type | System | Aliases | To Base (default) | To Base by System |
| ---- | ------ | ------ | ---------------------------------------------------- | ----------------- | ----------------- |
Expand All @@ -29,20 +28,20 @@ The following table shows all recognized units:
| dl | volume | metric | deciliter, deciliters, decilitre, decilitres | 100 | |
| l | volume | metric | liter, liters, litre, litres | 1000 | |

### Volume (JP)
#### JP

| Name | Type | System | Aliases | To Base (default) | To Base by System |
| ---- | ------ | ------ | -------------------- | ----------------- | ----------------- |
| go | volume | JP | gou, goo, 合, rice cup | 180 | |

### Volume (Ambiguous: metric/US/UK)
#### Ambiguous: metric/US/UK

| Name | Type | System | Aliases | To Base (default) | To Base by System |
| ---- | ------ | --------- | ---------------------- | ----------------- | ----------------------------------- |
| tsp | volume | ambiguous | teaspoon, teaspoons | 5 (metric) | metric: 5, US: 4.929, UK: 5.919 |
| tbsp | volume | ambiguous | tablespoon, tablespoons | 15 (metric) | metric: 15, US: 14.787, UK: 17.758 |

### Volume (Ambiguous: US/UK only)
#### Ambiguous: US/UK only

| Name | Type | System | Aliases | To Base (default) | To Base by System |
| ------ | ------ | --------- | ------------------------- | ----------------- | ------------------------ |
Expand All @@ -58,89 +57,7 @@ The following table shows all recognized units:
| ----- | ----- | ------ | ---------- | ----------------- | ----------------- |
| piece | count | metric | pieces, pc | 1 | |

## Ambiguous units

Some units like `cup`, `tsp`, and `tbsp` have different sizes depending on the measurement system. These are marked as **ambiguous** and have system-specific conversion factors in the `toBaseBySystem` column.

## Specifying a unit system

You can specify a unit system in your recipe metadata to control how ambiguous units are resolved:

```cooklang
---
unit system: UK
---
Add @water{1%cup} and some more @&water{1%fl-oz}
```

Valid values (case insensitive) are: `metric`, `US`, `UK`, `JP` (see [Unit Reference Table](#unit-reference-table) above)

When no `unit system` is specified:
- Units with a **metric** definition (like `tsp`, `tbsp`) default to metric
- Units without a metric definition (like `cup`, `pint`) default to US

## Adding quantities

When quantities are added together (e.g., from [referenced ingredients](/guide-extensions.html#reference-to-an-existing-ingredient)), the parser selects the most appropriate unit for the result. This does **not** apply to individual quantities—`@flour{500%g}` will always parse as `500 g`.

### System selection

The target system depends on the input units and recipe metadata:

1. **Recipe has `unit system` metadata** → Use the specified system. Example with `unit system: UK`: `1%cup` + `1%fl-oz` becomes `11%fl-oz`

Otherwise:

2. **One unit is metric** → Convert to metric. Example: `1%lb` + `500%g` becomes `954%g`

3. **Both units are ambiguous and US-compatible** → Use US system. Example: `1%cup` + `1%fl-oz` becomes `9%fl-oz`

4. **Different non-metric systems** → Convert to metric. Example: `1%go` + `1%cup` becomes `417%ml`

5. **Incompatible units** (e.g., text values, or volume and mass) → Quantities won't be added and will be kept separate.

### Unit selection algorithm

Once the system is determined, the best unit is selected based on:

1. **Candidates units**:
- Units that belong to that system are considered potential candidates for best unit. The JP system also includes all the metric units. Certain units are disabled as not commonly used, by setting `isBestUnit` to false (default: true)
- The units of the input quantities are restored into that list, as they are actually already used in the recipe.

2. **Valid range**: A value is considered "in range" for a unit if:
- It's between 1 and the unit's `maxValue` (default: 999), OR
- It's less than 1 but can be approximated as a fraction (for units with fractions enabled)

::: info Example: fraction-aware selection
With US units, a value of 1.7 ml (~0.345 tsp) will select `tsp` because:
- 0.345 ≈ 1/3, which is a valid fraction (denominator 3 is allowed)
- `tsp` has `fractions.enabled: true`
- Therefore 0.345 tsp is considered "in range" and is the smallest valid option
:::

3. **Selection priority** (among in-range candidates):
- Smallest integer in the input unit family. Examples:
- `1 cup + 1 cup` -> `2 cup` and not 1 pint
- `0.5 pint + 0.5 pint` -> `1 pint` and not 2 cup
- `2 cup + 1 pint` -> `2 pint` and not 4 cup
- Smallest integers in any compatible family
- Smallest non-integer value in range

4. **Fallback**: If no candidate is in range, the unit closest to the valid range is selected. This is in particular used for potential edge cases with values above 999 liters or 999 gallons.

### Per-unit configuration

Each unit can have custom configuration:

| Config | Description | Default |
| ------ | ----------- | ------- |
| `isBestUnit` | Whether a unit is eligible for best unit | true |
| `maxValue` | Maximum value before upgrading to a larger unit | 999 |
| `fractions.enabled` | Whether to approximate decimals as fractions | false |
| `fractions.denominators` | Allowed denominators for fraction approximation | [2, 3, 4, 8] |
| `fractions.maxWhole` | Maximum whole number in mixed fraction | 4 |

Complete configuration for all units.
## Units configuration

| Unit | maxValue | fractions.enabled | fractions.denominators | isBestUnit |
| ------ | -------- | ----------------- | ---------------------- | ---------- |
Expand Down
53 changes: 53 additions & 0 deletions playground/app/components/recipe/RecipeChoices.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
Recipe,
RecipeChoices,
IngredientAlternative,
SpecificUnitSystem,
} from "cooklang-parser";
import { formatItemQuantity } from "cooklang-parser";

Expand All @@ -12,6 +13,30 @@ const props = defineProps<{

const servings = defineModel<number>("servings", { required: true });
const choices = defineModel<RecipeChoices>("choices", { required: true });
const unitSystem = defineModel<SpecificUnitSystem | null>("unitSystem", {
required: true,
});
const conversionMethod = defineModel<"keep" | "replace" | "remove">(
"conversionMethod",
{ required: true },
);

// Unit conversion options
const unitSystems: { label: string; value: SpecificUnitSystem | null }[] = [
{ label: "None", value: null },
{ label: "Metric", value: "metric" },
{ label: "US", value: "US" },
{ label: "UK", value: "UK" },
{ label: "Japan", value: "JP" },
];
const conversionMethods: {
label: string;
value: "keep" | "replace" | "remove";
}[] = [
{ label: "Keep original as equivalent", value: "keep" },
{ label: "Replace original", value: "replace" },
{ label: "Remove equivalents", value: "remove" },
];

// Reset servings when recipe's base servings change
watch(
Expand Down Expand Up @@ -161,6 +186,34 @@ function setSelectedGrouped(groupKey: string, value: number | undefined) {
</p>
</div>

<!-- Unit Conversion Section -->
<div class="flex flex-col gap-3">
<h3 class="text-sm font-semibold">Convert Units</h3>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<label class="text-xs font-medium text-gray-700 dark:text-gray-300"
>Target system:</label
>
<USelectMenu
v-model="unitSystem"
:items="unitSystems"
value-key="value"
class="w-32"
/>
</div>
<div v-if="unitSystem" class="flex flex-col gap-2">
<label class="text-xs font-medium text-gray-700 dark:text-gray-300"
>Conversion method:</label
>
<URadioGroup
v-model="conversionMethod"
:items="conversionMethods"
value-key="value"
/>
</div>
</div>
</div>

<!-- Ingredient Choices Section -->
<div class="flex flex-col gap-4">
<h3 class="text-sm font-semibold">Possible Ingredient Choices</h3>
Expand Down
Loading
Loading