Repository: crsmithdev/arrow
Pull request #575 (2019-04-13 16:50:12)

Fix humanize behavior with future days
After doing a little bit of debugging I found that the floating point delta value was being truncated by a cast to an int. If we simply round the number before casting this fixed the issue mentioned in https://github.com/crsmithdev/arrow/issues/541.

I have also included the test cases described in the issue thread.

Closes: #541 

codecov-io: # [Codecov](https://codecov.io/gh/crsmithdev/arrow/pull/575?src=pr&el=h1) Report
> Merging [#575](https://codecov.io/gh/crsmithdev/arrow/pull/575?src=pr&el=desc) into [master](https://codecov.io/gh/crsmithdev/arrow/commit/43ffd2ac65984ca90d6c44b6e6dc9769f5288117?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/crsmithdev/arrow/pull/575/graphs/tree.svg?width=650&token=4EkGnFsIGn&height=150&src=pr)](https://codecov.io/gh/crsmithdev/arrow/pull/575?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master   #575   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          13     13           
  Lines        3399   3405    +6     
  Branches      238    238           
=====================================
+ Hits         3399   3405    +6
```


| [Impacted Files](https://codecov.io/gh/crsmithdev/arrow/pull/575?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tests/arrow\_tests.py](https://codecov.io/gh/crsmithdev/arrow/pull/575/diff?src=pr&el=tree#diff-dGVzdHMvYXJyb3dfdGVzdHMucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [arrow/arrow.py](https://codecov.io/gh/crsmithdev/arrow/pull/575/diff?src=pr&el=tree#diff-YXJyb3cvYXJyb3cucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/crsmithdev/arrow/pull/575?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/crsmithdev/arrow/pull/575?src=pr&el=footer). Last update [43ffd2a...9ea6019](https://codecov.io/gh/crsmithdev/arrow/pull/575?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/arrow/arrow.py b/arrow/arrow.py
index 8db28be..f4563d9 100644
--- a/arrow/arrow.py
+++ b/arrow/arrow.py
@@ -802,7 +802,7 @@ class Arrow(object):
         else:
             raise TypeError()
 
-        delta = int(util.total_seconds(self._datetime - dt))
+        delta = int(round(util.total_seconds(self._datetime - dt)))
         sign = -1 if delta < 0 else 1
         diff = abs(delta)
         delta = diff
@@ -871,6 +871,65 @@ class Arrow(object):
             if(trunc(abs(delta)) != 1):
                 granularity += 's'
             return locale.describe(granularity, delta, only_distance=only_distance)
+
+    # query functions
+
+    def is_between(self, start, end, bounds='()'):
+        ''' Returns a boolean denoting whether the specified date and time is between
+        the start and end dates and times.
+
+        :param start: an :class:`Arrow <arrow.arrow.Arrow>`.
+        :param end: an :class:`Arrow <arrow.arrow.Arrow>`.
+        :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies
+            whether to include or exclude the start and end values in the range. '(' excludes
+            the start, '[' includes the start, ')' excludes the end, and ']' includes the end.
+            If the bounds are not specified, the default bound '()' is used.
+
+        Usage::
+
+            >>> start = arrow.get(datetime(2013, 5, 5, 12, 30, 10))
+            >>> end = arrow.get(datetime(2013, 5, 5, 12, 30, 36))
+            >>> arrow.get(datetime(2013, 5, 5, 12, 30, 27)).is_between(start, end)
+            True
+
+            >>> start = arrow.get(datetime(2013, 5, 5))
+            >>> end = arrow.get(datetime(2013, 5, 8))
+            >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[]')
+            True
+
+            >>> start = arrow.get(datetime(2013, 5, 5))
+            >>> end = arrow.get(datetime(2013, 5, 8))
+            >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[)')
+            False
+
+        '''
+
+        if bounds != '()' and bounds != '(]' and bounds != '[)' and bounds != '[]':
+            raise AttributeError('Error. Could not understand the specified bounds. Please select between \
+                "()", "(]", "[)", or "[]"')
+
+        if not isinstance(start, Arrow):
+            raise TypeError('Can\'t parse start date argument type of \'{}\''.format(type(start)))
+
+        if not isinstance(end, Arrow):
+            raise TypeError('Can\'t parse end date argument type of \'{}\''.format(type(end)))
+
+        include_start = (bounds[0] == '[')
+        include_end = (bounds[1] == ']')
+
+        target_timestamp = self.float_timestamp
+        start_timestamp = start.float_timestamp
+        end_timestamp = end.float_timestamp
+
+        if include_start and include_end:
+            return target_timestamp >= start_timestamp and target_timestamp <= end_timestamp
+        elif include_start and not include_end:
+            return target_timestamp >= start_timestamp and target_timestamp < end_timestamp
+        elif not include_start and include_end:
+            return target_timestamp > start_timestamp and target_timestamp <= end_timestamp
+        else:
+            return target_timestamp > start_timestamp and target_timestamp < end_timestamp
+
     # math
 
     def __add__(self, other):
diff --git a/arrow/locales.py b/arrow/locales.py
index 2efc859..db9433a 100644
--- a/arrow/locales.py
+++ b/arrow/locales.py
@@ -234,6 +234,19 @@ class EnglishLocale(Locale):
                 return '{}rd'.format(n)
         return '{}th'.format(n)
 
+    def describe(self, timeframe, delta=0, only_distance=False):
+        ''' Describes a delta within a timeframe in plain language.
+
+        :param timeframe: a string representing a timeframe.
+        :param delta: a quantity representing a delta in a timeframe.
+        :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords
+        '''
+
+        humanized = super(EnglishLocale, self).describe(timeframe, delta, only_distance)
+        if only_distance and timeframe == 'now':
+            humanized = 'instantly'
+
+        return humanized
 
 class ItalianLocale(Locale):
     names = ['it', 'it_it']
@@ -843,6 +856,7 @@ class _DeutschLocaleCommonMixin(object):
 
     def describe(self, timeframe, delta=0, only_distance=False):
         ''' Describes a delta within a timeframe in plain language.
+
         :param timeframe: a string representing a timeframe.
         :param delta: a quantity representing a delta in a timeframe.
         :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords


Test patch:
diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py
index afa3ca3..851f889 100644
--- a/tests/arrow_tests.py
+++ b/tests/arrow_tests.py
@@ -1244,6 +1244,16 @@ class ArrowHumanizeTests(Chai):
         assertEqual(self.now.humanize(later, only_distance=True), '2 days')
         assertEqual(later.humanize(self.now, only_distance=True), '2 days')
 
+        # Regression tests for humanize bug referenced in issue 541
+        later = self.now.shift(days=3)
+        assertEqual(later.humanize(), 'in 3 days')
+
+        later = self.now.shift(days=3, seconds=1)
+        assertEqual(later.humanize(), 'in 3 days')
+
+        later = self.now.shift(days=4)
+        assertEqual(later.humanize(), 'in 4 days')
+
     def test_month(self):
 
         later = self.now.shift(months=1)
@@ -1360,6 +1370,82 @@ class ArrowHumanizeTestsWithLocale(Chai):
 
         assertEqual(result, '2 года назад')
 
+class ArrowIsBetweenTests(Chai):
+
+    def test_start_before_end(self):
+        target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+        start = arrow.Arrow.fromdatetime(datetime(2013, 5, 8))
+        end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5))
+        result = target.is_between(start, end)
+        assertFalse(result)
+
+    def test_exclusive_exclusive_bounds(self):
+        target = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 27))
+        start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 10))
+        end = arrow.Arrow.fromdatetime(datetime(2013, 5, 5, 12, 30, 36))
+        result = target.is_between(start, end, '()')
+        assertTrue(result)
+        result = target.is_between(start, end)
+        assertTrue(result)
+
+    def test_exclusive_exclusive_bounds_same_date(self):
+        target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+        start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+        end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+        result = target.is_between(start, end, '()')
+        assertFalse(result)
+
+    def test_inclusive_exclusive_bounds(self):
+        target = arrow.Arrow.fromdatetime(datetime(2013, 5, 6))
+        start = arrow.Arrow.fromdatetime(datetime(2013, 5, 4))
+        end = arrow.Arrow.fromdatetime(datetime(2013, 5, 6))
+        result = target.is_between(start, end, '[)')
+        assertFalse(result)
+
+    def test_exclusive_inclusive_bounds(self):
+        target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+        start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5))
+        end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+        result = target.is_between(start, end, '(]')
+        assertTrue(result)
+
+    def test_inclusive_inclusive_bounds_same_date(self):
+        target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+        start = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+        end = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+        result = target.is_between(start, end, '[]')
+        assertTrue(result)
+
+    def test_type_error_exception(self):
+        with assertRaises(TypeError):
+            target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+            start = datetime(2013, 5, 5)
+            end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8))
+            result = target.is_between(start, end)
+
+        with assertRaises(TypeError):
+            target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+            start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5))
+            end = datetime(2013, 5, 8)
+            result = target.is_between(start, end)
+
+        with assertRaises(TypeError):
+            result = target.is_between(None, None)
+
+    def test_attribute_error_exception(self):
+        target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))
+        start = arrow.Arrow.fromdatetime(datetime(2013, 5, 5))
+        end = arrow.Arrow.fromdatetime(datetime(2013, 5, 8))
+        with assertRaises(AttributeError):
+            result = target.is_between(start, end, "][")
+        with assertRaises(AttributeError):
+            result = target.is_between(start, end, "")
+        with assertRaises(AttributeError):
+            result = target.is_between(start, end, "]")
+        with assertRaises(AttributeError):
+            result = target.is_between(start, end, "[")
+        with assertRaises(AttributeError):
+            result = target.is_between(start, end, "hello")
 
 class ArrowUtilTests(Chai):
 
diff --git a/tests/locales_tests.py b/tests/locales_tests.py
index 82b31ef..adaab86 100644
--- a/tests/locales_tests.py
+++ b/tests/locales_tests.py
@@ -96,6 +96,18 @@ class LocaleTests(Chai):
         assertEqual(self.locale.meridian(7, 'NONSENSE'), None)
 
 
+class EnglishLocaleTests(Chai):
+
+    def setUp(self):
+        super(EnglishLocaleTests, self).setUp()
+
+        self.locale = locales.EnglishLocale()
+
+    def test_describe(self):
+        assertEqual(self.locale.describe('now', only_distance=True), 'instantly')
+        assertEqual(self.locale.describe('now', only_distance=False), 'just now')
+
+
 class ItalianLocalesTests(Chai):
 
     def test_ordinal_number(self):