AXLR / Catalog / health

nutrition_totals

Totals of energy and nutrients from a food table given in the input (amounts per 100 g) and the grams of each item, per item, in total and per serving, with the percentage of reference intakes: the adult reference intakes of Regulation (EU) No 1169/2011 Annex XIII (energy 8400 kJ / 2000 kcal, fat 70 g, saturates 20 g, carbohydrate 260 g, sugars 90 g, protein 50 g, salt 6 g, and the vitamin and mineral NRVs) by default, or values given in the input. Exact arithmetic; every output carries a disclaimer.

Install

/tools install nutrition_totals
/tools install nutrition_totals@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 · nutrition_totals 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 nutrition_totals; each run is approved as a local_exec.

Arguments

One JSON object on stdin, validated against the schema before the program starts.

nametyperequireddefaultmeaning
foodsobject of objectyesFood name -> {nutrient key: amount per 100 g}; Annex XIII keys use its units (g, mg, ug, kJ, kcal)
itemsarray of objectyesitems ≥ 1
servingsintegerno1≥ 1; ≤ 1000
reference_intakesobject of ['number', 'string']noReplaces the Annex XIII defaults
decimalsintegerno2≥ 0; ≤ 6
Input schema (JSON)
{
  "type": "object",
  "required": [
    "foods",
    "items"
  ],
  "additionalProperties": false,
  "properties": {
    "foods": {
      "type": "object",
      "description": "Food name -> {nutrient key: amount per 100 g}; Annex XIII keys use its units (g, mg, ug, kJ, kcal)",
      "additionalProperties": {
        "type": "object",
        "additionalProperties": {
          "type": [
            "number",
            "string"
          ]
        }
      }
    },
    "items": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "food",
          "grams"
        ],
        "properties": {
          "food": {
            "type": "string"
          },
          "grams": {
            "type": [
              "number",
              "string"
            ]
          }
        }
      }
    },
    "servings": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000,
      "default": 1
    },
    "reference_intakes": {
      "type": "object",
      "additionalProperties": {
        "type": [
          "number",
          "string"
        ]
      },
      "description": "Replaces the Annex XIII defaults"
    },
    "decimals": {
      "type": "integer",
      "minimum": 0,
      "maximum": 6,
      "default": 2
    }
  }
}

Example

Input

{
  "foods": {
    "oats": {
      "energy_kcal": 375,
      "protein": 13
    },
    "milk": {
      "energy_kcal": 64,
      "protein": 3.3
    }
  },
  "items": [
    {
      "food": "oats",
      "grams": 80
    },
    {
      "food": "milk",
      "grams": 250
    }
  ],
  "servings": 2
}

Output (stdout)

{
  "totals": {
    "energy_kcal": "460.00",
    "protein": "18.65"
  },
  "per_serving": {
    "energy_kcal": "230.00"
  },
  "percent_reference_intake_per_serving": {
    "energy_kcal": "11.5",
    "protein": "18.7"
  }
}

Files

The command the install card registers is python3 nutrition_totals.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.

nutrition_totals.py · sha256 aae502e1905d0cf1aada442c6d53302e8108c2151daedf8f504f589816636277
#!/usr/bin/env python3
"""Totals of energy and nutrients for a recipe or a day's food, from a food table given in the
input (values per 100 g) and the grams of each item, per serving and as a percentage of the
reference intakes (default: Regulation (EU) No 1169/2011 Annex XIII, bundled in
reference_intakes.json).

Input (JSON on stdin):
  foods              {"name": {"nutrient": amount per 100 g, ...}, ...}
  items              [{"food": "name", "grams": 150}, ...]
  servings           integer, default 1
  reference_intakes  optional {"nutrient": amount, ...}: replaces the Annex XIII defaults
  decimals           optional, default 2 (amounts); percentages get 1

Nutrient keys are free; for the Annex XIII keys (energy_kj, energy_kcal, fat, saturates,
carbohydrate, sugars, protein, salt, vitamin_c, iron, ...) the food table must use the
reference intake's unit (g, mg, ug, kJ, kcal).
"""
import json
import re
import sys
from decimal import ROUND_HALF_UP, Decimal, localcontext
from fractions import Fraction
from pathlib import Path

DISCLAIMER = ("Not medical or dietary advice: arithmetic on the food values given. Reference intakes are general "
              "adult labelling values, not personal requirements; ask a qualified clinician or dietitian.")
KEY = re.compile(r"^[a-z][a-z0-9_]{0,47}$")
DECIMAL = re.compile(r"\s*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d{1,3})?\s*")


def fail(message):
    print(json.dumps({"error": message, "disclaimer": DISCLAIMER}))
    sys.exit(2)


def number(value, field, positive=False):
    if isinstance(value, bool) or not isinstance(value, (int, float, str)):
        fail(f"{field} must be a number or a decimal string")
    if isinstance(value, str) and not DECIMAL.fullmatch(value):
        fail(f"{field} must be a number or a decimal string")
    try:
        result = Fraction(str(value).strip())
    except (ValueError, ZeroDivisionError):
        fail(f"{field} must be a number or a decimal string")
    if abs(result) > 10 ** 12:
        fail(f"{field} is too large")
    if result < 0 or (positive and result == 0):
        fail(f"{field} must be {'positive' if positive else 'zero or more'}")
    return result


def text(value, places):
    with localcontext() as ctx:
        ctx.prec = 60
        d = Decimal(value.numerator) / Decimal(value.denominator)
        return str(d.quantize(Decimal(1).scaleb(-places), rounding=ROUND_HALF_UP))


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 = sorted(set(request) - {"foods", "items", "servings", "reference_intakes", "decimals"})
    if extra:
        fail(f"unknown field(s): {', '.join(extra)}")
    try:
        table = json.loads((Path(__file__).resolve().parent / "reference_intakes.json").read_text(encoding="utf-8"))
    except (OSError, ValueError):
        fail("reference_intakes.json is missing or unreadable")
    foods = request.get("foods")
    if not isinstance(foods, dict) or not foods:
        fail("foods must be an object of food names, each an object of nutrient amounts per 100 g")
    per100 = {}
    for name, nutrients in foods.items():
        if not isinstance(nutrients, dict) or not nutrients:
            fail(f"foods[{name!r}] must be an object of nutrient amounts per 100 g")
        row = {}
        for key, value in nutrients.items():
            if not KEY.match(key):
                fail(f"nutrient key {key!r} must be lowercase letters, digits and underscores")
            row[key] = number(value, f"foods[{name!r}].{key}")
        per100[name] = row
    items = request.get("items")
    if not isinstance(items, list) or not items:
        fail("items must be a non-empty list of {food, grams}")
    servings = request.get("servings", 1)
    if isinstance(servings, bool) or not isinstance(servings, int) or not 1 <= servings <= 1000:
        fail("servings must be an integer from 1 to 1000")
    decimals = request.get("decimals", 2)
    if isinstance(decimals, bool) or not isinstance(decimals, int) or not 0 <= decimals <= 6:
        fail("decimals must be an integer from 0 to 6")
    if request.get("reference_intakes") is not None:
        given = request["reference_intakes"]
        if not isinstance(given, dict) or not given:
            fail("reference_intakes must be an object of nutrient amounts")
        intakes = {k: {"amount": number(v, f"reference_intakes.{k}", positive=True), "unit": None} for k, v in given.items()}
        ri_source = "given in the input"
    else:
        intakes = {k: {"amount": Fraction(v["amount"]), "unit": v["unit"]} for k, v in table["intakes"].items()}
        ri_source = f"{table['title']}; {table['source_url']} (transcribed {table['transcribed']})"

    nutrients = sorted({k for row in per100.values() for k in row})
    totals = {k: Fraction(0) for k in nutrients}
    incomplete = {k: [] for k in nutrients}
    breakdown, grams_total = [], Fraction(0)
    for i, item in enumerate(items):
        if not isinstance(item, dict) or set(item) - {"food", "grams"}:
            fail(f"items[{i}] must be an object with food and grams")
        food = item.get("food")
        if food not in per100:
            fail(f"items[{i}].food {food!r} is not in foods")
        grams = number(item.get("grams"), f"items[{i}].grams", positive=True)
        grams_total += grams
        contribution = {}
        for k in nutrients:
            if k in per100[food]:
                amount = per100[food][k] * grams / 100
                totals[k] += amount
                contribution[k] = text(amount, decimals)
            elif food not in incomplete[k]:
                incomplete[k].append(food)
        breakdown.append({"food": food, "grams": text(grams, decimals), "nutrients": contribution})

    def percent(amounts):
        return {k: text(v * 100 / intakes[k]["amount"], 1) for k, v in amounts.items() if k in intakes}

    per_serving = {k: v / servings for k, v in totals.items()}
    units = {k: intakes[k]["unit"] for k in nutrients if k in intakes and intakes[k]["unit"]}
    result = {
        "total_grams": text(grams_total, decimals), "servings": servings,
        "grams_per_serving": text(grams_total / servings, decimals),
        "totals": {k: text(v, decimals) for k, v in totals.items()},
        "per_serving": {k: text(v, decimals) for k, v in per_serving.items()},
        "percent_reference_intake_per_serving": percent(per_serving),
        "percent_reference_intake_total": percent(totals),
        "units": units,
        "missing_values": {k: v for k, v in incomplete.items() if v},
        "items": breakdown,
        "reference_intakes": {k: text(v["amount"], 2).rstrip("0").rstrip(".") + (f" {v['unit']}" if v["unit"] else "")
                              for k, v in intakes.items() if k in nutrients},
        "reference_intakes_source": ri_source,
        "notes": ["A nutrient missing from some foods is totalled over the foods that have it (see missing_values)."],
        "disclaimer": DISCLAIMER,
    }
    print(json.dumps(result, ensure_ascii=False))


if __name__ == "__main__":
    try:
        main()
    except SystemExit:
        raise
    except Exception as error:  # last resort: a usage error, never a traceback
        fail(f"invalid input ({type(error).__name__})")
reference_intakes.json · sha256 caedf0956ff5ead915c4a82c566cbea5ad2968ba3bc4464439b4738c3f221dcb
{
 "title": "Reference intakes, Regulation (EU) No 1169/2011, Annex XIII (adults)",
 "source_url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32011R1169",
 "licence": "(c) European Union; reuse authorised with acknowledgement of the source (Commission Decision 2011/833/EU)",
 "transcribed": "2026-10-18",
 "generator": "tools/catalog/data/nutrition_totals.py",
 "intakes": {
  "energy_kj": {
   "label": "Energy",
   "amount": "8400",
   "unit": "kJ",
   "part": "Annex XIII Part B"
  },
  "energy_kcal": {
   "label": "Energy",
   "amount": "2000",
   "unit": "kcal",
   "part": "Annex XIII Part B"
  },
  "fat": {
   "label": "Total fat",
   "amount": "70",
   "unit": "g",
   "part": "Annex XIII Part B"
  },
  "saturates": {
   "label": "Saturates",
   "amount": "20",
   "unit": "g",
   "part": "Annex XIII Part B"
  },
  "carbohydrate": {
   "label": "Carbohydrate",
   "amount": "260",
   "unit": "g",
   "part": "Annex XIII Part B"
  },
  "sugars": {
   "label": "Sugars",
   "amount": "90",
   "unit": "g",
   "part": "Annex XIII Part B"
  },
  "protein": {
   "label": "Protein",
   "amount": "50",
   "unit": "g",
   "part": "Annex XIII Part B"
  },
  "salt": {
   "label": "Salt",
   "amount": "6",
   "unit": "g",
   "part": "Annex XIII Part B"
  },
  "vitamin_a": {
   "label": "Vitamin A",
   "amount": "800",
   "unit": "ug",
   "part": "Annex XIII Part A point 1"
  },
  "vitamin_d": {
   "label": "Vitamin D",
   "amount": "5",
   "unit": "ug",
   "part": "Annex XIII Part A point 1"
  },
  "vitamin_e": {
   "label": "Vitamin E",
   "amount": "12",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "vitamin_k": {
   "label": "Vitamin K",
   "amount": "75",
   "unit": "ug",
   "part": "Annex XIII Part A point 1"
  },
  "vitamin_c": {
   "label": "Vitamin C",
   "amount": "80",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "thiamin": {
   "label": "Thiamin",
   "amount": "1.1",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "riboflavin": {
   "label": "Riboflavin",
   "amount": "1.4",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "niacin": {
   "label": "Niacin",
   "amount": "16",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "vitamin_b6": {
   "label": "Vitamin B6",
   "amount": "1.4",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "folic_acid": {
   "label": "Folic acid",
   "amount": "200",
   "unit": "ug",
   "part": "Annex XIII Part A point 1"
  },
  "vitamin_b12": {
   "label": "Vitamin B12",
   "amount": "2.5",
   "unit": "ug",
   "part": "Annex XIII Part A point 1"
  },
  "biotin": {
   "label": "Biotin",
   "amount": "50",
   "unit": "ug",
   "part": "Annex XIII Part A point 1"
  },
  "pantothenic_acid": {
   "label": "Pantothenic acid",
   "amount": "6",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "potassium": {
   "label": "Potassium",
   "amount": "2000",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "chloride": {
   "label": "Chloride",
   "amount": "800",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "calcium": {
   "label": "Calcium",
   "amount": "800",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "phosphorus": {
   "label": "Phosphorus",
   "amount": "700",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "magnesium": {
   "label": "Magnesium",
   "amount": "375",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "iron": {
   "label": "Iron",
   "amount": "14",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "zinc": {
   "label": "Zinc",
   "amount": "10",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "copper": {
   "label": "Copper",
   "amount": "1",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "manganese": {
   "label": "Manganese",
   "amount": "2",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "fluoride": {
   "label": "Fluoride",
   "amount": "3.5",
   "unit": "mg",
   "part": "Annex XIII Part A point 1"
  },
  "selenium": {
   "label": "Selenium",
   "amount": "55",
   "unit": "ug",
   "part": "Annex XIII Part A point 1"
  },
  "chromium": {
   "label": "Chromium",
   "amount": "40",
   "unit": "ug",
   "part": "Annex XIII Part A point 1"
  },
  "molybdenum": {
   "label": "Molybdenum",
   "amount": "50",
   "unit": "ug",
   "part": "Annex XIII Part A point 1"
  },
  "iodine": {
   "label": "Iodine",
   "amount": "150",
   "unit": "ug",
   "part": "Annex XIII Part A point 1"
  }
 }
}

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/nutrition_totals/1.0.0/manifest.json
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/nutrition_totals/1.0.0/nutrition_totals.py
curl -A axlr-docs/1.0 -O https://registry.underpassai.com/tools/nutrition_totals/1.0.0/reference_intakes.json