import re from typing import List, Dict, Any, Optional COMMON_UNITS = [ "kg", "g", "mg", "l", "liter", "ml", "cl", "el", "tl", "esslöffel", "teelöffel", "pck.", "pck", "päckchen", "pkt.", "pkt", "dose", "dosen", "glas", "gläser", "prise", "prisen", "msp.", "msp", "messerspitze", "bund", "stk.", "stk", "stück", "stücke", "zehe", "zehen", "zweig", "zweige", "blatt", "blätter", "scheibe", "scheiben", "stange", "stangen", "kopf", "nest", "nester", "tropfen", "tr." ] def parse_fraction(val_str: str) -> Optional[float]: val_str = val_str.strip() if "/" in val_str: parts = val_str.split("/") if len(parts) == 2: try: return float(parts[0]) / float(parts[1]) except ValueError: pass try: return float(val_str.replace(",", ".")) except ValueError: return None def parse_ingredient_line(line: str) -> Dict[str, Any]: raw = line.strip() if not raw: return {"type": "empty", "raw": ""} if raw.endswith(":") or raw.lower().startswith("für ") or raw.lower() in [ "teig", "sauce", "soße", "füllung", "belag", "creme", "glasur", "marinade", "deko", "schichten & deko", "keksschicht", "heidelbeersoße", "zitronen-zimt-sirup" ]: return { "type": "header", "text": raw.rstrip(":"), "raw": raw } match = re.match(r"^(\d+\s+\d+/\d+|\d+/\d+|\d+(?:[.,]\d+)?)(?:\s*-\s*(\d+(?:[.,]\d+)?))?\s*(.*)$", raw) if match: amt_str = match.group(1).strip() range_end = match.group(2) rest = match.group(3).strip() if " " in amt_str and "/" in amt_str: whole, frac = amt_str.split() val = float(whole) + (parse_fraction(frac) or 0.0) elif "/" in amt_str: val = parse_fraction(amt_str) or 0.0 else: val = parse_fraction(amt_str) or 0.0 display_amt = f"{amt_str}-{range_end}" if range_end else amt_str rest_parts = rest.split(None, 1) if rest_parts: first_word_clean = rest_parts[0].lower().rstrip(".,") if first_word_clean in COMMON_UNITS or rest_parts[0].lower() in COMMON_UNITS: unit = rest_parts[0] ing_name = rest_parts[1] if len(rest_parts) > 1 else "" return { "type": "ingredient", "amount_val": val, "amount_str": display_amt, "unit": unit, "name": ing_name, "raw": raw } return { "type": "ingredient", "amount_val": val, "amount_str": display_amt, "unit": "", "name": rest, "raw": raw } return { "type": "ingredient", "amount_val": None, "amount_str": "", "unit": "", "name": raw, "raw": raw } def parse_ingredients_block(text: str) -> List[Dict[str, Any]]: lines = [l.strip() for l in (text or "").split("\n") if l.strip()] return [parse_ingredient_line(l) for l in lines] # Test cases tests = [ "150 g Mehl", "1 EL Zucker", "1/2 TL Muskat", "1,5 TL Salz", "2 frische Eier", "2-3 Karotten", "Für den Teig:", "Salz und Pfeffer nach Belieben", "300 ml Milch", "1 Pck. Vanillezucker" ] for t in tests: res = parse_ingredient_line(t) print(f"'{t}' => {res['type']}: amt={res.get('amount_val')}, unit='{res.get('unit')}', name='{res.get('name') or res.get('text')}'")