Skip to content

Fix spurious overflow in Fraction.multiplyBy for unreduced operands - #1769

Open
alhudz wants to merge 2 commits into
apache:masterfrom
alhudz:fraction-multiplyby-overflow
Open

Fix spurious overflow in Fraction.multiplyBy for unreduced operands#1769
alhudz wants to merge 2 commits into
apache:masterfrom
alhudz:fraction-multiplyby-overflow

Conversation

@alhudz

@alhudz alhudz commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Repro: Fraction.getFraction(-1, 46341).multiplyBy(Fraction.getFraction(100, 1000000)) throws ArithmeticException: overflow: mulPos, yet the same value with a reduced operand, Fraction.getFraction(-1, 46341).multiplyBy(Fraction.getFraction(1, 10000)), returns -1/463410000. divideBy and pow route through multiplyBy and behave the same way.
Cause: the Knuth 4.5.1 cross-gcd (d1 = gcd(numerator, fraction.denominator), d2 = gcd(fraction.numerator, denominator)) cancels the cross terms only and assumes both operands are already in lowest terms. A Fraction from getFraction is not reduced, so a factor shared inside an operand survives into the mulAndCheck/mulPosAndCheck product and overflows an int before getReducedFraction can cancel it, although the resulting numerator (-1) and denominator (463410000) both fit. The Javadoc only permits a throw when the resulting numerator or denominator exceeds Integer.MAX_VALUE.
Fix: reduce both operands before the cross-gcd multiply. The product still passes through getReducedFraction, so the value is unchanged and the operation now overflows only when the reduced result genuinely exceeds int. add/subtract are untouched; they return unreduced results by design.

  • Read the contribution guidelines for this project.
  • Read the ASF Generative Tooling Guidance if you use Artificial Intelligence (AI).
  • I used AI to create any part of, or all of, this pull request. Which AI tool was used to create this pull request, and to what extent did it contribute?
  • Run a successful build using the default Maven goal with mvn; that's mvn on the command line by itself.
  • Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied. This may not always be possible, but it is a best practice.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Each commit in the pull request should have a meaningful subject line and body. Note that a maintainer may squash commits during the merge process.

The Knuth 4.5.1 cross-gcd cancels only the cross terms and assumes both operands are reduced, so an unreduced operand can overflow the intermediate int product even when the reduced result fits. Reduce both operands before the multiply; divideBy and pow route through multiplyBy.

Copilot AI 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.

Pull request overview

Fixes Fraction.multiplyBy (and, transitively, divideBy/pow) throwing spurious ArithmeticException overflows when multiplying/dividing unreduced operands whose reduced result still fits in int.

Changes:

  • Reduce both operands before the Knuth 4.5.1 cross-GCD multiply to prevent intermediate int overflow when the final reduced result is representable.
  • Add unit tests covering multiply/divide with unreduced operands that previously triggered mulPos overflow.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/main/java/org/apache/commons/lang3/math/Fraction.java Reduces operands before cross-cancellation in multiplyBy to avoid intermediate overflow for unreduced inputs.
src/test/java/org/apache/commons/lang3/math/FractionTest.java Adds regression tests for multiply/divide cases where unreduced operands previously caused spurious overflow.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +772 to +776
final Fraction a = reduce();
final Fraction b = fraction.reduce();
final int d1 = greatestCommonDivisor(a.numerator, b.denominator);
final int d2 = greatestCommonDivisor(b.numerator, a.denominator);
return getReducedFraction(mulAndCheck(a.numerator / d1, b.numerator / d2), mulPosAndCheck(a.denominator / d2, b.denominator / d1));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, switched to reducing into local ints instead of calling reduce(), so no intermediate Fraction allocations now. Behaviour is unchanged: the repro getFraction(-1, 46341).multiplyBy(getFraction(100, 1000000)) still returns -1/463410000, and FractionTest plus checkstyle:check pass.

@garydgregory

Copy link
Copy Markdown
Member

@alhudz Please review Copilot comments.

@garydgregory

Copy link
Copy Markdown
Member

Hello @alhudz
Is this comment in the class Javadoc still warranted?

 * <p>
 * Note that this class is intended for common use cases, it is <em>int</em> based and thus suffers from various overflow issues. For a BigInteger based
 * equivalent, please see the Commons Math BigFraction class.
 * </p>

Thank you!

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.

3 participants