cooking_unit_convert
Kitchen unit conversion: cups, tablespoons, teaspoons, fluid ounces, pints and millilitres under a stated cup definition (US customary 236.588 mL, US legal 240 mL, metric 250 mL, UK 284 mL); grams, ounces and pounds; volume to weight by a given density in g/mL (or the bundled ingredient table: water now, USDA FoodData Central rows when the generator's fetch succeeds); oven temperatures in °C, °F and gas marks (UK table and exact scale).
Install
/tools install cooking_unit_convert /tools install cooking_unit_convert@1.0.0
In a console session, or let the model find it in normal mode: axlr_catalog describes it and axlr_install_tool asks for the same install card. The card reads catalog axlr · cooking_unit_convert 1.0.0 · verified once the index signature, the manifest digest and every file digest check out. Then the model runs it with axlr_run_tool cooking_unit_convert; each run is approved as a local_exec.
Arguments
One JSON object on stdin, validated against the schema before the program starts.
| name | type | required | default | meaning |
|---|---|---|---|---|
value | number | string | no | 2, "1.5", "3/4", "1 1/2", "½" | |
from | string | no | ml, l, dl, cl, cup, tbsp, tsp, fl_oz, pint (in system); us_cup, legal_cup, metric_cup, uk_cup, us_tbsp, us_tsp, au_tbsp, us_fl_oz, uk_fl_oz, us_pint, uk_pint; g, kg, mg, oz, lb; C, F, gas_mark | |
to | string | no | ||
system | string | no | "us_customary" | Meaning of cup, tbsp, tsp, fl_oz and pint. one of "us_customary", "us_legal", "metric", "uk" |
ingredient | string | no | Bundled ingredient for volume <-> weight (list: true lists them) | |
density | number | string | no | Grams per millilitre, instead of ingredient | |
digits | integer | no | 6 | ≥ 1; ≤ 30 |
list | boolean | no | List the ingredients, cup systems and gas marks |
Input schema (JSON)
{
"type": "object",
"additionalProperties": false,
"properties": {
"value": {
"type": [
"number",
"string"
],
"description": "2, \"1.5\", \"3/4\", \"1 1/2\", \"½\""
},
"from": {
"type": "string",
"description": "ml, l, dl, cl, cup, tbsp, tsp, fl_oz, pint (in system); us_cup, legal_cup, metric_cup, uk_cup, us_tbsp, us_tsp, au_tbsp, us_fl_oz, uk_fl_oz, us_pint, uk_pint; g, kg, mg, oz, lb; C, F, gas_mark"
},
"to": {
"type": "string"
},
"system": {
"type": "string",
"enum": [
"us_customary",
"us_legal",
"metric",
"uk"
],
"default": "us_customary",
"description": "Meaning of cup, tbsp, tsp, fl_oz and pint"
},
"ingredient": {
"type": "string",
"description": "Bundled ingredient for volume <-> weight (list: true lists them)"
},
"density": {
"type": [
"number",
"string"
],
"description": "Grams per millilitre, instead of ingredient"
},
"digits": {
"type": "integer",
"minimum": 1,
"maximum": 30,
"default": 6
},
"list": {
"type": "boolean",
"description": "List the ingredients, cup systems and gas marks"
}
}
}
Example
Input
{
"value": 1,
"from": "tbsp",
"to": "ml"
}
Output (stdout)
{
"result": "14.7868",
"exact": "473176473/32000000"
}
Files
The command the install card registers is python3 cooking_unit_convert.py, run in the workspace with PATH and HOME as its only environment. Each file's SHA-256 is in the signed index; the console checks them before showing the card and again before writing.
cooking.json · sha256 c0cbf5e1e22df51561608023b0d5b45e2cb7236e87e507f3bf8a960cb37fb632
{
"generated": "2026-10-18",
"generator": "tools/catalog/data/cooking_unit_convert.py",
"volume_systems": {
"us_customary": {
"cup": "236.5882365",
"tbsp": "14.78676478125",
"tsp": "4.92892159375",
"fl_oz": "29.5735295625",
"pint": "473.176473",
"source": "NIST Handbook 44 (2024), Appendix C (US gallon = 231 in^3)"
},
"us_legal": {
"cup": "240",
"tbsp": "15",
"tsp": "5",
"fl_oz": "30",
"pint": "480",
"source": "21 CFR 101.9(b)(5)(viii) (US nutrition labelling)"
},
"metric": {
"cup": "250",
"tbsp": "15",
"tsp": "5",
"fl_oz": "29.5735295625",
"pint": "500",
"source": "metric cup of Australia, Canada, New Zealand and South Africa; fl oz is the US one, pint 500 mL"
},
"uk": {
"cup": "284.130625",
"tbsp": "15",
"tsp": "5",
"fl_oz": "28.4130625",
"pint": "568.26125",
"source": "half imperial pint; imperial gallon 4.54609 L (NIST SP 811 App. B.8, Weights and Measures Act 1985); metric spoons"
}
},
"gas_marks": [
{
"gas_mark": "1/4",
"fahrenheit": 225,
"celsius": 110,
"description": "very slow / very cool"
},
{
"gas_mark": "1/2",
"fahrenheit": 250,
"celsius": 120,
"description": "very slow / very cool"
},
{
"gas_mark": "1",
"fahrenheit": 275,
"celsius": 140,
"description": "slow / cool"
},
{
"gas_mark": "2",
"fahrenheit": 300,
"celsius": 150,
"description": "slow / cool"
},
{
"gas_mark": "3",
"fahrenheit": 325,
"celsius": 170,
"description": "moderately slow / warm"
},
{
"gas_mark": "4",
"fahrenheit": 350,
"celsius": 180,
"description": "moderate"
},
{
"gas_mark": "5",
"fahrenheit": 375,
"celsius": 190,
"description": "moderately hot"
},
{
"gas_mark": "6",
"fahrenheit": 400,
"celsius": 200,
"description": "moderately hot"
},
{
"gas_mark": "7",
"fahrenheit": 425,
"celsius": 220,
"description": "hot"
},
{
"gas_mark": "8",
"fahrenheit": 450,
"celsius": 230,
"description": "very hot"
},
{
"gas_mark": "9",
"fahrenheit": 475,
"celsius": 240,
"description": "very hot"
},
{
"gas_mark": "10",
"fahrenheit": 500,
"celsius": 260,
"description": "extremely hot"
}
],
"gas_mark_source": "conventional UK table (BBC Good Food; The Guardian, 2007) as reproduced in Wikipedia, 'Gas mark', retrieved 2026-10-18",
"ingredients_source": "USDA FoodData Central, SR Legacy (2018), household-measure portions; public domain (CC0 1.0); fetched with the FDC API",
"licence": "NIST and CFR: US Government works, public domain; USDA FoodData Central: CC0 1.0; gas marks: factual values",
"ingredients": {
"water": {
"aliases": [
"tap water"
],
"fdc_id": null,
"description": "water (by definition, about 1 g/mL)",
"grams_per_us_cup": "236.5882365",
"density_g_per_ml": "1.000000",
"density_basis": "1 mL = 1 g (water near 4 degC)"
}
},
"not_fetched": {
"all-purpose flour": "FDC 168894: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"bread flour": "FDC 168896: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"whole wheat flour": "FDC 168944: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"granulated sugar": "FDC 169655: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"brown sugar": "FDC 168833: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"powdered sugar": "FDC 169656: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"butter": "FDC 173410: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"whole milk": "FDC 171265: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"heavy cream": "FDC 170859: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"honey": "FDC 169640: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"maple syrup": "FDC 169661: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"olive oil": "FDC 171413: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"vegetable oil": "FDC 172336: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"table salt": "FDC 173468: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"baking soda": "FDC 175040: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"baking powder": "FDC 172805: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"cocoa powder": "FDC 169593: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"rolled oats": "FDC 173904: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"white rice": "FDC 169756: API request failed (HTTPError: HTTP Error 429: Too Many Requests)",
"cornstarch": "FDC 169698: API request failed (HTTPError: HTTP Error 429: Too Many Requests)"
}
}
cooking_unit_convert.py · sha256 e14125bd10150aa6ba792f70c3c48438b3ee965f0b11d17f2645531c83465de4
#!/usr/bin/env python3
"""Kitchen conversions: cups, tablespoons, teaspoons, fluid ounces, pints and millilitres under a
stated cup definition (US customary, US legal, metric, UK); grams, ounces and pounds; volume to
weight and back through an ingredient's density (bundled USDA table or given); oven temperatures in
degrees Celsius, Fahrenheit and gas marks.
Input (JSON on stdin):
value number or string: 2, "1.5", "3/4", "1 1/2", "½"
from, to units: ml, l, dl, cl, cup, tbsp, tsp, fl_oz, pint (in `system`), or explicitly
us_cup, legal_cup, metric_cup, uk_cup, us_tbsp, us_tsp, au_tbsp, us_fl_oz, uk_fl_oz,
us_pint, uk_pint; g, kg, mg, oz, lb; C, F, gas_mark
system "us_customary" (default) | "us_legal" | "metric" | "uk"
ingredient name in the bundled table (volume <-> weight) or
density grams per millilitre (volume <-> weight)
digits significant digits of the decimal result (default 6)
list true: list the ingredients, cup systems and gas marks
"""
import json
import re
import sys
from decimal import Decimal, getcontext
from fractions import Fraction
getcontext().prec = 40
UNICODE = {"½": Fraction(1, 2), "⅓": Fraction(1, 3), "⅔": Fraction(2, 3), "¼": Fraction(1, 4), "¾": Fraction(3, 4),
"⅛": Fraction(1, 8), "⅜": Fraction(3, 8), "⅝": Fraction(5, 8), "⅞": Fraction(7, 8)}
MASS = {"g": Fraction(1), "kg": Fraction(1000), "mg": Fraction(1, 1000), "oz": Fraction("28.349523125"),
"lb": Fraction("453.59237")}
MASS_ALIASES = {"gram": "g", "grams": "g", "kilogram": "kg", "kilograms": "kg", "kilo": "kg", "milligram": "mg",
"ounce": "oz", "ounces": "oz", "pound": "lb", "pounds": "lb", "lbs": "lb"}
METRIC_VOLUME = {"ml": Fraction(1), "cl": Fraction(10), "dl": Fraction(100), "l": Fraction(1000)}
VOLUME_ALIASES = {"millilitre": "ml", "milliliter": "ml", "millilitres": "ml", "milliliters": "ml", "litre": "l", "liter": "l",
"litres": "l", "liters": "l", "cups": "cup", "c": "cup", "tablespoon": "tbsp", "tablespoons": "tbsp",
"tbs": "tbsp", "teaspoon": "tsp", "teaspoons": "tsp", "fl oz": "fl_oz", "floz": "fl_oz",
"fluid ounce": "fl_oz", "fluid ounces": "fl_oz", "pints": "pint", "pt": "pint"}
TEMPS = {"c": "C", "°c": "C", "degc": "C", "celsius": "C", "f": "F", "°f": "F", "degf": "F", "fahrenheit": "F",
"gas_mark": "gas_mark", "gas mark": "gas_mark", "gas": "gas_mark", "gasmark": "gas_mark", "regulo": "gas_mark"}
def fail(message):
print(json.dumps({"error": message}))
sys.exit(2)
def load():
try:
with open("cooking.json", encoding="utf-8") as handle:
return json.load(handle)
except (OSError, ValueError) as error:
fail(f"cannot read the bundled cooking.json: {error}")
def number(value, field):
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
fail(f"{field} must be a number or a string such as \"1 1/2\"")
text = str(value).strip()
try:
m = re.fullmatch(r"(-?\d+)\s+(\d+)/(\d+)", text)
if m:
return int(m.group(1)) + Fraction(int(m.group(2)), int(m.group(3)))
m = re.fullmatch(r"(\d*)\s*([½⅓⅔¼¾⅛⅜⅝⅞])", text)
if m:
return (int(m.group(1)) if m.group(1) else 0) + UNICODE[m.group(2)]
return Fraction(text)
except (ValueError, ZeroDivisionError):
fail(f"{field}: cannot read {value!r} as a number")
def dec(x, digits):
d = Decimal(x.numerator) / Decimal(x.denominator)
if d == 0:
return "0"
text = format(d, f".{digits}g")
if "e" in text:
text = format(Decimal(text), "f")
if "." in text:
text = text.rstrip("0").rstrip(".")
return text
def frac(x):
return str(x.numerator) if x.denominator == 1 else f"{x.numerator}/{x.denominator}"
def unit(name, systems, system, field):
"""('volume', mL per unit, label) | ('mass', g per unit, label) | ('temp', kind, label)."""
if not isinstance(name, str) or not name.strip():
fail(f"{field} must be a unit name")
key = name.strip()
low = key.lower()
if low in TEMPS or key in ("°C", "°F"):
return "temp", TEMPS.get(low, key[-1]), TEMPS.get(low, key[-1])
low = MASS_ALIASES.get(low, low)
if low in MASS:
return "mass", MASS[low], low
low = VOLUME_ALIASES.get(low, low)
if low in METRIC_VOLUME:
return "volume", METRIC_VOLUME[low], low
explicit = {"us_cup": ("us_customary", "cup"), "legal_cup": ("us_legal", "cup"), "metric_cup": ("metric", "cup"),
"uk_cup": ("uk", "cup"), "us_tbsp": ("us_customary", "tbsp"), "us_tsp": ("us_customary", "tsp"),
"us_fl_oz": ("us_customary", "fl_oz"), "uk_fl_oz": ("uk", "fl_oz"), "us_pint": ("us_customary", "pint"),
"uk_pint": ("uk", "pint"), "legal_tbsp": ("us_legal", "tbsp"), "legal_tsp": ("us_legal", "tsp")}
if low == "au_tbsp":
return "volume", Fraction(20), "au_tbsp (20 mL)"
if low in explicit:
sysname, base = explicit[low]
return "volume", Fraction(systems[sysname][base]), f"{base} ({sysname})"
if low in ("cup", "tbsp", "tsp", "fl_oz", "pint"):
return "volume", Fraction(systems[system][low]), f"{low} ({system})"
fail(f"{field}: unknown unit {name!r}")
def find_ingredient(table, name):
if not isinstance(name, str) or not name.strip():
fail("ingredient must be a name")
key = name.strip().lower()
for k, info in table["ingredients"].items():
if key == k or key in info.get("aliases", []):
return k, info
for k, info in table["ingredients"].items():
if key in k:
return k, info
if any(key in k for k in table.get("not_fetched", {})):
fail(f"ingredient {name!r} is listed for the table but its USDA data were not fetched in this version; give density in g/mL")
fail(f"ingredient {name!r} is not in the bundled table ({', '.join(sorted(table['ingredients']))}); give density in g/mL instead")
def gas_rows(table):
return [(Fraction(r["gas_mark"]), r) for r in table["gas_marks"]]
def to_fahrenheit_from_gas(g):
if g >= 1:
return 250 + 25 * g
# below gas mark 1 the scale halves for each 25 degF step: 1/2 = 250 F, 1/4 = 225 F
steps = 0
x = g
while x < 1:
x *= 2
steps += 1
if x != 1:
fail("gas marks below 1 are 1/2 and 1/4 (and 1/8)")
return 275 - 25 * steps
def temperature(value, src, dst, table, digits):
result = {"from": src, "to": dst, "value": dec(value, digits)}
if src == "gas_mark":
f = Fraction(to_fahrenheit_from_gas(value))
row = next((r for g, r in gas_rows(table) if g == value), None)
c_exact = (f - 32) * Fraction(5, 9)
if dst == "F":
result.update({"result": dec(f, digits), "exact": frac(f)})
elif dst == "C":
if row:
result.update({"result": str(row["celsius"]), "convention": "UK cookery table (rounded to 10 degC)",
"description": row["description"]})
else:
result["result"] = dec(c_exact, 4)
result["exact_celsius_from_fahrenheit"] = dec(c_exact, digits)
else:
result.update({"result": dec(value, digits)})
result["fahrenheit"] = dec(f, digits)
result["formula"] = "degF = 250 + 25 x gas mark (gas mark >= 1); 1/2 = 250 degF, 1/4 = 225 degF"
result["table_source"] = table["gas_mark_source"]
return result
c = value if src == "C" else (value - 32) * Fraction(5, 9)
f = value if src == "F" else value * Fraction(9, 5) + 32
if dst in ("C", "F"):
out = c if dst == "C" else f
result.update({"result": dec(out, digits), "exact": frac(out), "formula": "degF = degC x 9/5 + 32"})
return result
# to gas mark
if f >= 275:
exact = (f - 250) / 25
else: # below gas mark 1 each 25 degF step halves the mark
steps = (f - 275) / 25
exact = Fraction(2) ** int(steps) if steps.denominator == 1 else Fraction(2 ** float(steps))
rows = gas_rows(table)
if src == "C":
g, row = min(rows, key=lambda gr: (abs(gr[1]["celsius"] - c), gr[0]))
basis = "nearest by the UK table's Celsius values"
else:
g, row = min(rows, key=lambda gr: (abs(gr[1]["fahrenheit"] - f), gr[0]))
basis = "nearest by degF"
result.update({"result": row["gas_mark"], "basis": basis, "description": row["description"],
"fahrenheit": dec(f, digits), "celsius": dec(c, digits),
"exact_gas_mark": dec(exact, 4) if exact is not None else None,
"table_source": table["gas_mark_source"]})
if f < 200 or f > 525:
result["warning"] = "outside the gas mark scale (1/4 to 10)"
return result
def main():
try:
request = json.load(sys.stdin)
except ValueError as error:
fail(f"stdin is not JSON: {error}")
if not isinstance(request, dict):
fail("input must be a JSON object")
extra = set(request) - {"value", "from", "to", "system", "ingredient", "density", "digits", "list"}
if extra:
fail(f"unknown fields: {sorted(extra)}")
table = load()
systems = table["volume_systems"]
if request.get("list"):
print(json.dumps({"volume_systems": systems, "gas_marks": table["gas_marks"],
"ingredients": {k: {"aliases": v.get("aliases", []), "grams_per_us_cup": v["grams_per_us_cup"],
"density_g_per_ml": v["density_g_per_ml"], "fdc_id": v.get("fdc_id"),
"usda_description": v.get("description")} for k, v in table["ingredients"].items()},
"ingredients_source": table["ingredients_source"], "generated": table["generated"]}, ensure_ascii=False))
return
system = request.get("system", "us_customary")
if system not in systems:
fail(f"system must be one of {', '.join(systems)}")
digits = request.get("digits", 6)
if isinstance(digits, bool) or not isinstance(digits, int) or not 1 <= digits <= 30:
fail("digits must be an integer from 1 to 30")
if "value" not in request or "from" not in request or "to" not in request:
fail("give value, from and to (or list: true)")
value = number(request["value"], "value")
kind_a, fa, la = unit(request["from"], systems, system, "from")
kind_b, fb, lb = unit(request["to"], systems, system, "to")
if (kind_a == "temp") != (kind_b == "temp"):
fail("temperatures convert only to temperatures")
if kind_a == "temp":
if fa == "gas_mark" and value <= 0:
fail("gas mark must be positive")
print(json.dumps(temperature(value, fa, fb, table, digits), ensure_ascii=False))
return
if value < 0:
fail("value must not be negative")
result = {"value": dec(value, digits), "from": la, "to": lb, "system": system}
definitions = {}
for label, kind, factor in ((la, kind_a, fa), (lb, kind_b, fb)):
definitions[label] = f"{dec(factor, 15)} {'mL' if kind == 'volume' else 'g'}"
result["definitions"] = definitions
if any(k == "volume" and "(" + system + ")" in lab for k, lab in ((kind_a, la), (kind_b, lb))):
result["system_source"] = systems[system]["source"]
if kind_a == kind_b:
out = value * fa / fb
result.update({"result": dec(out, digits), "exact": frac(out), "factor": frac(fa / fb)})
print(json.dumps(result, ensure_ascii=False))
return
# volume <-> mass through a density in g/mL
if "density" in request and "ingredient" in request:
fail("give ingredient or density, not both")
if "density" in request:
density = number(request["density"], "density")
if density <= 0:
fail("density must be positive")
result["density_g_per_ml"] = dec(density, digits)
result["density_source"] = "given in the input"
elif "ingredient" in request:
name, info = find_ingredient(table, request["ingredient"])
density = Fraction(info["grams_per_us_cup"]) / Fraction(systems["us_customary"]["cup"])
result["ingredient"] = name
result["density_g_per_ml"] = info["density_g_per_ml"]
result["grams_per_us_cup"] = info["grams_per_us_cup"]
source = {"basis": info["density_basis"]}
if info.get("fdc_id"):
source.update({"usda": info["description"], "fdc_id": info["fdc_id"], "dataset": table["ingredients_source"]})
else:
source["note"] = info["description"]
result["density_source"] = source
result["note"] = "household measures vary with how the cup is filled (sifted, spooned, packed): treat as approximate"
else:
fail("converting between volume and weight needs ingredient or density (g/mL)")
ml = value * fa if kind_a == "volume" else value * fa / density
out = ml * density / fb if kind_b == "mass" else ml / fb
result.update({"result": dec(out, digits), "exact": None, "millilitres": dec(ml, digits)})
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
Where it is served
The registry serves only AXLR clients, so a browser gets 403; fetch with a User-Agent that starts with axlr:
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/cooking_unit_convert/1.0.0/manifest.json
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/cooking_unit_convert/1.0.0/cooking.json
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/cooking_unit_convert/1.0.0/cooking_unit_convert.py