Skip to content

Quantity components render in random order when units cannot be combined #441

Description

@dubadub

When an ingredient has two quantities in units that cannot be combined, they are rendered in random order — different on every run, and different between output formats within the same run.

Reproduction

mkdir -p config
printf 'Mix @flour{1%%cup} with more @flour{100%%g}.\n' > bake.cook
printf '[baking]\nflour\n' > config/aisle.conf

for i in 1 2 3 4 5 6; do cook shopping-list bake.cook | grep flour; done
flour 1 cup, 100 g
flour 100 g, 1 cup
flour 1 cup, 100 g
flour 100 g, 1 cup
flour 1 cup, 100 g
flour 100 g, 1 cup

Cause

cooklang::quantity::GroupedQuantity stores unconvertible units in a HashMap<String, Quantity>, and Rust randomises HashMap iteration order per process. Every consumer that walks GroupedQuantity::iter() or into_vec() inherits that.

Why it appeared now

#433 removed the bundled_units feature so quantities keep their authored units. Previously there was a unit database, cup and g were known units, and they merged into a single quantity — so ordering never came up. With the feature off, both components survive and the randomised map becomes visible.

#433's own updated snapshots did not catch it because none of their fixtures has an ingredient with two inconvertible units.

Scope — wider than the shopping list

Each of these walked the map independently, so they could disagree with each other in a single run:

  • cook shopping-list — human table, Markdown, JSON and YAML
  • cook recipe — ingredient table and cookware amounts (human, Markdown, LaTeX, Typst, JSON-LD)
  • GET /api/recipes/...grouped_ingredients[].quantities was in random order per request
  • The web UI's ingredient lists

Anything diffing CookCLI output between runs, or comparing the API response to what the CLI prints, sees spurious differences.

Suggested direction

Sort the components at every render/serialise boundary. Ordering by unit name with the unitless component first is deterministic, platform-independent (byte-wise str comparison, no locale), and easy to state. Declaration order is not recoverable — the HashMap has already lost it.

Notes

Fixed in the cookcli-core extraction branch (#434), which routes all writers through one ordering helper and adds a 64-iteration regression test using units no converter knows, so it exercises the randomised map regardless of feature flags.

This is the fourth instance of HashMap iteration order reaching user-visible output in this codebase — see also #427.

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions