diff --git a/src/java.base/share/classes/java/math/BigRational.java b/src/java.base/share/classes/java/math/BigRational.java new file mode 100644 index 00000000000..d415d66dfa3 --- /dev/null +++ b/src/java.base/share/classes/java/math/BigRational.java @@ -0,0 +1,545 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.math; + +import java.util.Arrays; + +/** + * Rational numbers using {@link BigInteger} values to store the + * numerator and denominator. + * + *

TODO: write-up on what a rational number is, (ideally) obeys the + * field axioms, etc. Describe canonical form, and so on. + *

Blanket statement on null-handling. + *

Explain (a/b) and (c/d) notational convention. + * + * @apiNote + * As a prototype, this class is not intended for production work. + * + * @since Valhalla + */ +@jdk.internal.MigratedValueClass +@jdk.internal.ValueBased +//@Deprecated +public final /* value */ class BigRational { + private static final OrderedNumerical WITNESS = + new OrderedNumerical() { + public BigRational add(BigRational addend, + BigRational augend) { + return BigRational.add(addend, augend); + } + + public BigRational subtract(BigRational minuend, + BigRational subtrahend) { + return BigRational.subtract(minuend, subtrahend); + } + + public BigRational multiply(BigRational multiplier, + BigRational multiplicand) { + return BigRational.multiply(multiplier, multiplicand); + } + + public BigRational divide(BigRational dividend, + BigRational divisor) { + return BigRational.divide(dividend, divisor); + } + + public BigRational remainder(BigRational dividend, + BigRational divisor) { + return BigRational.remainder( dividend, divisor); + } + + public BigRational plus(BigRational operand) { + return BigRational.plus(operand); + } + + public BigRational negate(BigRational operand) { + return BigRational.negate( operand); + } + + public boolean lessThan(BigRational op1, BigRational op2) { + return BigRational.compare(op1, op2) < 0; + } + + public boolean lessThanEqual(BigRational op1, BigRational op2) { + return BigRational.compare(op1, op2) <= 0; + } + }; + + /** + * Witness for the {@link Numerical} interface. + */ + public static final __witness Numerical NUM = WITNESS; + + /** + * Witness for the {@link Orderable} interface. + */ + public static final __witness Orderable ORD = WITNESS; + + /* + * If the rational number is negative, the numerator is + * negative. If the rational number is positive, both numerator + * and denominator are positive. + * + * In this iteration, nothing complicated is attempted to give + * integral values a distinguished representation, such as having + * the denominator be a null pointer rather than BigInteger.ONE. + */ + private final BigInteger num; + private final BigInteger dem; + + /* + * Used in lieu of a two-element array to return a pair of values. + */ + private record Reduced(BigInteger num, BigInteger dem) {} + + private Reduced reduceToLowest(BigInteger num, BigInteger dem) { + if (BigInteger.ZERO.equals(dem)) { + throw new ArithmeticException("Attempt to divide by zero"); + } + if (BigInteger.ZERO.equals(num)) { + return new Reduced(BigInteger.ZERO, BigInteger.ONE); + } else { + int sign = num.signum() * dem.signum(); + var numAbs = num.abs(); + var demAbs = dem.abs(); + BigInteger gcd = numAbs.gcd(demAbs); + + var reducedNum = numAbs.divide(gcd); + if (sign == -1) { + reducedNum = reducedNum.negate(); + } + + var reducedDem = demAbs.divide(gcd); + return new Reduced(reducedNum, reducedDem); + } + } + + private BigRational(BigInteger num, BigInteger dem) { + var reduced = reduceToLowest(num, dem); + this.num = reduced.num; + this.dem = reduced.dem; + } + + /** + * Canonical representation of zero, 0/1. + */ + public static final BigRational ZERO = + new BigRational(BigInteger.ZERO, BigInteger.ONE); + + private boolean isZero() { + return BigInteger.ZERO.equals(num); + } + + /** + * Canonical representation of one, 1/1. + */ + public static final BigRational ONE = + new BigRational(BigInteger.ONE, BigInteger.ONE); + + /** + * {@return a rational number equal to the argument} + * + * @param i the argument + */ + public static BigRational valueOf(int i) { + return new BigRational(BigInteger.valueOf(i), + BigInteger.ONE); + } + + /** + * {@return a rational number equal to the argument} + * + * @param ell the argument + */ + public static BigRational valueOf(long ell) { + return new BigRational(BigInteger.valueOf(ell), + BigInteger.ONE); + } + + /** + * {@return a rational number equal to the value of the argument + * if the argument is finite} An {@code ArithmeticException} is + * thrown for non-finite arguments, NaNs and infinities. + * + * TODO: signed zero special case; spell out infinity and NaN behavior. + * + * @param d the argument + * @throws ArithmeticException if the argument is not finite. + */ + public static BigRational valueOf(double d) { + if (!Double.isFinite(d)) { + throw new ArithmeticException("non-finite double value " + d); + } + // Could do the double -> rational conversion more directly too + return valueOf(new BigDecimal(d)); // Leverage exact double -> BigDecimal conversion + } + + /** + * {@return a rational number equal to the numeric value of the + * argument} + * + * @param bd the argument + */ + public static BigRational valueOf(BigDecimal bd) { + BigInteger unscaledValue = copyIfNeeded(bd.unscaledValue()); + int scale = bd.scale(); + + var scalingFactor = BigDecimal.TEN.pow(Math.abs(scale)).toBigIntegerExact(); + + // Construct final return value as a product or quotient of + // unscaledValue and scalingFactor. + if (scale >= 0) { + return divide( valueOf(unscaledValue), valueOf(scalingFactor)); + } else { + return multiply(valueOf(unscaledValue), valueOf(scalingFactor)); + } + } + + /** + * {@return a rational number equal to the argument} + * + * @param bi the argument + */ + public static BigRational valueOf(BigInteger bi) { + return new BigRational(copyIfNeeded(bi), BigInteger.ONE); + } + + /** + * {@return a rational number constructed from the string argument} + * TODO: Talk about the grammar of the accepted strings, etc. + * + * @param s the string to parse into a rational number + * @throws NumberFormatException if the string is not a valid representation + */ + public static BigRational valueOf(String s) { + // Look for "/" if not present, parse as big int, ... + int slashOffset = s.indexOf("/"); + try { + if (slashOffset == -1) { + return valueOf(new BigInteger(s), BigInteger.ONE); + } else { + BigInteger numerator = new BigInteger(s.substring(0, slashOffset)); + BigInteger denominator = new BigInteger(s.substring(slashOffset + 1)); + return valueOf(numerator, denominator); + } + } catch(NumberFormatException cause) { + var nfe = new NumberFormatException("Malformed rational number"); + nfe.initCause(cause); + throw nfe; + } + } + + /** + * {@return a rational number equal to the ratio of the arguments, + * in lowest terms} + * + * @param numerator the numerator + * @param denominator the denominator + */ + public static BigRational valueOf(int numerator, int denominator) { + return new BigRational(BigInteger.valueOf(numerator), + BigInteger.valueOf(denominator)); + } + + /** + * {@return a rational number equal to the ratio of the arguments, + * in lowest terms} + * + * @param numerator the numerator + * @param denominator the denominator + */ + public static BigRational valueOf(long numerator, long denominator) { + return new BigRational(BigInteger.valueOf(numerator), + BigInteger.valueOf(denominator)); + } + + /** + * {@return a rational number equal to the ratio of the arguments, + * in lowest terms} + * + * @param numerator the numerator + * @param denominator the denominator + */ + public static BigRational valueOf(BigInteger numerator, + BigInteger denominator) { + return new BigRational(copyIfNeeded(numerator), + copyIfNeeded(denominator)); + } + + /* + * Guard against unknown BigInteger subclass. + */ + private static BigInteger copyIfNeeded(BigInteger bi) { + return (bi.getClass() == BigInteger.class) ? + bi : + new BigInteger(bi.toByteArray()); + } + + /** + * {@return a {@code BigDecimal} with the numerical value of the + * argument rounding according to the context settings} + * + * @param rat the rational number to convert + * @param mc the math context to use in the conversion + */ + public static BigDecimal toBigDecimal(BigRational rat, MathContext mc) { + return (new BigDecimal(rat.num)).divide(new BigDecimal(rat.dem), mc); + } + + /** + * {@return a {@code BigDecimal} equal to the argument, if possible} + * + * @param rat the rational number to convert + * @throws ArithmeticException is an exact conversion is not possible + */ + public static BigDecimal toBigDecimalExact(BigRational rat) { + return (new BigDecimal(rat.num)).divide(new BigDecimal(rat.dem)); + } + + /** + * Addition operation, binary operator "{@code +}". + * + * @implSpec + * (a/b) + (c/d) = + * (a·d + b·c) / (b·d) + * + * @param addend the first operand + * @param augend the second operand + * @return the sum of the operands + */ + public static BigRational add(BigRational addend, + BigRational augend) { + if(addend.isZero()) { + return augend; + } + if(augend.isZero()) { + return addend; + } + + var a = addend.num; var b = addend.dem; + var c = augend.num; var d = augend.dem; + + // (ad + bc)/(bd) + return valueOf(a.multiply(d).add(b.multiply(c)), + b.multiply(d)); + } + + /** + * Subtraction operation, binary operator "{@code -}". + * + * @implSpec + * (a/b) − (c/d) = + * (a·db·c) / (b·d) + * + * @param minuend the first operand + * @param subtrahend the second operand + * @return the difference of the operands + */ + public static BigRational subtract(BigRational minuend, + BigRational subtrahend) { + // Equivalent to (ad - bc)/(bd) + return add(minuend, negate(subtrahend)); + } + + /** + * Multiplication operation, binary operator "{@code *}". + * + * @implSpec + * (a/b) * (c/d) = + * (a·c)/(b·d) + * + * @param multiplier the first operand + * @param multiplicand the second operand + * @return the product of the operands + */ + public static BigRational multiply(BigRational multiplier, + BigRational multiplicand) { + if (multiplier.isZero() || multiplicand.isZero() ) { + return ZERO; + } + + var a = multiplier.num; var b = multiplier.dem; + var c = multiplicand.num; var d = multiplicand.dem; + + // (ac)/(bd) + return valueOf(a.multiply(c), b.multiply(d)); + } + + /** + * Division operation, binary operator "{@code /}". + * + * @implSpec + * (a/b) / (c/d) = + * (a·d)/(b·c) + * + * @throws ArithmeticException if the divisor is zero + * @param dividend the first operand + * @param divisor the second operand + * @return the quotient of the operands + */ + public static BigRational divide(BigRational dividend, + BigRational divisor) { + if (divisor.isZero() ) { + throw new ArithmeticException("attempt to divide by zero"); + } + if (dividend.isZero()) { + return ZERO; + } + + var a = dividend.num; var b = dividend.dem; + var c = divisor.num; var d = divisor.dem; + + // (ad)/(bc) + return valueOf(a.multiply(d), b.multiply(c)); + } + + /** + * Remainder operation, binary operator "{@code %}". + * + * @throws ArithmeticException if the divisor is zero + * @param dividend the first operand + * @param divisor the second operand + * @return the remainder of the operands + */ + public static BigRational remainder(BigRational dividend, + BigRational divisor) { + throw new ArithmeticException("tbd"); + } + + /** + * Unary plus operation, unary operator "{@code +}". + * + * @implSpec + * This implementation returns the operand. + * + * @param operand the operand + * @return unary plus of the operand + */ + public static BigRational plus(BigRational operand) { + return operand; + } + + /** + * Negation operation, unary operator "{@code -}". + * + * @param operand the operand + * @return the negation of the operand + */ + public static BigRational negate(BigRational operand) { + return new BigRational(operand.num.negate(), operand.dem); + } + + + /** + * {@return the reciprocal of the operand} + * + * @param operand the operand + */ + public static BigRational reciprocal(BigRational operand) { + if (operand.isZero()) { + throw new ArithmeticException("Zero does not have a reciprocal"); + } + return new BigRational(operand.dem, operand.num); + } + + /** + * Returns the exponentiation of the first operand by the second + * operand. + * + * @param operand the operand + * @param exponent the exponent + * @return the first operands raised to the power the second operand + */ + public static BigRational pow(BigRational operand, int exponent) { + if (exponent == 0) { + return BigRational.ONE; + } + + // TODO: screen out Integer.MIN_VALUE + // For a negative exponent, flip numerator and denominator + return (exponent > 0) ? + valueOf(operand.num.pow( exponent), operand.dem.pow( exponent)): + valueOf(operand.dem.pow(-exponent), operand.num.pow(-exponent)); + } + + /** + * {@return the comparison of two specified rational values} + * + * @implSpec + * {@code ad < bd}, etc. + * + * @param op1 the first operand + * @param op2 the second operand + * + * @see Double#compare(double, double) + * @see BigInteger#compareTo(BigInteger) + */ + public static int compare(BigRational op1, + BigRational op2) { + var a = op1.num; var b = op1.dem; + var c = op2.num; var d = op2.dem; + + int signumOp1 = a.signum(); + int signumOp2 = c.signum(); + + if (signumOp1 < signumOp2) { + return -1; + } else if (signumOp1 > signumOp2) { + return 1; + } else { // signumOp1 == signumOp2 + if (signumOp1 == 0) { + return 0; + } else { + var a_d = a.multiply(d); + var b_c = b.multiply(c); + + return a_d.compareTo(b_c); + } + } + } + + /** + * {@return the hashcode of this rational} + */ + @Override + public int hashCode() { + return java.util.Objects.hash(num.hashCode(), dem.hashCode()); + } + + /** + * {@return a string representing the rational} + */ + @Override + public String toString() { + // Numerator always represented + var sb = new StringBuilder(num.toString()); + if (!BigInteger.ONE.equals(dem)) { + sb.append("/").append(dem.toString()); + } + return sb.toString(); + } +}