Skip to content

Keep the sign of negative sub-second durations in in_words() - #1086

Open
kevin9327 wants to merge 2 commits into
python-pendulum:masterfrom
kevin9327:fix-in-words-negative-subseconds
Open

kevin9327 wants to merge 2 commits into
python-pendulum:masterfrom
kevin9327:fix-in-words-negative-subseconds

Conversation

@kevin9327

Copy link
Copy Markdown

Pull Request Check List

  • Added tests for changed code.
  • Updated documentation for changed code. (no doc change needed)

A negative duration shorter than a second loses its sign in in_words():

>>> pendulum.duration(seconds=-1).in_words()
'-1 second'
>>> pendulum.duration(microseconds=-123456).in_words()
'0.12 seconds'   # expected '-0.12 seconds'

The same happens for an interval whose end is less than a second before its start ('0.12 second'). Longer negative values already keep their sign (test_singular_negative_values expects -1 day). The sub-second branch formatted abs(self.microseconds), and microseconds is the only place the sign is kept for these values.

The fix formats self.microseconds without abs() in both Duration.in_words() and Interval.in_words(). Positive values and the zero case (0 microseconds) are unchanged.

Tests: added test_negative_subseconds to tests/duration/test_in_words.py and tests/interval/test_in_words.py. Both fail before the fix and pass after it. The full suite has 1872 passed and 1 failed here, run with PENDULUM_EXTENSIONS=0. The failure is tests/test_main.py::test_instance_with_aware_datetime_any_tzinfo, which also fails on master in this environment. ruff check and ruff format --check (v0.14.11, the pinned version) pass.

I wrote this change with help from an AI assistant. I reviewed it and ran the tests listed above.

@1AbdullahButt 1AbdullahButt left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The sign fix itself is right, and both Duration.in_words() and Interval.in_words() are covered. A rounding side effect is worth handling now that the sign isn't stripped, though. The value goes through :.2f, so anything between -0.005 s and 0 renders as negative zero:

>>> f"{-1000 / 1e6:.2f}", f"{-4999 / 1e6:.2f}"
('-0.00', '-0.00')

So pendulum.duration(microseconds=-1000).in_words() would become "-0.00 seconds". Before this PR it was "0.00 seconds", which is also odd, but at least not signed. Options: round first and drop the sign when the rounded value is zero, e.g.

value = round(self.microseconds / 1e6, 2)
count = f"{value + 0.0:.2f}"  # +0.0 turns -0.0 into 0.0

or fall through to the microsecond unit when abs(microseconds) < 5000. A test with microseconds=-1000 would pin whichever you choose.

Separately, and pre-existing: Duration uses plural(0) ("seconds") while Interval uses plural(1) ("second"), which is why the two new tests expect different words for the same value. It's not this PR's job to change that, but worth knowing if someone asks why the tests differ.

Reviewed with help from Claude Code.

@1AbdullahButt 1AbdullahButt left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The sign fix itself is right, and both Duration.in_words() and Interval.in_words() are covered. A rounding side effect is worth handling now that the sign isn't stripped, though. The value goes through :.2f, so anything between -0.005 s and 0 renders as negative zero:

>>> f"{-1000 / 1e6:.2f}", f"{-4999 / 1e6:.2f}"
('-0.00', '-0.00')

So pendulum.duration(microseconds=-1000).in_words() would become "-0.00 seconds". Before this PR it was "0.00 seconds", which is also odd, but at least not signed. Options: round first and drop the sign when the rounded value is zero, e.g.

value = round(self.microseconds / 1e6, 2)
count = f"{value + 0.0:.2f}"  # +0.0 turns -0.0 into 0.0

or fall through to the microsecond unit when abs(microseconds) < 5000. A test with microseconds=-1000 would pin whichever you choose.

Separately, and pre-existing: Duration uses plural(0) ("seconds") while Interval uses plural(1) ("second"), which is why the two new tests expect different words for the same value. It's not this PR's job to change that, but worth knowing if someone asks why the tests differ.

Reviewed with help from Claude Code.

Values between -0.005 s and 0 were formatting as "-0.00" after the
sign fix. Round to two decimals first and add 0.0 so -0.0 becomes 0.0.
@kevin9327

Copy link
Copy Markdown
Author

Good catch on the rounded negative zero. I went with the first option:

value = round(self.microseconds / 1e6, 2)
count = f"{value + 0.0:.2f}"

in both Duration and Interval, plus tests with microseconds=-1000 that expect "0.00 seconds" / "0.00 second". -0.12 for larger magnitudes is unchanged.

Agreed the plural(0) vs plural(1) mismatch is pre-existing and out of scope here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants