Fix three bugs found by running against the live instance
suggest_recipes returned 422 for every call. The endpoint takes food UUIDs as
repeated query parameters, not a comma-joined string of names — the rule
inherited from the skill was never actually verified. Added resolve_foods to
map names to food records first, and report anything unresolved instead of
dropping it.
scale_ingredients silently returned unscaled lines. It read the structured
quantity field and string-replaced str(quantity) into the display text, which
failed twice over: unparsed recipes store quantity 0.0 with the real amount only
in the text, and str(800.0) never matches "800 g kycklinglårfilé". Amounts are
now read from the display text, ranges scale at both ends ("1-1,5 msk"), mixed
numbers scale as one value ("1 1/2 msk"), and lines with no amount are reported
in `unscaled` rather than presented as if they had been scaled.
looks_mangled flagged the readable "1 1/2 msk tomatpuré" because its duplicate-word
rule matched the repeated digit. Restricted to alphabetic words, so it still
catches "2 dl dl grädde".
Verified against all 238 recipes in the live library: no false positives remain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+61
-1
@@ -6,6 +6,7 @@ Mealie instance — see resources/import-pitfalls.md.
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
#: Campaign/navigation suffixes seen appended to imported titles, notably from Köket.
|
||||
TITLE_JUNK_SUFFIXES = (
|
||||
@@ -93,6 +94,63 @@ def ingredient_display_lines(recipe: dict) -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
#: One amount: a decimal ("4,7"), a fraction ("1/2"), or a mixed number ("1 1/2").
|
||||
_AMOUNT = r"\d+(?:[.,]\d+)?(?:\s+\d+/\d+)?|\d+/\d+"
|
||||
|
||||
#: A leading amount in a Swedish ingredient line, optionally a range:
|
||||
#: "800 g ...", "4,7 dl ...", "1 klyfta ...", "1-1,5 msk ...", "1 1/2 msk ...".
|
||||
_LEADING_AMOUNT = re.compile(
|
||||
rf"^\s*(?P<low>{_AMOUNT})(?:\s*[-–]\s*(?P<high>{_AMOUNT}))?(?=\s|[^\d.,/-]|$)"
|
||||
)
|
||||
|
||||
|
||||
def parse_amount(raw: str) -> Decimal:
|
||||
"""Read a Swedish/ASCII amount: ``4,7``, ``1/2``, or the mixed number ``1 1/2``."""
|
||||
text = raw.strip().replace(",", ".")
|
||||
whole = Decimal(0)
|
||||
if " " in text:
|
||||
head, text = text.split(None, 1)
|
||||
whole = Decimal(head)
|
||||
if "/" in text:
|
||||
numerator, denominator = text.split("/", 1)
|
||||
return whole + Decimal(numerator) / Decimal(denominator)
|
||||
return whole + Decimal(text)
|
||||
|
||||
|
||||
def format_amount(value: Decimal) -> str:
|
||||
"""Render a scaled amount the way the recipe library writes them.
|
||||
|
||||
Whole numbers lose the decimal part (``1200``, not ``1200.0``) and fractions
|
||||
use the Swedish decimal comma (``1,5``) to match the stored ingredient lines.
|
||||
"""
|
||||
quantized = value.quantize(Decimal("0.1"), rounding=ROUND_HALF_UP)
|
||||
if quantized == quantized.to_integral_value():
|
||||
return str(int(quantized))
|
||||
return format(quantized, "f").replace(".", ",")
|
||||
|
||||
|
||||
def scale_line(line: str, factor: Decimal) -> tuple[str, bool]:
|
||||
"""Scale the leading amount of an ingredient line.
|
||||
|
||||
Returns the line and whether anything was scaled. The amount is read from the
|
||||
display text rather than the structured ``quantity`` field, because unparsed
|
||||
recipes carry ``quantity: 0.0`` while the real number lives in the text — and
|
||||
because ``str(800.0)`` never substring-matches ``"800 g ..."``.
|
||||
"""
|
||||
match = _LEADING_AMOUNT.match(line)
|
||||
if not match:
|
||||
return line, False
|
||||
|
||||
def scaled(raw: str) -> str:
|
||||
return format_amount(parse_amount(raw) * factor)
|
||||
|
||||
low = scaled(match.group("low"))
|
||||
high = match.group("high")
|
||||
# A range must move at both ends: "1-1,5 msk" doubled is "2-3 msk", not "2-1,5 msk".
|
||||
replacement = f"{low}-{scaled(high)}" if high else low
|
||||
return line[: match.start("low")] + replacement + line[match.end() :], True
|
||||
|
||||
|
||||
def looks_mangled(line: str) -> bool:
|
||||
"""Heuristic for parser-damaged display text.
|
||||
|
||||
@@ -101,6 +159,8 @@ def looks_mangled(line: str) -> bool:
|
||||
"""
|
||||
if any(glyph in line for glyph in ("⁄", "½", "¼", "¾", "⅓", "⅔")):
|
||||
return True
|
||||
if re.search(r"\b(\w+)\s+\1\b", line, flags=re.IGNORECASE):
|
||||
# Duplicated words, but only alphabetic ones: "2 dl dl grädde" is parser damage,
|
||||
# while the mixed number in "1 1/2 msk tomatpuré" is perfectly readable.
|
||||
if re.search(r"\b([^\W\d_]+)\s+\1\b", line, flags=re.IGNORECASE):
|
||||
return True
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user