Repository: hhursev/recipe-scrapers

Issue:
Handle fractions and decimals in get_minutes() 
current the get_minutes() utility fails to hour durations when parsing duration attributes.
Examples
- "1.5 hours"
- "1 1/2 hours"
- "1½ hours"
I believe the code below works, but can is probably inefficient and I'm not confident on the full ramifications elsewhere in the code or which corner cases would introduce further defects.

```
from unicodedata import decomposition
TIME_REGEX = re.compile(
        r"(\D*(?P<hours>\d*.?(\s\d)?\/?\d+)\s*(hours|hrs|hr|h|óra))?(\D*(?P<minutes>\d+)\s*(minutes|mins|min|m|perc))?",
        re.IGNORECASE,
    )

def get_minutes(element, return_zero_on_not_found=False):
    if element is None:
        # to be removed
        if return_zero_on_not_found:
            return 0
        raise ElementNotFoundInHtml(element)

    try:
        return int(element)
    except Exception:
        pass

    if isinstance(element, str):
        time_text = element
    else:
        time_text = element.get_text()

    if time_text.startswith("P") and "T" in time_text:
        time_text = time_text.split("T", 2)[1]
    if "-" in time_text:
        time_text = time_text.split("-", 2)[
            1
        ]  # sometimes formats are like this: '12-15 minutes'
    if " to " in time_text:
        time_text = time_text.split("to", 2)[
            1
        ]  # sometimes formats are like this: '12 to 15 minutes'

    empty = ''
    for x in time_text:
        if 'fraction' in decomposition(x):
            f = decomposition(x[-1:]).split()
            empty += f" {f[1].replace('003', '')}/{f[3].replace('003', '')}"
        else:
            empty += x
    time_text = empty
    matched = TIME_REGEX.search(time_text)

    minutes = int(matched.groupdict().get("minutes") or 0)

    if "/" in (hours := matched.groupdict().get("hours")):
        number = hours.split(" ")
        if len(number) == 2:
            minutes += 60*int(number[0])
        fraction = number[-1:][0].split("/")
        minutes += 60 * float(int(fraction[0])/int(fraction[1]))
    else:
        minutes += 60 * float(hours)

    return int(minutes)
```

- [ ] I'd like to try fixing this scraper myself
- [ ] I'd like guidance to help me develop a fix
- [x] I'd prefer if the `recipe-scrapers` team try to fix this


Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/46706310/badge)](https://coveralls.io/builds/46706310)

Coverage decreased (-0.02%) to 95.446% when pulling **28598c364173a680469793e68e5ad33c9d9034b8 on get-minutes-to-handle-fractions-and-decimals** into **941f062572b15264be3fdf802b66c7eaaba7b291 on main**.


Patch:
diff --git a/recipe_scrapers/_utils.py b/recipe_scrapers/_utils.py
index 07f826bf..c84a4ac2 100644
--- a/recipe_scrapers/_utils.py
+++ b/recipe_scrapers/_utils.py
@@ -3,8 +3,19 @@ import re
 
 from ._exceptions import ElementNotFoundInHtml
 
+FRACTIONS = {
+    "¼": 0.25,
+    "½": 0.50,
+    "¾": 0.75,
+    "⅓": 0.33,
+    "⅔": 0.66,
+    "⅕": 0.20,
+    "⅖": 0.40,
+    "⅗": 0.60,
+}
+
 TIME_REGEX = re.compile(
-    r"(\D*(?P<hours>\d+)\s*(hours|hrs|hr|h|óra))?(\D*(?P<minutes>\d+)\s*(minutes|mins|min|m|perc))?",
+    r"(\D*(?P<hours>[\d.\s/?¼½¾⅓⅔⅕⅖⅗]+)\s*(hours|hrs|hr|h|óra))?(\D*(?P<minutes>\d+)\s*(minutes|mins|min|m|perc))?",
     re.IGNORECASE,
 )
 
@@ -41,13 +52,39 @@ def get_minutes(element, return_zero_on_not_found=False):
         time_text = time_text.split("-", 2)[
             1
         ]  # sometimes formats are like this: '12-15 minutes'
+    if " to " in time_text:
+        time_text = time_text.split("to", 2)[
+            1
+        ]  # sometimes formats are like this: '12 to 15 minutes'
     if "h" in time_text:
         time_text = time_text.replace("h", "hours") + "minutes"
 
     matched = TIME_REGEX.search(time_text)
 
     minutes = int(matched.groupdict().get("minutes") or 0)
-    minutes += 60 * int(matched.groupdict().get("hours") or 0)
+    hours_matched = matched.groupdict().get("hours")
+
+    if hours_matched:
+        hours_matched = hours_matched.strip()
+        if any([symbol in FRACTIONS.keys() for symbol in hours_matched]):
+            hours = 0
+            for fraction, value in FRACTIONS.items():
+                if fraction in hours_matched:
+                    hours += value
+                    hours_matched = hours_matched.replace(fraction, "")
+            hours += int(hours_matched) if hours_matched else 0
+        elif "/" in hours_matched:
+            # for example "1 1/2" is matched
+            hours_matched_split = hours_matched.split(" ")
+            hours = 0
+            if len(hours_matched_split) == 2:
+                hours += int(hours_matched_split[0])
+            fraction = hours_matched_split[-1:][0].split("/")
+            hours += float(int(fraction[0]) / int(fraction[1]))
+        else:
+            hours = float(hours_matched)
+
+        minutes += round(60 * hours, 0)
 
     return minutes
 


Test patch:
diff --git a/tests/library/test_utils.py b/tests/library/test_utils.py
index 3853c27d..a016e9ce 100644
--- a/tests/library/test_utils.py
+++ b/tests/library/test_utils.py
@@ -25,3 +25,47 @@ class TestUtils(unittest.TestCase):
     def test_get_minutes_int_in_string_literal(self):
         text = "90"
         self.assertEqual(90, get_minutes(text))
+
+    def test_get_minutes_fraction_in_hours_with_dot_notation(self):
+        text = "1.5 hours"
+        self.assertEqual(90, get_minutes(text))
+
+    def test_get_minutes_fraction_with_fraction_unicode_character_halves(self):
+        text = "1½ hours"
+        self.assertEqual(90, get_minutes(text))
+
+    def test_get_minutes_fraction_with_fraction_unicode_character_three_fours(self):
+        text = "1¾ hours"
+        self.assertEqual(105, get_minutes(text))
+
+    def test_get_minutes_fraction_with_fraction_unicode_character_one_fours(self):
+        text = "1¼ hours"
+        self.assertEqual(75, get_minutes(text))
+
+    def test_get_minutes_fraction_with_fraction_unicode_character_two_thirds(self):
+        text = "1⅔ hours"
+        self.assertEqual(100, get_minutes(text))
+
+    def test_get_minutes_fraction_with_fraction_digits_with_slash(self):
+        text = "1 1/2 hours"
+        self.assertEqual(90, get_minutes(text))
+
+    def test_get_minutes_fraction_with_fraction_digits_with_slash_three_fours(self):
+        text = "1 3/4 hours"
+        self.assertEqual(105, get_minutes(text))
+
+    def test_get_minutes_fraction_with_fraction_digits_with_slash_one_fours(self):
+        text = "1 1/4 hours"
+        self.assertEqual(75, get_minutes(text))
+
+    def test_get_minutes_fraction_with_fraction_digits_with_slash_two_thirds(self):
+        text = "1 2/3 hours"
+        self.assertEqual(100, get_minutes(text))
+
+    def test_get_minutes_handles_dashes(self):
+        text = "15 - 20 minutes"
+        self.assertEqual(20, get_minutes(text))
+
+    def test_get_minutes_handles_to(self):
+        text = "15 to 20 minutes"
+        self.assertEqual(20, get_minutes(text))