Fix Weissman recipe time parsing
This commit is contained in:
@@ -295,6 +295,10 @@ def enrich_recipe_node_from_dom(recipe_node: dict[str, Any], html: str) -> dict[
|
||||
|
||||
|
||||
DURATION_RE = re.compile(r'^P(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?$')
|
||||
TEXT_DURATION_PART_RE = re.compile(
|
||||
r'(?P<value>\d+(?:\.\d+)?)\s*(?P<unit>hours?|hrs?|hr|minutes?|mins?|min|seconds?|secs?|sec|s)\b',
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def parse_duration_minutes(value: Any) -> int | None:
|
||||
@@ -314,6 +318,20 @@ def parse_duration_minutes(value: Any) -> int | None:
|
||||
seconds = int(m.group('seconds') or 0)
|
||||
total = hours * 60 + minutes + (1 if seconds >= 30 else 0)
|
||||
return total or None
|
||||
textual_matches = list(TEXT_DURATION_PART_RE.finditer(value))
|
||||
if textual_matches:
|
||||
total_minutes = 0.0
|
||||
for part in textual_matches:
|
||||
amount = float(part.group('value'))
|
||||
unit = part.group('unit').lower()
|
||||
if unit.startswith(('hour', 'hr')):
|
||||
total_minutes += amount * 60
|
||||
elif unit.startswith(('minute', 'min')):
|
||||
total_minutes += amount
|
||||
else:
|
||||
total_minutes += amount / 60
|
||||
rounded = int(total_minutes + 0.5)
|
||||
return rounded or None
|
||||
num = re.search(r'(\d+)', value)
|
||||
return int(num.group(1)) if num else None
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ UNIT_WORDS = {
|
||||
STOPWORDS = {"fresh", "large", "small", "medium", "optional", "to", "taste", "for", "the", "a", "an"}
|
||||
TAG_KEYS = ("recipeCategory", "recipeCuisine", "keywords")
|
||||
DURATION_RE = re.compile(r"^P(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?$")
|
||||
TEXT_DURATION_PART_RE = re.compile(
|
||||
r"(?P<value>\d+(?:\.\d+)?)\s*(?P<unit>hours?|hrs?|hr|minutes?|mins?|min|seconds?|secs?|sec|s)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
class ScriptExtractor(HTMLParser):
|
||||
@@ -410,6 +414,20 @@ def parse_duration_minutes(value: Any) -> int | None:
|
||||
seconds = int(match.group("seconds") or 0)
|
||||
total = hours * 60 + minutes + (1 if seconds >= 30 else 0)
|
||||
return total or None
|
||||
textual_matches = list(TEXT_DURATION_PART_RE.finditer(text))
|
||||
if textual_matches:
|
||||
total_minutes = 0.0
|
||||
for part in textual_matches:
|
||||
amount = float(part.group("value"))
|
||||
unit = part.group("unit").lower()
|
||||
if unit.startswith(("hour", "hr")):
|
||||
total_minutes += amount * 60
|
||||
elif unit.startswith(("minute", "min")):
|
||||
total_minutes += amount
|
||||
else:
|
||||
total_minutes += amount / 60
|
||||
rounded = int(total_minutes + 0.5)
|
||||
return rounded or None
|
||||
number = re.search(r"(\d+)", text)
|
||||
return int(number.group(1)) if number else None
|
||||
|
||||
|
||||
@@ -6,10 +6,16 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.services.tagging import curate_recipe_tags
|
||||
from app.services.recipe_importer import normalize_recipe_from_url
|
||||
from app.services.recipe_importer import normalize_recipe_from_url, parse_duration_minutes
|
||||
|
||||
|
||||
class TaggingTests(unittest.TestCase):
|
||||
def test_textual_durations_cover_weissman_style_time_strings(self):
|
||||
self.assertEqual(parse_duration_minutes('1 hr, 15 min'), 75)
|
||||
self.assertEqual(parse_duration_minutes('45 min + 12 hrs'), 765)
|
||||
self.assertEqual(parse_duration_minutes('30 minutes'), 30)
|
||||
self.assertEqual(parse_duration_minutes('90 sec'), 2)
|
||||
|
||||
def test_tater_tots_drop_boilerplate_and_gain_meaningful_tags(self):
|
||||
payload = normalize_recipe_from_url(
|
||||
'https://www.joshuaweissman.com/recipes/the-best-tater-tots-ever-recipe'
|
||||
@@ -21,6 +27,9 @@ class TaggingTests(unittest.TestCase):
|
||||
self.assertIn('tater-tots', payload['tags'])
|
||||
self.assertIn('side-dish', payload['tags'])
|
||||
self.assertNotIn('beef', payload['tags'])
|
||||
self.assertEqual(payload['prep_time'], 30)
|
||||
self.assertEqual(payload['cook_time'], 45)
|
||||
self.assertEqual(payload['time'], 75)
|
||||
|
||||
def test_halal_chicken_rice_drops_boilerplate_and_infers_cuisine(self):
|
||||
payload = normalize_recipe_from_url(
|
||||
@@ -33,6 +42,9 @@ class TaggingTests(unittest.TestCase):
|
||||
self.assertIn('chicken', payload['tags'])
|
||||
self.assertIn('rice', payload['tags'])
|
||||
self.assertNotIn('skillet', payload['tags'])
|
||||
self.assertEqual(payload['prep_time'], 30)
|
||||
self.assertEqual(payload['cook_time'], 45)
|
||||
self.assertEqual(payload['time'], 75)
|
||||
|
||||
def test_existing_machine_tags_can_be_replaced_when_recipe_content_disagrees(self):
|
||||
tags = curate_recipe_tags(
|
||||
|
||||
Reference in New Issue
Block a user