Skip to content

Fix fractional() emitting degenerate output when the fraction rounds to a whole - #354

Merged
hugovk merged 1 commit into
python-humanize:mainfrom
semx:fix-fractional-whole-rounding
Aug 29, 2026
Merged

Fix fractional() emitting degenerate output when the fraction rounds to a whole#354
hugovk merged 1 commit into
python-humanize:mainfrom
semx:fix-fractional-whole-rounding

Conversation

@semx

@semx semx commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Noticed while probing edge cases: fractional() emits degenerate output when the fractional part rounds to a whole number.

>>> import humanize
>>> humanize.fractional(2.9999999)
'2 1/1'        # expected '3'
>>> humanize.fractional(0.9999999)
'1/1'          # expected '1'
>>> humanize.fractional(0)
'0/1'          # expected '0'

Cause

After frac = Fraction(number - whole_number).limit_denominator(1000), when the fractional part reduces to a whole number the denominator is 1 (numerator ∈ {-1, 0, 1}). The existing special case only handled numerator == 0 (a plain integer input like 1.0), so:

  • numerator == 1 (the fraction rounded up to 1/1) fell through to the mixed-fraction branch → "2 1/1";
  • fractional(0) produced "0/1".

Changes proposed in this pull request:

  • Fold any whole-valued fractional part (denominator == 1) into the integer part, so the result reads as a normal integer.
  • Add regression tests for 0, 0.0, 2.9999999, 0.9999999, -2.9999999.

All existing tests pass (705 passed); ruff and black clean.

@semx

semx commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Gentle ping. This fixes fractional() emitting degenerate output (a whole number rendered as "2 1/1") when the fraction rounds to a whole; it now returns the plain integer, with tests for the boundary cases. docs and pre-commit.ci are green. Let me know if any change would help it land. Thanks!

@MohammedAlkindi

Copy link
Copy Markdown

Ran this on Windows 11, CPython 3.14.7, fresh clone.

On main (ce4147b): fractional(2.9999999)'2 1/1', fractional(0.9999999)'1/1', fractional(0)'0/1', fractional(-2.9999999)'-2 1/1' — reproduces exactly as described.

On this branch (0fe540d): the same inputs give '3', '1', '0', '-3'. I also checked cases the PR doesn't list: -0.9999999'-1' (the whole_number + numerator fold handles the negative-numerator case correctly), string inputs '2.9999999''3' and '0''0', 1000.9999999'1001', and nan still passes through as 'NaN'.

Applying the new test rows to main's number.py gives 5 failures with the expected assertions (e.g. assert '1/1' == '1'), so the tests do pin the bug. Full suite: main 715 passed / 74 skipped, branch 720 passed / 69 skipped; the skip difference is only because this branch predates the si_LK locale on main — cherry-picking 0fe540d onto current main is clean and gives 720 passed / 74 skipped.

One cross-reference for the maintainers: this change also covers what #351 and #374 fix (fractional(0)'0/1'), plus the 1/1 carry case those don't handle, so merging this would supersede both.

Limits: I tested on 3.14.7, not the full CI matrix, and didn't run lint/mypy.

@semx

semx commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful cross-check @MohammedAlkindi, that's really helpful. Your numbers match what I see here too: on main the fraction rounds up but the whole/numerator split isn't collapsed, so you get 2 1/1, 1/1, -1/1 and 0/1; on this branch the same inputs give 3, 1, -1 and 0, and the string/nan/large-value cases stay correct.

Good call on the overlap with #351 and #374 as well: both only address the fractional(0) case, and this PR covers that plus the x/x carry (e.g. 0.9999999 and 2.9999999) that they don't, so landing this one would make those redundant.

@hugovk it's a clean cherry-pick onto current main and CI is green; happy to rebase or adjust anything if it helps it land.

@codspeed-hq

codspeed-hq Bot commented Aug 29, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 15 untouched benchmarks


Comparing semx:fix-fractional-whole-rounding (828dd24) with main (e1a5c7c)

Open in CodSpeed

@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.56%. Comparing base (e1a5c7c) to head (828dd24).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #354   +/-   ##
=======================================
  Coverage   99.56%   99.56%           
=======================================
  Files          12       12           
  Lines         916      916           
=======================================
  Hits          912      912           
  Misses          4        4           
Flag Coverage Δ
macos-latest 97.59% <100.00%> (ø)
ubuntu-latest 97.59% <100.00%> (ø)
windows-latest 95.41% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hugovk hugovk added the changelog: Fixed For any bug fixes label Aug 29, 2026
@hugovk hugovk closed this Aug 29, 2026
@hugovk hugovk reopened this Aug 29, 2026
…to a whole

When limit_denominator(1000) reduces the fractional part to a whole number
(denominator == 1), fold it into the integer part instead of printing a
degenerate "N/1". For example fractional(2.9999999) returned "2 1/1"
instead of "3", fractional(0.9999999) returned "1/1" instead of "1",
and fractional(0) returned "0/1" instead of "0".
@hugovk
hugovk force-pushed the fix-fractional-whole-rounding branch from 7ea0b1f to 828dd24 Compare August 29, 2026 17:02
@hugovk hugovk closed this Aug 29, 2026
@hugovk hugovk reopened this Aug 29, 2026
@hugovk
hugovk enabled auto-merge (squash) August 29, 2026 17:04
@hugovk
hugovk merged commit 08cf2c3 into python-humanize:main Aug 29, 2026
44 checks passed
@semx

semx commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @hugovk! And a nod once more to @Sreekant13, whose #351 spotted the fractional(0) half of this first.

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

Labels

changelog: Fixed For any bug fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants