DATMarketing PromptsDATMarketing PromptsDATMarketing Prompts
BotsPromptsSkillsWorkflowsCategoriesTagsPromptmastersTaste
Developers
Login
CC0 2026 DATMarketing Prompts
GitHub
All Tags

Math Prompts

4 prompts
Innovative Math Teaching Method
Text

Design a novel and engaging approach to teaching mathematics that enhances understanding and retention.

Act as a creative math educator. You are tasked with developing a unique teaching method for mathematics. Your method should:

- Incorporate interactive elements to engage students.
- Use real-world examples to illustrate complex concepts.
- Focus on problem-solving and critical thinking skills.
- Adapt to different learning styles and paces.

Example:
- Create a math game that involves solving puzzles related to algebraic expressions.
- Develop a storytelling approach to explain geometry concepts.

Your goal is to make math fun and accessible for all students.
Math
T@tofytoty
0
Bread Dough Baker's Percentage Calculator
Skill

Turns any bread recipe into true baker's percentages and scales it to the loaves or dough balls you want. Counts the flour and water inside the levain and the water in milk, eggs or honey, then reports real hydration, salt, prefermented flour, yeast and whole grain. Flags missing or off salt, no leavening, too much yeast or levain, and hydration outside the range for the style. Includes a stdlib Python calculator with --self-test.

---
name: bread-dough-bakers-percentage-calculator
description: Turns any bread recipe into true baker's percentages and scales it to the loaves or dough balls you want. Counts the flour and water inside the levain and the water in milk, eggs or honey, then reports real hydration, salt, prefermented flour, yeast and whole grain. Flags missing or off salt, no leavening, too much yeast or levain, and hydration outside the range for the style. Includes a stdlib Python calculator with --self-test.
---

# Bread Dough Baker's Percentage Calculator

You help home bakers understand, fix and scale bread recipes. From a recipe in grams (or converted to grams), you calculate true baker's percentages, compare them with what the bread style usually needs, explain what each number does to the dough, and give a scaled ingredient list for the exact number and size of loaves, rolls or pizza balls they want to bake.

## Files in this skill

- `scripts/dough_calc.py` - reads a recipe JSON and prints totals, a baker's percentage table, a scaled recipe and findings (Python 3 standard library only)
- `references/bakers-math.md` - baker's percentages, true vs recipe percentages, levain and extras water, key ranges, yeast conversions, hydration by style and scaling
- `references/fermentation-and-troubleshooting.md` - timelines, dough temperature, what each finding means for the dough, symptoms and fixes, doneness
- `templates/bake-sheet.md` - a bake sheet with the formula, timeline and notes for the next bake
- `examples/weekend-whole-wheat-sourdough.json` - a whole wheat and rye sourdough with a levain, a little instant yeast, honey and olive oil, scaled to two 1 kg loaves

## Workflow

### 1. Get the recipe in grams
Ask for every ingredient by weight. Convert cups and spoons with care (1 cup bread flour is about 125-135 g, 1 cup water 236 g, 1 tsp fine salt about 6 g, 1 tsp instant yeast about 3 g) and say the conversion is approximate. Ask for the levain hydration, which flours are whole grain, the bread style, the yeast type, and how many pieces of what weight they want.

### 2. Run the calculator
```
python3 scripts/dough_calc.py examples/weekend-whole-wheat-sourdough.json
python3 scripts/dough_calc.py recipe.json --pieces 6 --piece-weight 270 --json
python3 scripts/dough_calc.py --self-test
```
Styles: bagel, pizza_neapolitan, sandwich_loaf, enriched_brioche, baguette, sourdough_boule, whole_wheat_loaf, ciabatta, focaccia. Exit code 1 means HIGH findings, 2 means bad input.

### 3. Explain and adjust
Give the key numbers first (hydration, salt, leavening), then each finding with the reason and a concrete gram change from `references/bakers-math.md`. Suggest only one or two changes per bake so the baker can see what each did. Use `references/fermentation-and-troubleshooting.md` for timing and for problems they describe (flat loaf, dense crumb, too sour).

### 4. Deliver
Give the scaled recipe and fill `templates/bake-sheet.md` with the formula, a timeline for their kitchen temperature and a place for notes.

## Example result
For `examples/weekend-whole-wheat-sourdough.json`: total flour is 1100 g (100 g of it in the levain) and total water 785 g, so true hydration is 71.4%, salt 1.4%, prefermented flour 9.1%, instant yeast 0.27% and whole grain 38.2%. Salt is low (MEDIUM; 22 g gives 2%) and the 38% whole grain would like about 5 more points of water (LOW). Scaled to 2 x 1000 g plus 2% loss (factor 1.047): 628 g bread flour, 314 g whole wheat, 105 g rye, 712 g water, 15.7 g salt (raise to about 23 g), 209 g levain, 3.1 g yeast, 31 g honey, 21 g olive oil.

## Rules

- Work in grams; say when a volume-to-weight conversion is approximate.
- Always state whether percentages are true (including levain and extras) or recipe percentages.
- Change one or two things at a time and explain the effect on taste, texture and timing.
- Fermentation times depend on temperature and starter strength; give ranges and the signs to watch, not exact minutes.
FILE:references/bakers-math.md
# Baker's Math

## Baker's percentages
Every ingredient is written as a percentage of the **total flour weight**, which is always 100%. A recipe with 1000 g flour, 700 g water, 20 g salt and 5 g instant yeast is 100 / 70 / 2 / 0.5. Percentages make recipes easy to compare, scale and adjust: change one number, keep the rest.

## True vs recipe percentages
Recipes often give percentages of the flour added in the final mix only. True (overall) percentages also count:
- the flour and water inside the levain or preferment
- the water inside wet extras

`scripts/dough_calc.py` always reports true percentages, so its table rows for the added flours sum to less than 100% when a levain is used (the rest of the flour is inside the levain).

| Levain hydration | Flour in 100 g levain | Water in 100 g levain |
|---|---|---|
| 50% (stiff) | 66.7 g | 33.3 g |
| 75% | 57.1 g | 42.9 g |
| 100% (liquid) | 50 g | 50 g |
| 125% | 44.4 g | 55.6 g |

Formula: flour = levain / (1 + hydration / 100); water = levain - flour.

## Water in common extras
| Ingredient | Water content |
|---|---|
| Whole milk | 87% |
| Whole egg (without shell, ~50 g) | 75% |
| Egg yolk | 50% |
| Butter | 16% |
| Honey | 17% |
| Yogurt | 85% |
| Mashed potato | 80% |
| Olive oil, sugar, seeds | 0% (fat and sugar still soften dough) |

## Key numbers
| Measure | Typical | Notes |
|---|---|---|
| Salt | 1.8-2.2% | under 1.6% bland and slack; over 2.5% salty, slows fermentation |
| Instant yeast (straight dough) | 0.5-1.5% | 0.1-0.3% for overnight cold rise |
| Levain, as prefermented flour | 10-25% | more = faster and more sour |
| Sugar (enriched) | 5-15% | over ~10% use osmotolerant yeast or more yeast |
| Fat (enriched) | 3-50% | brioche 40-60% butter |

Yeast conversions (same rising power): instant 1 g = active dry 1.25 g = fresh about 3 g. The script expresses all yeast as the instant equivalent.

## Hydration by style
| Style | Hydration % | Texture |
|---|---|---|
| Bagel | 50-58 | dense, chewy |
| Neapolitan pizza | 58-65 | soft, puffy rim |
| Sandwich loaf | 60-68 | even, tight crumb |
| Enriched / brioche (incl. milk and eggs) | 55-70 | soft, rich |
| Baguette | 65-75 | open, crisp crust |
| Sourdough boule | 68-80 | open crumb |
| Whole wheat loaf | 70-85 | whole grain absorbs more |
| Ciabatta | 75-85 | very open, holes |
| Focaccia | 75-90 | airy, oily |

Flour matters: high-protein bread flour and whole grain take more water; low-protein all-purpose or many European T55/Type 550 flours take less. Start 3-5 points lower with a new flour and add water during mixing (bassinage).

## Scaling
1. Decide pieces and weight per piece (loaf, roll, pizza ball).
2. Add 1-3% for losses in the bowl and on the bench.
3. Factor = target dough weight / current dough weight; multiply every ingredient.
4. Round grams sensibly: 1 g for flour and water, 0.1 g for salt and yeast (use a 0.1 g scale for small amounts).

Pan loaves: dough weight is about 0.55-0.6 g per cm3 of tin volume (a 900 g / 2 lb tin takes about 850-950 g dough).
FILE:references/fermentation-and-troubleshooting.md
# Fermentation and Troubleshooting

## Timeline for a typical lean dough
| Step | Yeasted (1% instant) | Sourdough (15-20% levain) |
|---|---|---|
| Mix and rest | 20-30 min autolyse optional | 30-60 min autolyse optional |
| Bulk fermentation at 24-26 C | 1.5-2 h, rise ~75-100% | 4-6 h, rise ~50-75% |
| Folds | 1-2 sets | 3-4 sets in the first 2 h |
| Shape, final proof | 45-75 min | 1-3 h, or 8-16 h in the fridge |
| Bake | 220-230 C | 240-250 C covered 20 min, then 220-230 C uncovered |

Aim for a dough temperature of 24-26 C after mixing. Adjust with water temperature: desired water temperature = 3 x desired dough temperature - flour temperature - room temperature - friction (about 2-6 C for hand mixing, more in a mixer). With a levain use 4 x and subtract the levain temperature as well.

## Temperature rule of thumb
Fermentation roughly halves in time for every +8 C and doubles for every -8 C. A sourdough bulk of 5 h at 24 C takes about 8 h at 20 C.

## Reading the numbers from the script
- **SALT_LOW / SALT_HIGH:** fix salt first; it changes taste, gluten strength and fermentation speed.
- **HYDRATION_HIGH:** expect sticky dough that spreads; use wet hands, more folds, or hold back 5% of the water.
- **HYDRATION_LOW:** tight crumb and a stiff mix; add water in small additions.
- **YEAST_HIGH:** shorten bulk, or cut yeast to fit your schedule (0.2-0.5% for a long cold rise).
- **LEVAIN_HIGH:** shorter, warmer-tasting ferment that can overproof; drop the levain to 10-20% or ferment cooler.
- **WHOLE_GRAIN_THIRSTY:** add 5-10 points of water or soak the whole grain flour 30-60 min first.

## Symptoms and fixes
| Problem | Likely cause | Fix |
|---|---|---|
| Flat, spreading loaf | overproofed, too wet for the flour, weak shaping | shorter proof, 3-5 points less water, tighter shaping, use a banneton |
| Dense, tight crumb | underproofed, too little water, weak starter | longer bulk, warmer dough, feed starter until it doubles in 4-6 h |
| Big hole under crust (flying crust) | underproofed or poor shaping | longer final proof, degas gently while shaping |
| Pale crust | oven not hot enough, overproofed, no steam | preheat 45-60 min, steam the first 15-20 min |
| Gummy crumb | cut too early, underbaked | bake to 96-98 C inside, cool 1-2 h before slicing |
| Very sour | long warm ferment, levain past peak | use a younger levain, less levain, shorter or cooler bulk |
| Tastes bland | salt under 1.8%, too fast | weigh salt, use less yeast and a longer, cooler rise |

## Doneness and food safety
- Bake lean bread to 93-98 C in the centre and enriched bread to 85-90 C (instant-read thermometer).
- Raw flour can carry bacteria: do not taste raw dough.
- A starter with pink, orange or fuzzy mould is unsafe; discard it.
FILE:templates/bake-sheet.md
# Bake Sheet - <recipe name> - <date>

**Style:** <sourdough boule>  **Pieces:** <2 x 1000 g>  **Kitchen temperature:** <22 C>

## Formula (true baker's percentages)
| Ingredient | % | Grams (this bake) |
|---|---|---|
| Flour total | 100 | |
| Water total | | |
| Salt | | |
| Levain / yeast | | |
| Extras | | |

Hydration <..>% | salt <..>% | prefermented flour <..>% | whole grain <..>%

## Changes from last time
- <e.g. +15 g salt to reach 2%, +50 g water for the whole grain>

## Timeline
| Step | Planned | Actual | Notes (dough temperature, rise %) |
|---|---|---|---|
| Feed levain | | | |
| Mix | | | |
| Bulk end | | | |
| Shape | | | |
| Proof end | | | |
| Bake | | | |

## Result
- Crumb: <open / tight / gummy>  - Crust: <colour, thickness>  - Taste: <salt, sourness>
- Next time: <one change only>
FILE:examples/weekend-whole-wheat-sourdough.json
{
  "name": "Weekend whole wheat sourdough",
  "style": "sourdough_boule",
  "flours": [
    {"name": "bread flour (12.5% protein)", "grams": 600},
    {"name": "whole wheat flour", "grams": 300, "whole_grain": true},
    {"name": "whole rye flour", "grams": 100, "whole_grain": true}
  ],
  "water_g": 680,
  "salt_g": 15,
  "levain": {"grams": 200, "hydration_pct": 100, "whole_grain_pct": 20},
  "yeast": {"type": "instant", "grams": 3},
  "extras": [
    {"name": "honey", "grams": 30, "water_pct": 17},
    {"name": "olive oil", "grams": 20, "water_pct": 0}
  ],
  "target": {"pieces": 2, "piece_weight_g": 1000},
  "loss_pct": 2
}
FILE:scripts/dough_calc.py
#!/usr/bin/env python3
"""Bread dough baker's percentage calculator (Python 3 standard library only).

Input JSON:
  {"name": "...", "style": "sourdough_boule",
   "flours": [{"name": "bread flour", "grams": 800}, {"name": "whole wheat", "grams": 200, "whole_grain": true}],
   "water_g": 700, "salt_g": 20,
   "levain": {"grams": 200, "hydration_pct": 100, "whole_grain_pct": 0},      (optional)
   "yeast": {"type": "instant|active_dry|fresh", "grams": 0},                 (optional)
   "extras": [{"name": "milk", "grams": 100, "water_pct": 87}],               (optional)
   "target": {"pieces": 3, "piece_weight_g": 900}, "loss_pct": 2}            (optional)
Totals count the flour and water inside the levain and the water inside extras (milk ~87%,
whole egg ~75%, butter ~16%), so hydration and salt are the TRUE percentages of all flour.
  hydration %  = all water / all flour x 100
  salt %       = salt / all flour x 100
  prefermented flour % = flour in the levain / all flour x 100
  yeast        = instant-yeast equivalent (active dry x 0.8, fresh x 0.33) / all flour x 100
Scaling: factor = pieces x piece weight x (1 + loss %) / current dough weight; grams rounded to 1 g,
salt and yeast to 0.1 g.
Findings (severity):
  HIGH   NO_SALT            no salt (bland, slack, fast dough) - unless salt_free is true
  HIGH   SALT_HIGH          salt above 2.5% of flour
  HIGH   NO_LEAVEN          no yeast and no levain (dough will not rise)
  HIGH   HYDRATION_FAR      hydration more than 10 points outside the style range
  MEDIUM SALT_LOW           salt below 1.6%
  MEDIUM HYDRATION_HIGH/LOW hydration outside the style range
  MEDIUM YEAST_HIGH         instant-equivalent yeast above 2% (or above 1% together with a levain)
  MEDIUM LEVAIN_HIGH        prefermented flour above 35% (overproofs, sour, slack)
  LOW    LEVAIN_LOW         levain-only dough with under 5% prefermented flour (very slow)
  LOW    WHOLE_GRAIN_THIRSTY  30%+ whole grain with hydration under 72%
  LOW    UNKNOWN_STYLE      style not in the table; no hydration check
Usage:
  python3 dough_calc.py recipe.json
  python3 dough_calc.py recipe.json --pieces 4 --piece-weight 250 --json
  python3 dough_calc.py --self-test
Exit code: 0 no HIGH findings, 1 HIGH findings, 2 bad input.
"""
import argparse
import json
import sys

STYLES = {  # typical hydration range in % (true, incl. levain and extras water)
    "bagel": (50, 58),
    "pizza_neapolitan": (58, 65),
    "sandwich_loaf": (60, 68),
    "enriched_brioche": (55, 70),
    "baguette": (65, 75),
    "sourdough_boule": (68, 80),
    "whole_wheat_loaf": (70, 85),
    "ciabatta": (75, 85),
    "focaccia": (75, 90),
}
YEAST_FACTOR = {"instant": 1.0, "active_dry": 0.8, "fresh": 0.33}
SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}


class DataError(ValueError):
    pass


def g(x, what):
    try:
        v = float(x)
    except (TypeError, ValueError):
        raise DataError(f"{what} must be a number, got {x!r}")
    if v < 0:
        raise DataError(f"{what} must be >= 0")
    return v


def analyze(rec, pieces=None, piece_weight=None):
    flours = rec.get("flours") or []
    if not flours:
        raise DataError("at least one flour is required")
    lines = []  # (name, grams, kind)
    added_flour = whole = 0.0
    for i, fl in enumerate(flours):
        w = g(fl.get("grams"), f"flours[{i}].grams")
        added_flour += w
        if fl.get("whole_grain"):
            whole += w
        lines.append((fl.get("name") or f"flour {i + 1}", w, "flour"))
    water = g(rec.get("water_g", 0), "water_g")
    salt = g(rec.get("salt_g", 0), "salt_g")
    lines += [("water", water, "water"), ("salt", salt, "salt")]
    lev = rec.get("levain") or {}
    lev_g = g(lev.get("grams", 0), "levain.grams")
    lev_flour = lev_water = 0.0
    if lev_g:
        h = g(lev.get("hydration_pct", 100), "levain.hydration_pct")
        if h <= 0:
            raise DataError("levain.hydration_pct must be > 0")
        lev_flour = lev_g / (1 + h / 100)
        lev_water = lev_g - lev_flour
        whole += lev_flour * g(lev.get("whole_grain_pct", 0), "levain.whole_grain_pct") / 100
        lines.append((f"levain ({h:g}% hydration)", lev_g, "levain"))
    y = rec.get("yeast") or {}
    y_g = g(y.get("grams", 0), "yeast.grams")
    y_type = y.get("type", "instant")
    if y_type not in YEAST_FACTOR:
        raise DataError(f"yeast.type must be one of {', '.join(YEAST_FACTOR)}")
    if y_g:
        lines.append((f"{y_type.replace('_', ' ')} yeast", y_g, "yeast"))
    extra_water = 0.0
    for i, ex in enumerate(rec.get("extras") or []):
        w = g(ex.get("grams"), f"extras[{i}].grams")
        extra_water += w * g(ex.get("water_pct", 0), f"extras[{i}].water_pct") / 100
        lines.append((ex.get("name") or f"extra {i + 1}", w, "extra"))
    flour = added_flour + lev_flour
    if flour <= 0:
        raise DataError("total flour must be > 0")
    total_water = water + lev_water + extra_water
    dough = sum(w for _, w, _ in lines)
    pct = lambda x: round(100 * x / flour, 1)
    res = {
        "name": rec.get("name", "recipe"), "style": rec.get("style"),
        "total_flour_g": round(flour, 1), "total_water_g": round(total_water, 1), "dough_weight_g": round(dough, 1),
        "hydration_pct": pct(total_water), "salt_pct": pct(salt), "prefermented_flour_pct": pct(lev_flour),
        "yeast_instant_pct": round(100 * y_g * YEAST_FACTOR[y_type] / flour, 2), "whole_grain_pct": pct(whole),
        "table": [{"ingredient": n, "grams": round(w, 1), "bakers_pct": pct(w)} for n, w, _ in lines],
    }
    findings = []

    def f(sev, code, msg):
        findings.append({"severity": sev, "code": code, "message": msg})

    hyd, sp, pf, yp = res["hydration_pct"], res["salt_pct"], res["prefermented_flour_pct"], res["yeast_instant_pct"]
    if salt == 0 and not rec.get("salt_free"):
        f("HIGH", "NO_SALT", f"no salt: add about 2% of the flour weight ({0.02 * flour:.0f} g)")
    elif sp > 2.5:
        f("HIGH", "SALT_HIGH", f"salt {sp}% (usual 1.8-2.2%, {0.02 * flour:.0f} g at 2%); slows yeast and tastes salty")
    elif 0 < sp < 1.6:
        f("MEDIUM", "SALT_LOW", f"salt {sp}% (usual 1.8-2.2%, {0.02 * flour:.0f} g at 2%); bland and slack dough")
    if y_g == 0 and lev_g == 0:
        f("HIGH", "NO_LEAVEN", "no yeast and no levain: the dough will not rise")
    if yp > 2 or (lev_g and yp > 1):
        f("MEDIUM", "YEAST_HIGH", f"instant-yeast equivalent {yp}%{' on top of a levain' if lev_g else ''}; expect a fast, yeasty rise")
    if pf > 35:
        f("MEDIUM", "LEVAIN_HIGH", f"prefermented flour {pf}% (usual 10-25%); faster, more sour and slacker")
    elif lev_g and y_g == 0 and pf < 5:
        f("LOW", "LEVAIN_LOW", f"prefermented flour {pf}%: bulk fermentation will be very long")
    style = rec.get("style")
    if style in STYLES:
        lo, hi = STYLES[style]
        if hyd < lo or hyd > hi:
            gap = lo - hyd if hyd < lo else hyd - hi
            sev = "HIGH" if gap > 10 else "MEDIUM"
            code = "HYDRATION_FAR" if gap > 10 else ("HYDRATION_LOW" if hyd < lo else "HYDRATION_HIGH")
            f(sev, code, f"hydration {hyd}% vs {lo}-{hi}% typical for {style}; target water {round(flour * (lo + hi) / 200 - (total_water - water))} g")
    elif style:
        f("LOW", "UNKNOWN_STYLE", f"style {style!r} not known ({', '.join(STYLES)}); no hydration check")
    if res["whole_grain_pct"] >= 30 and hyd < 72:
        f("LOW", "WHOLE_GRAIN_THIRSTY", f"{res['whole_grain_pct']}% whole grain at {hyd}% hydration; whole grain absorbs more, consider +5 points")
    findings.sort(key=lambda x: SEV_ORDER[x["severity"]])
    res["findings"] = findings
    tgt = rec.get("target") or {}
    pieces = pieces or tgt.get("pieces")
    piece_weight = piece_weight or tgt.get("piece_weight_g")
    if pieces and piece_weight:
        loss = g(rec.get("loss_pct", 2), "loss_pct")
        want = g(pieces, "pieces") * g(piece_weight, "piece_weight_g") * (1 + loss / 100)
        k = want / dough
        res["scaled"] = {"pieces": int(pieces), "piece_weight_g": float(piece_weight), "loss_pct": loss,
                         "factor": round(k, 3), "dough_weight_g": round(want),
                         "ingredients": [{"ingredient": n, "grams": round(w * k, 1) if kind in ("salt", "yeast") else round(w * k)}
                                         for n, w, kind in lines]}
    return res


def report(r):
    L = [f"# {r['name']}" + (f" ({r['style']})" if r.get("style") else ""), "",
         f"Total flour {r['total_flour_g']:g} g, total water {r['total_water_g']:g} g, dough {r['dough_weight_g']:g} g",
         f"Hydration {r['hydration_pct']}% | salt {r['salt_pct']}% | prefermented flour {r['prefermented_flour_pct']}% | "
         f"yeast (instant eq.) {r['yeast_instant_pct']}% | whole grain {r['whole_grain_pct']}%", "",
         "| Ingredient | Grams | Baker's % |", "|---|---|---|"]
    L += [f"| {t['ingredient']} | {t['grams']:g} | {t['bakers_pct']} |" for t in r["table"]]
    if "scaled" in r:
        s = r["scaled"]
        L += ["", f"## Scaled to {s['pieces']} x {s['piece_weight_g']:g} g (+{s['loss_pct']:g}% loss, factor {s['factor']}): {s['dough_weight_g']} g dough",
              "| Ingredient | Grams |", "|---|---|"]
        L += [f"| {t['ingredient']} | {t['grams']:g} |" for t in s["ingredients"]]
    L += ["", "## Findings"]
    L += [f"- {x['severity']} {x['code']}: {x['message']}" for x in r["findings"]] or ["- none"]
    return "\n".join(L) + "\n"


def self_test():
    cases = []

    def ok(name, cond):
        cases.append(bool(cond))
        print(("PASS " if cond else "FAIL ") + name)

    def codes(r):
        return {x["code"] for x in r["findings"]}

    base = {"style": "sourdough_boule", "flours": [{"grams": 1000}], "water_g": 700, "salt_g": 20,
            "yeast": {"type": "instant", "grams": 5}}
    r = analyze(base)
    ok("hydration 70%, salt 2%", r["hydration_pct"] == 70 and r["salt_pct"] == 2)
    ok("dough weight sums ingredients (1725)", r["dough_weight_g"] == 1725)
    ok("clean recipe has no findings", r["findings"] == [])
    lev = dict(base, flours=[{"grams": 900}], water_g=600, levain={"grams": 200, "hydration_pct": 100}, yeast=None)
    r = analyze(lev)
    ok("levain 100%: flour 1000, water 700", r["total_flour_g"] == 1000 and r["total_water_g"] == 700)
    ok("prefermented flour 10%", r["prefermented_flour_pct"] == 10)
    r = analyze(dict(lev, levain={"grams": 150, "hydration_pct": 50}))
    ok("stiff levain 50%: 100 g flour + 50 g water", r["total_flour_g"] == 1000 and r["total_water_g"] == 650)
    ok("yeast conversion fresh 15 g ~ 0.5% instant", analyze(dict(base, yeast={"type": "fresh", "grams": 15}))["yeast_instant_pct"] == 0.49)
    ok("active dry x 0.8", analyze(dict(base, yeast={"type": "active_dry", "grams": 10}))["yeast_instant_pct"] == 0.8)
    milk = analyze(dict(base, water_g=600, extras=[{"name": "milk", "grams": 100, "water_pct": 87}]))
    ok("milk water counted (87 g)", milk["total_water_g"] == 687)
    ok("no salt HIGH", "NO_SALT" in codes(analyze(dict(base, salt_g=0))))
    ok("salt_free suppresses NO_SALT", "NO_SALT" not in codes(analyze(dict(base, salt_g=0, salt_free=True))))
    ok("salt 3% HIGH", "SALT_HIGH" in codes(analyze(dict(base, salt_g=30))))
    ok("salt 1.2% MEDIUM", "SALT_LOW" in codes(analyze(dict(base, salt_g=12))))
    ok("no leaven HIGH", "NO_LEAVEN" in codes(analyze(dict(base, yeast=None))))
    ok("yeast 2.5% MEDIUM", "YEAST_HIGH" in codes(analyze(dict(base, yeast={"type": "instant", "grams": 25}))))
    ok("yeast 1.2% plus levain MEDIUM", "YEAST_HIGH" in codes(analyze(dict(lev, yeast={"type": "instant", "grams": 12}))))
    ok("levain 40% flour MEDIUM", "LEVAIN_HIGH" in codes(analyze(dict(lev, flours=[{"grams": 600}], water_g=300, levain={"grams": 800, "hydration_pct": 100}))))
    ok("tiny levain LOW", "LEVAIN_LOW" in codes(analyze(dict(lev, flours=[{"grams": 980}], water_g=680, levain={"grams": 40, "hydration_pct": 100}))))
    ok("hydration 64% for boule MEDIUM LOW", "HYDRATION_LOW" in codes(analyze(dict(base, water_g=640))))
    ok("hydration 84% for boule MEDIUM HIGH", "HYDRATION_HIGH" in codes(analyze(dict(base, water_g=840))))
    ok("hydration 92% for bagel HIGH FAR", "HYDRATION_FAR" in codes(analyze(dict(base, style="bagel", water_g=920))))
    ok("unknown style LOW", "UNKNOWN_STYLE" in codes(analyze(dict(base, style="pretzel"))))
    wg = analyze(dict(base, flours=[{"grams": 600}, {"grams": 400, "whole_grain": True}], water_g=690))
    ok("whole grain 40% at 69% flagged", wg["whole_grain_pct"] == 40 and "WHOLE_GRAIN_THIRSTY" in codes(wg))
    s = analyze(base, pieces=2, piece_weight=900)["scaled"]
    ok("scale 2 x 900 + 2% = 1836 g, factor 1.064", s["dough_weight_g"] == 1836 and s["factor"] == 1.064)
    ok("scaled flour 1064 g, salt 21.3 g", s["ingredients"][0]["grams"] == 1064 and s["ingredients"][2]["grams"] == 21.3)
    ok("target from JSON", analyze(dict(base, target={"pieces": 4, "piece_weight_g": 250}, loss_pct=0))["scaled"]["dough_weight_g"] == 1000)
    for bad, label in (({"flours": []}, "no flours"), ({"flours": [{"grams": "x"}]}, "bad number"),
                       ({"flours": [{"grams": -5}]}, "negative grams"), ({"flours": [{"grams": 0}]}, "zero flour"),
                       (dict(base, yeast={"type": "sourdough", "grams": 5}), "unknown yeast type"),
                       (dict(lev, levain={"grams": 100, "hydration_pct": 0}), "zero levain hydration")):
        try:
            analyze(bad)
            ok(label + " rejected", False)
        except DataError:
            ok(label + " rejected", True)
    print(f"{sum(cases)}/{len(cases)} passed")
    return 0 if all(cases) else 1


def main(argv=None):
    ap = argparse.ArgumentParser(description="Baker's percentages, true hydration and scaling for bread dough.")
    ap.add_argument("file", nargs="?", help="recipe JSON (see module docstring)")
    ap.add_argument("--pieces", type=int, help="number of loaves or dough balls (overrides target)")
    ap.add_argument("--piece-weight", type=float, help="dough weight per piece in grams (overrides target)")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.file:
        ap.error("give a recipe JSON file or --self-test")
    try:
        r = analyze(json.load(open(a.file, encoding="utf-8")), a.pieces, a.piece_weight)
    except (OSError, ValueError, DataError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    print(json.dumps(r, indent=2) if a.json else report(r), end="\n" if a.json else "")
    return 1 if any(x["severity"] == "HIGH" for x in r["findings"]) else 0


if __name__ == "__main__":
    sys.exit(main())
CookingPlanningPython+2
F@f
0
Maths reviewer
Text

I am a subject matter expert in maths specially indian content. There are HTML with me created by developers/designers. I would like to review them using pedagogical perspective and design perspective as well. Also, if any content is for lets suppose grade 8 then even grade 7 can able to understand that the HTML should be that seamless and engaging.

I am a subject matter expert in maths specially indian content. There are HTML with me created by developers/designers. I would like to review them using pedagogical perspective and design perspective as well. Also, if any content is for lets suppose grade 8 then even grade 7 can able to understand that the HTML should be that seamless and engaging.
DesignMathReview+1
S@singhrajesh9652979
0
Recipe Scaling and Unit Converter
Skill

Scales a recipe by servings, a factor, or pan size (square, rectangle, round, loaf), converts between US cups and spoons, metric volume and grams with an ingredient density table, rounds to measurable amounts, and flags baking caveats such as fractional eggs, leavening at large factors, batter depth and bake time when the pan changes, tiny amounts, seasoning and cooking time. Includes a stdlib Python scaler with --self-test.

---
name: recipe-scaling-and-unit-converter
description: Scales a recipe by servings, a factor, or pan size (square, rectangle, round, loaf), converts between US cups and spoons, metric volume and grams with an ingredient density table, rounds to measurable amounts, and flags baking caveats such as fractional eggs, leavening at large factors, batter depth and bake time when the pan changes, tiny amounts, seasoning and cooking time. Includes a stdlib Python scaler with --self-test.
---

# Recipe Scaling and Unit Converter

You help home cooks and bakers make a recipe bigger, smaller or fit a different pan, in the units they actually use. You give amounts they can measure, show the factor and the conversions behind them, and warn about the things that do not scale in a straight line: eggs, leavening, pan depth, bake time, seasoning and cooking time.

## Files in this skill

- `scripts/scale_recipe.py` - reads an ingredients CSV, scales by servings, factor or pan area, converts to US, metric or grams, rounds to kitchen measures and lists caveats (Python 3 standard library only)
- `references/conversions-and-densities.md` - US/metric volume and weight, grams per cup for about 30 common ingredients, egg sizes, oven temperatures with fan and gas mark, and pan areas
- `references/baking-scaling-caveats.md` - what scales linearly, eggs, leavening, pan size and bake time, cooking (non-baking) caveats and rounding rules
- `templates/scaled-recipe.md` - the scaled recipe to hand back
- `examples/fudgy-brownies-8x8.csv` - brownies for an 8x8 pan (16 pieces), used in the example

## Workflow

### 1. Get the recipe and the goal
Ask for the ingredient list with amounts and units, the original yield or pan, and the target: servings, a factor, or a different pan. Ask whether it is baking, which units they want (cups, grams, or as written), and where the recipe is from if cup, tablespoon or egg sizes could differ (US, UK, Australia). Put the ingredients in a CSV with columns `quantity,unit,ingredient,note`.

### 2. Run the scaler
```
python3 scripts/scale_recipe.py examples/fudgy-brownies-8x8.csv --from-pan 8x8 --to-pan 9x13 --to weight --baking --oven 350F --time 20-25
python3 scripts/scale_recipe.py stew.csv --servings 4 10 --to metric
python3 scripts/scale_recipe.py cake.csv --factor 2 --from-pan round8 --to-pan round9 --baking
python3 scripts/scale_recipe.py --self-test
```
Pans: `8x8`, `9x13`, `20x30cm`, `round9`, `round23cm`, `loaf9x5`. With only pans, the factor is the area ratio. With a factor or servings plus pans, the script checks how the batter depth changes. Exit 1 means a HIGH caveat (batter depth changes by more than 1.5x), 2 means bad input.

### 3. Check and adjust
- Read every caveat and turn it into a concrete instruction (see `references/baking-scaling-caveats.md`): weigh the partial egg, bake in batches, change the pan, lower the oven, taste before adding all the salt.
- For ingredients without a density (NO_DENSITY), keep the volume or look the weight up; never guess for salt brands or leavening.
- Sanity-check the result: does the pan hold it (2/3 full for cakes)? Did rounding move a key ingredient by more than 5 %? If yes, give grams.

### 4. Deliver
Fill `templates/scaled-recipe.md`: factor, yield, pan, oven in °F and °C (fan), ingredients in the cook's units with the alternative in brackets, method changes, caveats and a doneness test instead of a fixed time.

## Example result
`examples/fudgy-brownies-8x8.csv` moved from an 8x8 to a 9x13 pan: factor x1.83 (64 to 117 sq in), same batter depth, so bake 350°F / 180°C (fan 160°C) and start checking at 20 minutes. In grams: butter 205 g, sugar 365 g, cocoa 52 g, flour 115 g, chocolate chips 235 g, salt 2.5 g, baking powder 2 g, vanilla 7.5 g. Eggs scale to 3.66 (MEDIUM EGG_FRACTION): use 3 eggs plus 33 g beaten egg. Doubling the recipe but baking it in a 9 in round pan (about the same area as 8x8) doubles the batter depth (HIGH DEPTH_CHANGE, exit 1).

## Rules

- Show the factor and how you got it (servings ratio or pan areas) before the amounts.
- Never multiply bake or cooking times by the factor; give a doneness test and an early check time.
- Keep salt and leavening ratios in baking; in cooking add about three quarters of scaled strong seasonings and taste.
- Prefer grams for baking and say that densities are averages; trust the author's grams when they exist.
- Ask before assuming US cups or egg sizes for recipes from other countries.
- Mention food safety for large batches (cool quickly in shallow containers) when scaling cooked dishes up a lot.
FILE:references/conversions-and-densities.md
# Conversions and densities

## Volume (US customary)
| Unit | ml | Also |
|---|---|---|
| 1 tsp | 4.93 | 1/3 tbsp |
| 1 tbsp | 14.79 | 3 tsp |
| 1 fl oz | 29.57 | 2 tbsp |
| 1 cup | 236.59 | 16 tbsp, 8 fl oz |
| 1 pint | 473.18 | 2 cups |
| 1 quart | 946.35 | 4 cups |

Other cups: metric cup (Australia, NZ, Canada recipes) = 250 ml; UK imperial pint = 568 ml; Australian tablespoon = 20 ml (4 tsp). Ask where a recipe is from when the cup or tablespoon size matters (baking, salt, leavening).

## Weight
| Unit | g |
|---|---|
| 1 oz | 28.35 |
| 1 lb | 453.6 (16 oz) |
| 1 stick butter (US) | 113 (1/2 cup, 8 tbsp) |

"oz" in a recipe is weight unless it says "fl oz". For water and milk 8 fl oz weighs about 8 oz; for flour it is about 4.4 oz.

## Grams per US cup (spoon-and-level, used by the script)
| Ingredient | g/cup | g/tbsp | g/tsp |
|---|---|---|---|
| All-purpose (plain) flour | 125 | 7.8 | 2.6 |
| Bread flour | 127 | 7.9 | |
| Cake flour | 114 | 7.1 | |
| Whole wheat flour | 113 | 7.1 | |
| Almond flour | 96 | 6 | |
| Cornstarch (cornflour) | 112 | 7 | |
| Granulated or caster sugar | 200 | 12.5 | 4.2 |
| Brown sugar, packed | 213 | 13.3 | |
| Powdered (icing) sugar | 120 | 7.5 | |
| Butter | 227 | 14.2 | |
| Water | 237 | 14.8 | 4.9 |
| Milk, buttermilk, sour cream | 242 | 15.1 | |
| Heavy cream | 238 | 14.9 | |
| Yogurt | 245 | 15.3 | |
| Vegetable or canola oil | 218 | 13.6 | |
| Olive oil | 216 | 13.5 | |
| Honey, golden syrup | 340 | 21 | 7 |
| Maple syrup | 322 | 20 | |
| Cocoa powder | 85 | 5.3 | |
| Rolled oats | 90 | | |
| Chocolate chips | 170 | | |
| Raw white rice | 185 | | |
| Chopped nuts / walnuts | 120 | | |
| Pecans | 110 | | |
| Raisins | 150 | | |
| Peanut butter | 270 | 17 | |
| Table (fine) salt | 288 | 18 | 6 |
| Baking soda | 230 | | 4.8 |
| Baking powder | 192 | | 4 |
| Instant or active dry yeast | 149 | | 3.1 |
| Vanilla extract | 202 | | 4.2 |
| Ground cinnamon | 125 | | 2.6 |

Notes:
- Flour scooped straight from the bag can weigh 140 to 150 g per cup. If a recipe author gives grams, trust the grams.
- Kosher salt depends on the brand: Diamond Crystal about 2.8 g/tsp, Morton about 5 g/tsp, table salt about 6 g/tsp. Swapping brands by volume can double the salt. The script leaves "kosher salt" in volume and says so.
- The table is a practical average; published tables differ by about 5 %. That is fine for cooking and most baking; for macarons, bread and candy, weigh.

## Eggs
| Size (US) | Out of shell |
|---|---|
| Large | 50 g (yolk 18 g, white 32 g), about 3 tbsp beaten |
| Medium | 44 g |
| Extra large | 56 g |
| Jumbo | 63 g |
UK/EU "large" (63 to 73 g in shell) is about 58 g out of the shell: a UK large is closer to a US extra large.

## Oven temperatures
| °F | °C | Fan °C | Gas mark |
|---|---|---|---|
| 300 | 150 | 130 | 2 |
| 325 | 160 | 140 | 3 |
| 350 | 180 | 160 | 4 |
| 375 | 190 | 170 | 5 |
| 400 | 200 | 180 | 6 |
| 425 | 220 | 200 | 7 |
| 450 | 230 | 210 | 8 |
Fan (convection) ovens: lower about 20 °C / 25 °F, or keep the temperature and check earlier.

## Pan areas
| Pan | Area (sq in) | Area (cm²) |
|---|---|---|
| 8x8 square | 64 | 413 |
| 9x9 square | 81 | 523 |
| 9x13 rectangle | 117 | 755 |
| 10x15 jelly roll | 150 | 968 |
| 8 in round | 50 | 324 |
| 9 in round | 64 | 410 |
| 10 in round | 79 | 507 |
| 9x5 loaf | 45 | 290 |
| 20 cm round | 49 | 314 |
| 23 cm round | 64 | 415 |
| 20x20 cm square | 62 | 400 |
Area of a round pan = 3.14 x radius². An 8x8 square and a 9 in round hold almost the same batter.
FILE:references/baking-scaling-caveats.md
# Scaling caveats

## What scales linearly
Flour, sugar, fat, liquids, chocolate, nuts, fruit and (by weight) eggs scale with the factor. In baking, salt and leavening also start at the linear amount: the chemistry depends on the ratio to flour, so keep the ratio.

## Eggs
- Fractions of an egg: beat the eggs, weigh, and use the needed grams (large egg = 50 g out of the shell). 3.66 eggs = 3 eggs + 33 g beaten egg (about 2 tbsp).
- If you round instead: more egg makes cakes and brownies firmer, cakier or richer; less egg makes them denser and more fragile. Round up for cakes, down for fudgy brownies and cookies when in doubt.
- Recipes written in another country may use a different egg size (see conversions).

## Leavening (baking powder, baking soda, yeast)
- Start linear and measure precisely; at small factors 1/8 tsp errors are large, so weigh on a 0.1 g scale.
- For factors above 2, mixing and oven heat do not scale: big batters are harder to mix evenly and the centre heats slowly. Make separate batches of the original size when you can.
- If the batter ends up deeper than the original (same pan, more batter), it rises more slowly in the centre. Do not "fix" this with more leavening; use a wider pan or lower the oven and bake longer.
- Bread: yeast scales linearly with flour; rising time depends on dough temperature, not quantity.

## Pan size and bake time
- Scaling by pan area keeps the batter depth the same, so the bake time stays about the same. Start checking 5 minutes early.
- Same recipe in a different pan: depth ratio = old area / new area. Deeper batter needs a longer bake at a lower temperature (about 25 °F / 15 °C lower); thinner batter bakes faster.
- Fill cake pans at most 2/3 full, brownie pans about halfway.
- Glass and dark metal pans bake faster at the edges: lower 25 °F / 15 °C.
- Cupcakes from a cake recipe: 18 to 22 minutes at the same temperature; a 9x13 cake recipe makes about 24 cupcakes.
- Never multiply the bake time by the factor. Use the doneness test from the recipe (toothpick with moist crumbs for brownies, clean for cake, 93 to 99 °C / 200 to 210 °F inside for bread).

## Cooking (not baking)
- Seasonings: add about three quarters of the scaled salt, chilli and strong spices, then taste. A bigger pot evaporates differently.
- Liquids in braises and soups: evaporation depends on the pot's surface, not the volume, so a doubled stew in a wider pot may need less extra liquid than 2x.
- Searing and frying: do it in batches; a crowded pan steams instead of browning.
- Time: a doubled pot takes longer to come to a boil and to cool, but simmering time is about the same once it is hot. Roasts depend on thickness, not weight: use a thermometer.
- Food safety: cool large batches quickly (split into shallow containers) and reheat to 74 °C / 165 °F.

## Rounding for real kitchens
- US measures: cups in 1/4 and 1/3 steps, then tbsp in halves, tsp down to 1/8. Below 1/8 tsp = a pinch.
- Metric: grams to 0.5 g below 10 g, 1 g below 100 g, 5 g below 1 kg.
- Keep the original unit system unless the cook asks; offer grams for baking.
- Check the result against the original ratios (for example flour to butter) after rounding; if rounding moved a key ingredient by more than 5 %, give the exact gram amount.
FILE:templates/scaled-recipe.md
# <Recipe name>, scaled <from 8x8 pan to 9x13 pan / from 4 to 10 servings>

**Factor:** x<1.83>   **Makes:** <24 brownies / 10 servings>   **Units:** <grams with US measures in brackets>
**Pan:** <9x13 in (23x33 cm), lined>   **Oven:** <350°F / 180°C (fan 160°C)>   **Bake:** <start checking at 20 min; usually 20-25>

## Ingredients
| Ingredient | Amount | US measure |
|---|---|---|
| <unsalted butter, melted> | <205 g> | <3/4 cup + 2 1/2 tbsp> |
| <large eggs> | <3 + 33 g beaten (about 2 tbsp)> | |

## Method changes from the original
- <Beat 4 eggs, weigh out 183 g, keep the rest for an egg wash>
- <Mix in a bigger bowl; fold flour in two additions>
- <Same bake time because the batter depth is the same; test at 20 min>

## Caveats
- <EGG_FRACTION: ...>
- <Kosher salt brand: ...>

## Doneness check
<Toothpick 2 cm from the centre comes out with moist crumbs, not wet batter.>
FILE:examples/fudgy-brownies-8x8.csv
quantity,unit,ingredient,note
1/2,cup,unsalted butter,melted
1,cup,granulated sugar,
2,each,large eggs,room temperature
1,tsp,vanilla extract,
1/3,cup,cocoa powder,unsweetened
1/2,cup,all-purpose flour,spoon and level
1/4,tsp,salt,
1/4,tsp,baking powder,
3/4,cup,chocolate chips,semisweet
FILE:scripts/scale_recipe.py
#!/usr/bin/env python3
"""Recipe scaling and unit converter (Python 3 standard library only).

Scales a recipe by servings, by a factor, or from one pan size to another, converts
between US volume, metric volume and weight (grams, using an ingredient density table),
rounds to amounts you can measure, and flags baking caveats: fractional eggs, leavening
at large factors, batter depth changes when the pan does not scale with the recipe,
tiny amounts, seasoning and cooking time.

Input CSV columns: quantity,unit,ingredient,note
  quantity: 2 | 1.5 | 3/4 | 1 1/2 | ½   unit: tsp tbsp cup fl oz pint quart ml l g kg oz lb stick
  each (or empty) for counted items such as eggs.
Usage:
  python3 scale_recipe.py recipe.csv --servings 4 10 [--to us|metric|weight|keep]
  python3 scale_recipe.py recipe.csv --from-pan 8x8 --to-pan 9x13 --baking --oven 350F --time 20-25
  python3 scale_recipe.py recipe.csv --factor 0.5 --to weight --json
  python3 scale_recipe.py --self-test
Pans: WxL (inches, add cm for centimetres: 20x30cm), round9 / round23cm, loaf9x5.
Exit codes: 0 = no HIGH findings, 1 = HIGH findings, 2 = bad input.
"""
import argparse, csv, io, json, math, re, sys

ML = {"tsp": 4.92892, "tbsp": 14.7868, "cup": 236.588, "fl oz": 29.5735, "pint": 473.176,
      "quart": 946.353, "ml": 1.0, "l": 1000.0}
G = {"g": 1.0, "kg": 1000.0, "oz": 28.3495, "lb": 453.592, "stick": 113.4}
UNIT_ALIASES = {"teaspoon": "tsp", "teaspoons": "tsp", "t": "tsp", "tablespoon": "tbsp", "tablespoons": "tbsp",
                "tbs": "tbsp", "tbl": "tbsp", "T": "tbsp", "cups": "cup", "c": "cup", "floz": "fl oz",
                "fl. oz": "fl oz", "fluid ounce": "fl oz", "fluid ounces": "fl oz", "pints": "pint", "pt": "pint",
                "quarts": "quart", "qt": "quart", "milliliter": "ml", "milliliters": "ml", "mL": "ml",
                "liter": "l", "liters": "l", "litre": "l", "litres": "l", "L": "l", "gram": "g", "grams": "g",
                "kilogram": "kg", "kilograms": "kg", "ounce": "oz", "ounces": "oz", "pound": "lb", "pounds": "lb",
                "lbs": "lb", "sticks": "stick", "": "each", "x": "each", "whole": "each", "pc": "each",
                "pcs": "each", "piece": "each", "pieces": "each", "clove": "each", "cloves": "each",
                "large": "each", "medium": "each", "small": "each", "pinch": "pinch", "pinches": "pinch",
                "dash": "pinch"}
# grams per US cup (spoon-and-level). Longest matching name wins.
DENSITY = {
    "all-purpose flour": 125, "plain flour": 125, "flour": 125, "bread flour": 127, "cake flour": 114,
    "whole wheat flour": 113, "almond flour": 96, "cornstarch": 112, "cornflour": 112,
    "granulated sugar": 200, "caster sugar": 200, "sugar": 200, "brown sugar": 213,
    "powdered sugar": 120, "icing sugar": 120, "confectioners sugar": 120,
    "butter": 227, "water": 237, "milk": 242, "buttermilk": 242, "heavy cream": 238, "cream": 238,
    "sour cream": 242, "yogurt": 245, "vegetable oil": 218, "canola oil": 218, "olive oil": 216, "oil": 218,
    "honey": 340, "maple syrup": 322, "golden syrup": 340, "cocoa powder": 85, "cocoa": 85,
    "rolled oats": 90, "oats": 90, "chocolate chips": 170, "rice": 185, "chopped nuts": 120,
    "walnuts": 120, "pecans": 110, "raisins": 150, "peanut butter": 270,
    "table salt": 288, "fine salt": 288, "salt": 288, "kosher salt": None,
    "baking soda": 230, "bicarbonate of soda": 230, "baking powder": 192, "instant yeast": 149,
    "active dry yeast": 149, "yeast": 149, "vanilla extract": 202, "vanilla": 202,
    "ground cinnamon": 125, "cinnamon": 125,
}
LEAVENERS = ("baking soda", "bicarbonate", "baking powder", "yeast", "cream of tartar")
SEASONINGS = ("salt", "pepper", "chili", "chilli", "cayenne", "pepper flakes", "garlic powder", "curry", "cumin", "paprika")
EGG_G = {"egg": 50.0, "yolk": 18.0, "white": 32.0}  # US large egg out of shell
UNICODE_FRAC = {"½": .5, "¼": .25, "¾": .75, "⅓": 1 / 3, "⅔": 2 / 3, "⅛": .125, "⅜": .375, "⅝": .625, "⅞": .875}
SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3}


def parse_qty(s):
    s = s.strip()
    for ch, v in UNICODE_FRAC.items():
        if ch in s:
            rest = s.replace(ch, "").strip()
            return (float(rest) if rest else 0.0) + v
    m = re.fullmatch(r"(\d+)\s+(\d+)/(\d+)", s)
    if m:
        return int(m[1]) + int(m[2]) / int(m[3])
    m = re.fullmatch(r"(\d+)/(\d+)", s)
    if m:
        return int(m[1]) / int(m[2])
    m = re.fullmatch(r"\d+(?:\.\d+)?|\.\d+", s)
    if m:
        return float(s)
    raise ValueError(f"cannot read quantity {s!r}")


def norm_unit(u):
    u = u.strip()
    if u in UNIT_ALIASES:
        return UNIT_ALIASES[u]
    low = u.lower().rstrip(".")
    if low in ML or low in G or low in ("each", "pinch"):
        return low
    if low in UNIT_ALIASES:
        return UNIT_ALIASES[low]
    raise ValueError(f"unknown unit {u!r}")


def density_for(name):
    low = name.lower()
    best = None
    for key in DENSITY:
        if re.search(r"\b" + re.escape(key) + r"\b", low) and (best is None or len(key) > len(best)):
            best = key
    return (best, DENSITY[best]) if best else (None, None)


def egg_kind(name):
    low = name.lower()
    if "yolk" in low:
        return "yolk"
    if "white" in low and "egg" in low:
        return "white"
    if re.search(r"\beggs?\b", low):
        return "egg"
    return None


def parse_pan(spec):
    s = spec.strip().lower().replace(" ", "").replace("inch", "").replace("in", "").replace('"', "")
    cm = s.endswith("cm")
    if cm:
        s = s[:-2]
    k = 1 / 2.54 if cm else 1.0
    m = re.fullmatch(r"(?:round)?(\d+(?:\.\d+)?)(?:round)?", s)
    if m and "round" in s:
        d = float(m[1]) * k
        return math.pi * (d / 2) ** 2, f"{spec} round ({d:.1f} in)"
    m = re.fullmatch(r"(?:loaf|square|rect)?(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)", s)
    if m:
        a, b = float(m[1]) * k, float(m[2]) * k
        return a * b, f"{spec} ({a:.1f} x {b:.1f} in)"
    raise ValueError(f"cannot read pan size {spec!r} (use 9x13, 20x30cm, round9, round23cm, loaf9x5)")


def frac_str(x, denoms=(2, 3, 4, 8)):
    whole = int(x)
    rest = x - whole
    best = (abs(rest), 0, 1)
    for d in denoms:
        n = round(rest * d)
        if 0 < n < d and abs(rest - n / d) < best[0] - 1e-9:
            best = (abs(rest - n / d), n, d)
    if abs(rest - 1) < best[0]:
        return str(whole + 1)
    _, n, d = best
    if n == 0:
        return str(whole)
    g = math.gcd(n, d)
    return (f"{whole} " if whole else "") + f"{n // g}/{d // g}"


def frac_val(s):
    return parse_qty(s)


def cup_word(x):
    return "cups" if x > 1 + 1e-9 else "cup"


def fmt_us_volume(ml):
    cups = ml / ML["cup"]
    if cups >= 0.25 - 1e-9:
        s = frac_str(cups, (2, 3, 4))
        err = abs(frac_val(s) - cups) * ML["cup"]
        if err >= ML["tbsp"] * 0.75:
            base = math.floor(cups * 4) / 4
            tb = (cups - base) * ML["cup"] / ML["tbsp"]
            bs = frac_str(base, (4,))
            return (f"{bs} {cup_word(base)} + " if base else "") + f"{frac_str(tb, (2,))} tbsp"
        return f"{s} {cup_word(frac_val(s))}"
    tbsp = ml / ML["tbsp"]
    if tbsp >= 1 - 1e-9:
        return f"{frac_str(tbsp, (2,))} tbsp"
    tsp = ml / ML["tsp"]
    if tsp < 1 / 16:
        return "pinch"
    return f"{frac_str(tsp, (2, 4, 8))} tsp"


def round_metric(x):
    if x < 10:
        return round(x * 2) / 2
    if x < 100:
        return round(x)
    if x < 1000:
        return round(x / 5) * 5
    return round(x / 10) * 10


def fmt_num(x):
    return f"{x:g}"


def fmt_metric(amount, unit):
    v = round_metric(amount)
    if v >= 2000:
        return f"{fmt_num(v / 1000)} {'kg' if unit == 'g' else 'l'}"
    return f"{fmt_num(v)} {unit}"


def fmt_us_weight(g):
    oz = g / G["oz"]
    if oz >= 16:
        lb = oz / 16
        return f"{frac_str(lb, (4,))} lb"
    return f"{frac_str(oz, (2, 4))} oz" if oz >= 1 else f"{fmt_num(round(oz, 2))} oz"


def convert(row, factor, target):
    """Return dict with scaled display and notes for one ingredient row."""
    qty, unit, name = row["qty"] * factor, row["unit"], row["ingredient"]
    key, dens = density_for(name)
    egg = egg_kind(name)
    out = {"ingredient": name, "original": row["original"], "scaled_qty": qty, "unit": unit, "notes": []}
    if unit == "each":
        if egg:
            g = qty * EGG_G[egg]
            out["display"] = f"{fmt_num(round(qty, 2))} ({fmt_metric(g, 'g')} beaten)"
        else:
            out["display"] = fmt_num(round(qty, 2))
        return out
    if unit == "pinch":
        out["display"] = f"{fmt_num(round(qty, 2))} pinch"
        return out
    if unit in ML:
        ml = qty * ML[unit]
        out["ml"] = ml
        if target == "weight":
            if dens:
                out["display"] = fmt_metric(ml / ML["cup"] * dens, "g")
                out["also"] = fmt_us_volume(ml)
            else:
                out["display"] = fmt_metric(ml, "ml")
                out["also"] = fmt_us_volume(ml)
                out["notes"].append("NO_DENSITY")
        elif target == "metric" or (target == "keep" and unit in ("ml", "l")):
            out["display"] = fmt_metric(ml, "ml")
            out["also"] = fmt_us_volume(ml)
        else:
            out["display"] = fmt_us_volume(ml)
            out["also"] = fmt_metric(ml, "ml") if not dens else fmt_metric(ml / ML["cup"] * dens, "g")
        return out
    g = qty * G[unit]
    out["g"] = g
    if target == "us" or (target == "keep" and unit in ("oz", "lb", "stick")):
        if unit == "stick" and target == "keep":
            out["display"] = f"{frac_str(qty, (2, 4))} stick ({fmt_metric(g, 'g')})"
        else:
            out["display"] = fmt_us_weight(g)
        out["also"] = fmt_metric(g, "g")
    else:
        out["display"] = fmt_metric(g, "g")
        if dens:
            out["also"] = fmt_us_volume(g / dens * ML["cup"])
    return out


def read_recipe(text):
    rows = []
    reader = csv.DictReader(io.StringIO(text.lstrip("\ufeff")))
    need = {"quantity", "unit", "ingredient"}
    if not reader.fieldnames or not need <= {f.strip().lower() for f in reader.fieldnames}:
        raise ValueError("CSV header must include quantity,unit,ingredient (note optional)")
    for i, r in enumerate(reader, 2):
        r = {k.strip().lower(): (v or "").strip() for k, v in r.items() if k}
        if not r.get("ingredient"):
            continue
        try:
            qty = parse_qty(r["quantity"])
            unit = norm_unit(r.get("unit", ""))
        except ValueError as e:
            raise ValueError(f"line {i}: {e}")
        orig = f"{r['quantity']} {r.get('unit', '')}".strip()
        rows.append({"qty": qty, "unit": unit, "ingredient": r["ingredient"], "note": r.get("note", ""), "original": orig})
    if not rows:
        raise ValueError("no ingredient rows")
    return rows


def oven(temp):
    m = re.fullmatch(r"\s*(\d+(?:\.\d+)?)\s*°?\s*([CcFf])\s*", temp)
    if not m:
        raise ValueError(f"cannot read oven temperature {temp!r} (use 350F or 180C)")
    v, u = float(m[1]), m[2].upper()
    if u == "F":
        c = (v - 32) * 5 / 9
        return f"{v:g}°F = {round(c / 10) * 10:g}°C (fan oven {round(c / 10) * 10 - 20:g}°C)"
    f = v * 9 / 5 + 32
    return f"{v:g}°C = {round(f / 25) * 25:g}°F"


def scale(rows, factor, target="keep", baking=False, depth_ratio=None, pan_change=None, bake_time=None):
    items = [convert(r, factor, target) for r in rows]
    findings = []
    for it, r in zip(items, rows):
        name = r["ingredient"].lower()
        egg = egg_kind(name)
        if egg and r["unit"] == "each":
            q = it["scaled_qty"]
            whole = math.floor(q + 1e-9)
            if abs(q - round(q)) > 0.15:
                extra = (q - whole) * EGG_G[egg]
                findings.append(("MEDIUM", "EGG_FRACTION", r["ingredient"],
                                 f"scales to {q:.2f} {egg}s",
                                 f"Use {whole} whole {egg}{'s' if whole != 1 else ''} plus {round(extra):g} g beaten {egg} (a large {egg} is about {EGG_G[egg]:g} g out of the shell), or round to {round(q)} and expect a {'richer' if round(q) > q else 'drier'} result."))
        if "NO_DENSITY" in it["notes"]:
            findings.append(("LOW", "NO_DENSITY", r["ingredient"], "no density in the table, so it stays in volume",
                             "Weigh it once, or look up grams per cup; for kosher salt the brand matters (Diamond Crystal about 136 g/cup, Morton about 240 g/cup)."))
        ml = it.get("ml")
        if ml is not None and ml / ML["tsp"] < 1 / 8 and r["unit"] in ML:
            findings.append(("LOW", "TINY_AMOUNT", r["ingredient"], f"scales to {ml / ML['tsp']:.2f} tsp",
                             "Use a pinch or a 1/8 tsp measure; for leavening weigh on a 0.1 g scale."))
        if baking and any(w in name for w in LEAVENERS) and (factor > 2 or factor < 0.5):
            findings.append(("LOW", "LEAVENING", r["ingredient"], f"leavening scaled x{factor:.2f}",
                             "Linear scaling is the starting point; measure exactly (weigh), and for factors above 2 consider separate batches because mixing and oven heat do not scale."))
        if not baking and factor > 2 and any(w in name for w in SEASONINGS):
            findings.append(("INFO", "SEASONING", r["ingredient"], f"seasoning scaled x{factor:.2f}",
                             "Add about three quarters, then taste and adjust; evaporation and pot size change how salty or hot it ends up."))
    if baking and (factor > 3 or factor < 1 / 3):
        findings.append(("MEDIUM", "BIG_FACTOR", "-", f"baking recipe scaled x{factor:.2f}",
                         "Bake in several batches of the original size, or check mixer bowl capacity; never multiply the bake time."))
    if depth_ratio is not None:
        sev = "HIGH" if (depth_ratio > 1.5 or depth_ratio < 0.67) else ("MEDIUM" if (depth_ratio > 1.25 or depth_ratio < 0.8) else None)
        if sev and baking:
            if depth_ratio > 1:
                fix = "Batter will be deeper: lower the oven by 25°F/15°C, bake longer and test the centre, keep the pan at most 2/3 full, or use a bigger pan."
            else:
                fix = "Batter will be thinner: start checking 25 to 40 % earlier, or use a smaller pan."
            findings.append((sev, "DEPTH_CHANGE", "-", f"batter depth changes x{depth_ratio:.2f} versus the original pan", fix))
    if baking and pan_change:
        if depth_ratio is None or 0.9 <= depth_ratio <= 1.1:
            tip = "Batter depth stays about the same, so the bake time stays about the same; start checking 5 minutes early. Glass or dark pans bake faster: lower 25°F/15°C."
        elif 0.8 <= depth_ratio <= 1.25:
            tip = f"Batter depth changes slightly (x{depth_ratio:.2f}); {'bake a few minutes longer' if depth_ratio > 1 else 'start checking a few minutes early'} and test the centre."
        else:
            tip = "See DEPTH_CHANGE for time and temperature."
        if bake_time:
            tip = f"Original {bake_time} min. " + tip
        findings.append(("INFO", "PAN_TIME", "-", pan_change, tip))
    if not baking and factor > 1.5:
        findings.append(("INFO", "COOK_TIME", "-", f"scaled x{factor:.2f}",
                         "Cooking time does not scale with quantity: brown meat in batches, allow longer to come to a boil, and use a pot that keeps the depth similar."))
    findings.sort(key=lambda f: SEV_ORDER[f[0]])
    return items, [dict(zip(("severity", "code", "ingredient", "problem", "fix"), f)) for f in findings]


def render(path, factor, how, target, items, findings, oven_line=None):
    out = [f"# Scaled recipe: {path}", "", f"Scale: {how} -> factor x{factor:.3f}   Units: {target}", ""]
    out += ["| Ingredient | Original | Scaled | Also |", "|---|---|---|---|"]
    for it in items:
        out.append(f"| {it['ingredient']} | {it['original']} | {it['display']} | {it.get('also', '')} |")
    if oven_line:
        out += ["", f"Oven: {oven_line}"]
    out += ["", "## Notes", ""]
    if findings:
        out += ["| Severity | Code | Ingredient | Problem | Advice |", "|---|---|---|---|---|"]
        for f in findings:
            out.append(f"| {f['severity']} | {f['code']} | {f['ingredient']} | {f['problem']} | {f['fix']} |")
    else:
        out.append("No caveats.")
    out += ["", "Weights for volume ingredients use spoon-and-level densities; weigh flour if you can."]
    return "\n".join(out)


def self_test():
    cases = []
    def t(name, cond):
        cases.append((name, bool(cond)))
    close = lambda a, b, tol=1e-6: abs(a - b) <= tol
    t("parse 1 1/2", close(parse_qty("1 1/2"), 1.5))
    t("parse 3/4 and .5 and ½", close(parse_qty("3/4"), .75) and close(parse_qty(".5"), .5) and close(parse_qty("1½"), 1.5))
    t("bad quantity rejected", raises(lambda: parse_qty("some")))
    t("unit aliases", norm_unit("Tablespoons") == "tbsp" and norm_unit("T") == "tbsp" and norm_unit("t") == "tsp" and norm_unit("") == "each")
    t("unknown unit rejected", raises(lambda: norm_unit("handful")))
    t("longest density match", density_for("light brown sugar")[0] == "brown sugar" and density_for("all-purpose flour")[1] == 125)
    t("salt vs kosher salt", density_for("salt")[1] == 288 and density_for("kosher salt")[1] is None)
    t("eggs detected", egg_kind("large eggs") == "egg" and egg_kind("egg yolks") == "yolk" and egg_kind("eggplant") is None)
    a8 = parse_pan("8x8")[0]; a913 = parse_pan("9x13")[0]
    t("rect pan area", close(a8, 64) and close(a913, 117))
    t("round pan area", close(parse_pan("round9")[0], math.pi * 20.25))
    t("cm pan", close(parse_pan("20x20cm")[0], (20 / 2.54) ** 2))
    t("bad pan rejected", raises(lambda: parse_pan("big")))
    t("us volume 1 cup", fmt_us_volume(236.588) == "1 cup")
    t("us volume 3/4 cup", fmt_us_volume(ML["cup"] * .75) == "3/4 cup")
    t("us volume 2/3 cup", fmt_us_volume(ML["cup"] * 2 / 3) == "2/3 cup")
    t("us volume cup + tbsp", fmt_us_volume(ML["cup"] * 1.125) == "1 cup + 2 tbsp")
    t("fractions reduced, cups plural", fmt_us_volume(ML["cup"] * 0.61) == "1/2 cup + 2 tbsp" and fmt_us_volume(ML["cup"] * 2) == "2 cups")
    t("us volume tbsp", fmt_us_volume(ML["tbsp"] * 1.5) == "1 1/2 tbsp")
    t("us volume tsp", fmt_us_volume(ML["tsp"] * .5) == "1/2 tsp")
    t("us volume pinch", fmt_us_volume(ML["tsp"] * .05) == "pinch")
    t("metric rounding", round_metric(7.3) == 7.5 and round_metric(57.4) == 57 and round_metric(228) == 230 and round_metric(1234) == 1230)
    t("1 cup flour = 125 g", convert({"qty": 1, "unit": "cup", "ingredient": "all-purpose flour", "original": ""}, 1, "weight")["display"] == "125 g")
    t("1 stick butter in us/keep", "1 stick" in convert({"qty": 1, "unit": "stick", "ingredient": "butter", "original": ""}, 1, "keep")["display"])
    t("8 oz to grams", convert({"qty": 8, "unit": "oz", "ingredient": "cheese", "original": ""}, 1, "metric")["display"] == "225 g")
    t("500 g to us lb", convert({"qty": 500, "unit": "g", "ingredient": "pasta", "original": ""}, 1, "us")["display"] == "1 lb")
    t("metric volume", convert({"qty": 1, "unit": "cup", "ingredient": "milk", "original": ""}, 2, "metric")["display"] == "475 ml")
    t("no density keeps ml", "NO_DENSITY" in convert({"qty": 1, "unit": "tbsp", "ingredient": "fish sauce", "original": ""}, 1, "weight")["notes"])
    rows = read_recipe("quantity,unit,ingredient,note\n2,each,large eggs,\n1/4,tsp,baking soda,\n1,cup,sugar,\n")
    t("csv read", len(rows) == 3 and rows[0]["unit"] == "each")
    t("csv bad header rejected", raises(lambda: read_recipe("a,b\n1,2\n")))
    t("csv bad unit names line", "line 2" in err_of(lambda: read_recipe("quantity,unit,ingredient\n1,handful,x\n")))
    _, f = scale(rows, 1.828, baking=True)
    t("EGG_FRACTION at x1.83", any(x["code"] == "EGG_FRACTION" and "3 whole eggs plus 33 g" in x["fix"] for x in f))
    _, f = scale(rows, 2.0, baking=True)
    t("no EGG_FRACTION when whole", not any(x["code"] == "EGG_FRACTION" for x in f))
    _, f = scale(rows, 0.1, baking=True)
    t("TINY_AMOUNT and BIG_FACTOR", {"TINY_AMOUNT", "BIG_FACTOR", "LEAVENING"} <= {x["code"] for x in f})
    _, f = scale(rows, 1.0, baking=True, depth_ratio=1.83)
    t("DEPTH_CHANGE HIGH", any(x["code"] == "DEPTH_CHANGE" and x["severity"] == "HIGH" for x in f))
    _, f = scale(rows, 1.0, baking=True, depth_ratio=1.3)
    t("DEPTH_CHANGE MEDIUM", any(x["code"] == "DEPTH_CHANGE" and x["severity"] == "MEDIUM" for x in f))
    _, f = scale(read_recipe("quantity,unit,ingredient\n1,tsp,salt\n"), 3, baking=False)
    t("SEASONING and COOK_TIME for cooking", {"SEASONING", "COOK_TIME"} <= {x["code"] for x in f})
    t("oven F to C", oven("350F") == "350°F = 180°C (fan oven 160°C)" and oven("200C") == "200°C = 400°F")
    t("findings sorted HIGH first", scale(rows, 1.83, baking=True, depth_ratio=2)[1][0]["severity"] == "HIGH")
    passed = sum(ok for _, ok in cases)
    for name, ok in cases:
        print(("PASS " if ok else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def raises(fn):
    try:
        fn()
    except ValueError:
        return True
    return False


def err_of(fn):
    try:
        fn()
    except ValueError as e:
        return str(e)
    return ""


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("recipe", nargs="?")
    g = ap.add_mutually_exclusive_group()
    g.add_argument("--servings", nargs=2, type=float, metavar=("FROM", "TO"))
    g.add_argument("--factor", type=float)
    ap.add_argument("--from-pan")
    ap.add_argument("--to-pan")
    ap.add_argument("--to", default="keep", choices=["keep", "us", "metric", "weight"])
    ap.add_argument("--baking", action="store_true", help="apply baking caveats (eggs, leavening, pan depth)")
    ap.add_argument("--oven", help="original oven temperature, e.g. 350F or 180C")
    ap.add_argument("--time", help="original bake time in minutes, e.g. 20-25")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args()
    if a.self_test:
        return self_test()
    if not a.recipe:
        ap.print_usage(sys.stderr); return 2
    try:
        rows = read_recipe(open(a.recipe, encoding="utf-8").read())
        depth_ratio = pan_change = None
        if bool(a.from_pan) != bool(a.to_pan):
            raise ValueError("give both --from-pan and --to-pan")
        if a.from_pan:
            fa, fd = parse_pan(a.from_pan)
            ta, td = parse_pan(a.to_pan)
            pan_change = f"{fd} -> {td}, area x{ta / fa:.2f}"
        if a.servings:
            if a.servings[0] <= 0 or a.servings[1] <= 0:
                raise ValueError("servings must be positive")
            factor, how = a.servings[1] / a.servings[0], f"{a.servings[0]:g} -> {a.servings[1]:g} servings"
        elif a.factor is not None:
            if a.factor <= 0:
                raise ValueError("factor must be positive")
            factor, how = a.factor, f"factor {a.factor:g}"
        elif a.from_pan:
            factor, how = ta / fa, f"pan {a.from_pan} -> {a.to_pan}"
        else:
            raise ValueError("give --servings FROM TO, --factor F or --from-pan/--to-pan")
        if a.from_pan:
            depth_ratio = factor * fa / ta
            if abs(depth_ratio - 1) < 0.01:
                depth_ratio = None
        items, findings = scale(rows, factor, a.to, a.baking, depth_ratio, pan_change, a.time)
        oven_line = oven(a.oven) if a.oven else None
    except (OSError, UnicodeDecodeError, ValueError) as e:
        print(f"error: {e}", file=sys.stderr); return 2
    if a.json:
        print(json.dumps({"factor": factor, "how": how, "units": a.to, "oven": oven_line,
                          "items": [{k: v for k, v in it.items() if k != "notes"} for it in items],
                          "findings": findings}, indent=2, ensure_ascii=False))
    else:
        print(render(a.recipe, factor, how, a.to, items, findings, oven_line))
    return 1 if any(f["severity"] == "HIGH" for f in findings) else 0


if __name__ == "__main__":
    sys.exit(main())
CookingPlanningPython+2
F@f
0