Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Humanize and describe_multi Bug Fix #997

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions arrow/arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,7 @@ def humanize(
locale: str = DEFAULT_LOCALE,
only_distance: bool = False,
granularity: Union[_GRANULARITY, List[_GRANULARITY]] = "auto",
dynamic: bool = False,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this False by default to avoid a breaking change? That's understandable, but also disappointing since to me, the dynamic behaviour seems much more useful than outputting a bunch of zeros for units.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MarkKoz, yes we left as False by default in order to avoid changing the behaviour of humanize drastically. I agree that it would make more sense to leave it as True by default however. @jadchaar @krisfremen @systemcatch what are your thoughts?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the least, I hope changing this can be considered for the next major release (assuming you're following SemVer).

Unrelated: dynamic isn't a good name — it's vague and non-self-descriptive. omit_zeros or something similar would be clearer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should leave it as False for the time being, do a warning for changing behavior and change it to default True after a few versions.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable. Any thoughts on my name suggestion?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errr throwing out a few ideas for the name, only_natural, minimal, drop_zeros. omit_zeros is fine as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think omit_zeros is probably the best name

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I think the dynamic naming is fine, as IMO it indicates it will use dynamically any of the granularity fields that are specified, as the time progresses or is shifted.

Although, omit_zeroes is a nice alternative name, I would prefer dynamic.

) -> str:
"""Returns a localized, humanized representation of a relative difference in time.

Expand Down Expand Up @@ -1264,7 +1265,11 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float:
if _frame in granularity:
value = sign * _delta / self._SECS_MAP[_frame]
_delta %= self._SECS_MAP[_frame]
if trunc(abs(value)) != 1:

# If user chooses dynamic and the display value is 0 don't subtract
if dynamic and trunc(abs(value)) == 0:
pass
elif trunc(abs(value)) != 1:
Comment on lines +1282 to +1284
Copy link

@MarkKoz MarkKoz Aug 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make any significant difference to save the value of trunc(abs(value)) rather than calculating it twice? This could also be said for the other parts of the diff that use trunc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decent catch.

Profiling the code, you'd need to run about 10k of the trunc(abs()) calls to even come close to seeing a 1ms difference.

timeframes.append(
(cast(TimeFrameLiteral, _frame + "s"), value)
)
Expand All @@ -1285,12 +1290,18 @@ def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float:
for frame in frames:
delta = gather_timeframes(delta, frame)

if len(timeframes) < len(granularity):
if len(timeframes) < len(granularity) and not dynamic:
raise ValueError(
"Invalid level of granularity. "
"Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'."
)

# Needed for the case of dynamic usage (could end up with only one frame unit)
if len(timeframes) == 1:
return locale.describe(
timeframes[0][0], delta, only_distance=only_distance
)

return locale.describe_multi(timeframes, only_distance=only_distance)

except KeyError as e:
Expand Down
21 changes: 19 additions & 2 deletions arrow/locales.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,17 @@ def describe_multi(
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

Expand Down Expand Up @@ -3320,8 +3330,15 @@ def describe_multi(
"""

humanized = ""
relative_delta = 0

for index, (timeframe, delta) in enumerate(timeframes):
last_humanized = self._format_timeframe(timeframe, trunc(delta))

# A check for the relative timeframe unit
if trunc(delta) != 0:
relative_delta = trunc(delta)

if index == 0:
humanized = last_humanized
elif index == len(timeframes) - 1: # Must have at least 2 items
Expand All @@ -3333,7 +3350,7 @@ def describe_multi(
humanized += ", " + last_humanized

if not only_distance:
humanized = self._format_relative(humanized, timeframe, delta)
humanized = self._format_relative(humanized, timeframe, relative_delta)

return humanized

Expand Down
20 changes: 20 additions & 0 deletions tests/test_arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2285,6 +2285,26 @@ def test_no_floats_multi_gran(self):
)
assert humanize_string == "916 минути 40 няколко секунди назад"

# Dynamic Humanize Tests

def test_dynamic_on(self):
arw = arrow.Arrow(2013, 1, 1, 0, 0, 0)
later = arw.shift(seconds=3630)
humanize_string = arw.humanize(
later, granularity=["second", "hour", "day", "month", "year"], dynamic=True
)

assert humanize_string == "an hour and 30 seconds ago"

def test_dynamic_off(self):
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"],
)
assert humanize_string == "0 years 0 months 0 days an hour and 0 seconds ago"


@pytest.mark.usefixtures("time_2013_01_01")
class TestArrowHumanizeTestsWithLocale:
Expand Down
3 changes: 3 additions & 0 deletions tests/test_locales.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,9 @@ def test_describe_multi(self):
assert describe(seconds60) == "בעוד דקה"
assert describe(seconds60, only_distance=True) == "דקה"

fulltestend0 = [("years", 5), ("weeks", 1), ("hours", 1), ("minutes", 0)]
assert describe(fulltestend0) == "בעוד 5 שנים, שבוע, שעה ו־0 דקות"


@pytest.mark.usefixtures("lang_locale")
class TestMarathiLocale:
Expand Down