Repository: arrow-py/arrow

Issue:
Humanize Format Relative Error For Multiple Granularities
<!--
Thanks for taking the time to submit this bug report.

Please provide us with a detailed description of the bug and a bit of information about your system.
-->

## Issue Description

<!--
Replace this comment block with a description of the bug.

Be sure to include details such as the expected and actual outcomes.
-->

## System Info

- 🖥  **OS name and version**: <!-- Replace with OS name and version (e.g. macOS 10.15.7). -->
- 🐍  **Python version**: <!-- Replace with Python version (e.g. Python 3.8.5). -->
- 🏹  **Arrow version**: <!-- Replace with Arrow version. Run arrow.__version__ to find out! -->

```python
    import arrow
    arw = arrow.Arrow(2013, 1, 1, 0, 0, 0)
    later = arw.shift(seconds=3600)
    humanize_string = arw.humanize(later, granularity=["second", "hour", "day", "month", "year"],)
    print(humanize_string)
    >>> "in 0 years 0 months 0 days an hour and 0 seconds"
```

"in 0 years 0 months 0 days an hour and 0 seconds" should be "0 years 0 months 0 days an hour and 0 seconds ago". I suspect this is dude to the fact in describe-mutli we take timeframes[-1], which in this case would be 0. This results in format relative defaulting to it's positive version. We might need a linear search through all the timeframe units to better determine the correct relative phrase to use. 




Discussion:

Patch:
diff --git a/arrow/arrow.py b/arrow/arrow.py
index fef66c1..d750445 100644
--- a/arrow/arrow.py
+++ b/arrow/arrow.py
@@ -1384,7 +1384,7 @@ class Arrow:
                 search_string = search_string.format(r"\d+")
 
                 # Create search pattern and find within string
-                pattern = re.compile(fr"{search_string}")
+                pattern = re.compile(fr"(^|\b|\d){search_string}")
                 match = pattern.search(input_string)
 
                 # If there is no match continue to next iteration
diff --git a/arrow/constants.py b/arrow/constants.py
index 008e8b7..d26bc0d 100644
--- a/arrow/constants.py
+++ b/arrow/constants.py
@@ -73,7 +73,20 @@ DEHUMANIZE_LOCALES = {
     "zh-hk",
     "nl",
     "nl-nl",
+    "be",
+    "be-by",
+    "pl",
+    "pl-pl",
+    "ru",
+    "ru-ru",
     "af",
+    "bg",
+    "bg-bg",
+    "ua",
+    "uk",
+    "uk-ua",
+    "mk",
+    "mk-mk",
     "de",
     "de-de",
     "de-ch",
diff --git a/arrow/locales.py b/arrow/locales.py
index 2698074..6b1627e 100644
--- a/arrow/locales.py
+++ b/arrow/locales.py
@@ -172,7 +172,16 @@ class Locale:
         humanized = " ".join(parts)
 
         if not only_distance:
-            humanized = self._format_relative(humanized, *timeframes[-1])
+            # Needed to determine the correct relative string to use
+            timeframe_value = 0
+
+            for _unit_name, unit_value in timeframes:
+                if trunc(unit_value) != 0:
+                    timeframe_value = trunc(unit_value)
+                    break
+
+            # Note it doesn't matter the timeframe unit we use on the call, only the value
+            humanized = self._format_relative(humanized, "seconds", timeframe_value)
 
         return humanized
 


Test patch:
diff --git a/tests/test_arrow.py b/tests/test_arrow.py
index 7cb3b51..a2f0881 100644
--- a/tests/test_arrow.py
+++ b/tests/test_arrow.py
@@ -2373,7 +2373,20 @@ def locale_list_no_weeks() -> List[str]:
         "zh-hk",
         "nl",
         "nl-nl",
+        "be",
+        "be-by",
+        "pl",
+        "pl-pl",
+        "ru",
+        "ru-ru",
         "af",
+        "bg",
+        "bg-bg",
+        "ua",
+        "uk",
+        "uk-ua",
+        "mk",
+        "mk-mk",
         "de",
         "de-de",
         "de-ch",
@@ -2485,6 +2498,12 @@ def locale_list_with_weeks() -> List[str]:
         "zh-hk",
         "nl",
         "nl-nl",
+        "pl",
+        "pl-pl",
+        "ru",
+        "ru-ru",
+        "mk",
+        "mk-mk",
         "de",
         "de-de",
         "de-ch",
@@ -2520,6 +2539,27 @@ def locale_list_with_weeks() -> List[str]:
     return tested_langs
 
 
+@pytest.fixture(scope="class")
+def slavic_locales() -> List[str]:
+    tested_langs = [
+        "be",
+        "be-by",
+        "pl",
+        "pl-pl",
+        "ru",
+        "ru-ru",
+        "bg",
+        "bg-bg",
+        "ua",
+        "uk",
+        "uk-ua",
+        "mk",
+        "mk-mk",
+    ]
+
+    return tested_langs
+
+
 class TestArrowDehumanize:
     def test_now(self, locale_list_no_weeks: List[str]):
 
@@ -2883,6 +2923,37 @@ class TestArrowDehumanize:
             with pytest.raises(ValueError):
                 arw.dehumanize(empty_future_string, locale=lang)
 
+    def test_slavic_locales(self, slavic_locales: List[str]):
+
+        # Relevant units for Slavic locale plural logic
+        units = [
+            0,
+            1,
+            2,
+            5,
+            21,
+            22,
+            25,
+        ]
+
+        # Only need to test on seconds as logic holds for all slavic plural units
+        for lang in slavic_locales:
+            for unit in units:
+                arw = arrow.Arrow(2000, 2, 18, 1, 50, 30)
+
+                past = arw.shift(minutes=-1 * unit, days=-1)
+                future = arw.shift(minutes=unit, days=1)
+
+                past_string = past.humanize(
+                    arw, locale=lang, granularity=["minute", "day"]
+                )
+                future_string = future.humanize(
+                    arw, locale=lang, granularity=["minute", "day"]
+                )
+
+                assert arw.dehumanize(past_string, locale=lang) == past
+                assert arw.dehumanize(future_string, locale=lang) == future
+
 
 class TestArrowIsBetween:
     def test_start_before_end(self):