From 62a2c78e7bf76dd67c0682b2788248073b4918be Mon Sep 17 00:00:00 2001 From: Looly Date: Fri, 17 Jan 2025 19:40:47 +0800 Subject: [PATCH] add Fraction --- .../dromara/hutool/core/math/Fraction.java | 710 ++++++++++++++++++ 1 file changed, 710 insertions(+) create mode 100644 hutool-core/src/main/java/org/dromara/hutool/core/math/Fraction.java diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/math/Fraction.java b/hutool-core/src/main/java/org/dromara/hutool/core/math/Fraction.java new file mode 100644 index 000000000..a617cd523 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/math/Fraction.java @@ -0,0 +1,710 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.core.math; + +import java.math.BigInteger; +import java.util.Objects; + +/** + * {@code Fraction} 是一个 {@link Number} 实现, + * 可以准确地存储分数。 + * + *

此类是不可变的,并且与大多数接受 + * {@link Number} 的方法兼容。

+ * + *

请注意,此类适用于常见用例,它是基于 int 的, + * 因此容易受到各种溢出问题的影响。对于基于 BigInteger 的等效类, + * 请参见 Commons Math 库中的 BigFraction 类。

+ *

+ * 此类来自:Apache Commons Lang3 + * + * @since 6.0.0 + */ +public final class Fraction extends Number implements Comparable { + private static final long serialVersionUID = 1L; + + /** + * {@link Fraction} 表示 0. + */ + public static final Fraction ZERO = new Fraction(0, 1); + /** + * {@link Fraction} 表示 1. + */ + public static final Fraction ONE = new Fraction(1, 1); + + /** + * 从 {@code double} 值创建一个 {@code Fraction} 实例。 + * + *

此方法使用 + * 连分数算法,最多计算 25 个收敛项,并将分母限制在 10,000 以内。

+ * + * @param value 要转换的 double 值 + * @return 一个新的分数实例,接近该值 + * @throws ArithmeticException 如果 {@code |value| > Integer.MAX_VALUE} 或 {@code value = NaN} + * @throws ArithmeticException 如果计算出的分母为 {@code zero} + * @throws ArithmeticException 如果算法不收敛 + */ + public static Fraction of(double value) { + final int sign = value < 0 ? -1 : 1; + value = Math.abs(value); + if (value > Integer.MAX_VALUE || Double.isNaN(value)) { + throw new ArithmeticException("The value must not be greater than Integer.MAX_VALUE or NaN"); + } + final int wholeNumber = (int) value; + value -= wholeNumber; + + int numer0 = 0; // the pre-previous + int denom0 = 1; // the pre-previous + int numer1 = 1; // the previous + int denom1 = 0; // the previous + int numer2; // the current, setup in calculation + int denom2; // the current, setup in calculation + int a1 = (int) value; + int a2; + double x1 = 1; + double x2; + double y1 = value - a1; + double y2; + double delta1, delta2 = Double.MAX_VALUE; + double fraction; + int i = 1; + do { + delta1 = delta2; + a2 = (int) (x1 / y1); + x2 = y1; + y2 = x1 - a2 * y1; + numer2 = a1 * numer1 + numer0; + denom2 = a1 * denom1 + denom0; + fraction = (double) numer2 / (double) denom2; + delta2 = Math.abs(value - fraction); + a1 = a2; + x1 = x2; + y1 = y2; + numer0 = numer1; + denom0 = denom1; + numer1 = numer2; + denom1 = denom2; + i++; + } while (delta1 > delta2 && denom2 <= 10000 && denom2 > 0 && i < 25); + if (i == 25) { + throw new ArithmeticException("Unable to convert double to fraction"); + } + return getReducedFraction((numer0 + wholeNumber * denom0) * sign, denom0); + } + + /** + * 使用分数的两个部分 Y/Z 创建一个 {@code Fraction} 实例。 + * + *

任何负号都会被解析到分子上。

+ * + * @param numerator 分子,例如 '3/7' 中的 3 + * @param denominator 分母,例如 '3/7' 中的 7 + * @return 一个新的分数实例 + * @throws ArithmeticException 如果分母为 {@code zero} 或分母为 {@code negative} 且分子为 {@code Integer#MIN_VALUE} + */ + public static Fraction of(int numerator, int denominator) { + if (denominator == 0) { + throw new ArithmeticException("The denominator must not be zero"); + } + if (denominator < 0) { + if (numerator == Integer.MIN_VALUE || denominator == Integer.MIN_VALUE) { + throw new ArithmeticException("overflow: can't negate"); + } + numerator = -numerator; + denominator = -denominator; + } + return new Fraction(numerator, denominator); + } + + /** + * 使用分数的三个部分 X Y/Z 创建一个 {@code Fraction} 实例。 + * + *

负号必须传递到整数部分。

+ * + * @param whole 整数部分,例如 '一又七分之三' 中的 一 + * @param numerator 分子,例如 '一又七分之三' 中的 三 + * @param denominator 分母,例如 '一又七分之三' 中的 七 + * @return 一个新的分数实例 + * @throws ArithmeticException 如果分母为 {@code zero} + * @throws ArithmeticException 如果分母为负数 + * @throws ArithmeticException 如果分子为负数 + * @throws ArithmeticException 如果结果分子超过 {@code Integer.MAX_VALUE} + */ + public static Fraction of(final int whole, final int numerator, final int denominator) { + if (denominator == 0) { + throw new ArithmeticException("The denominator must not be zero"); + } + if (denominator < 0) { + throw new ArithmeticException("The denominator must not be negative"); + } + if (numerator < 0) { + throw new ArithmeticException("The numerator must not be negative"); + } + final long numeratorValue; + if (whole < 0) { + numeratorValue = whole * (long) denominator - numerator; + } else { + numeratorValue = whole * (long) denominator + numerator; + } + if (numeratorValue < Integer.MIN_VALUE || numeratorValue > Integer.MAX_VALUE) { + throw new ArithmeticException("Numerator too large to represent as an Integer."); + } + return new Fraction((int) numeratorValue, denominator); + } + + /** + * 从 {@link String} 创建一个 Fraction。 + * + *

接受的格式有:

+ * + *
    + *
  1. 包含点的 {@code double} 字符串
  2. + *
  3. 'X Y/Z'
  4. + *
  5. 'Y/Z'
  6. + *
  7. 'X'(一个简单的整数)
  8. + *
+ *

和一个 .。

+ * + * @param str 要解析的字符串,必须不为 {@code null} + * @return 新的 {@code Fraction} 实例 + * @throws NullPointerException 如果字符串为 {@code null} + * @throws NumberFormatException 如果数字格式无效 + */ + public static Fraction of(String str) { + Objects.requireNonNull(str, "str"); + // parse double format + int pos = str.indexOf('.'); + if (pos >= 0) { + return of(Double.parseDouble(str)); + } + + // parse X Y/Z format + pos = str.indexOf(' '); + if (pos > 0) { + final int whole = Integer.parseInt(str.substring(0, pos)); + str = str.substring(pos + 1); + pos = str.indexOf('/'); + if (pos < 0) { + throw new NumberFormatException("The fraction could not be parsed as the format X Y/Z"); + } + final int numer = Integer.parseInt(str.substring(0, pos)); + final int denom = Integer.parseInt(str.substring(pos + 1)); + return of(whole, numer, denom); + } + + // parse Y/Z format + pos = str.indexOf('/'); + if (pos < 0) { + // simple whole number + return of(Integer.parseInt(str), 1); + } + final int numer = Integer.parseInt(str.substring(0, pos)); + final int denom = Integer.parseInt(str.substring(pos + 1)); + return of(numer, denom); + } + + /** + * 使用分数的两个部分 Y/Z 创建一个简化后的 {@code Fraction} 实例。 + * + *

例如,如果输入参数表示 2/4,则创建的分数将是 1/2。

+ * + *

任何负号都会被解析到分子上。

+ * + * @param numerator 分子,例如 '3/7' 中的 3 + * @param denominator 分母,例如 '3/7' 中的 7 + * @return 一个新的分数实例,分子和分母已简化 + * @throws ArithmeticException 如果分母为 {@code zero} + */ + public static Fraction ofReduced(int numerator, int denominator) { + if (denominator == 0) { + throw new ArithmeticException("The denominator must not be zero"); + } + if (numerator == 0) { + return ZERO; // normalize zero. + } + // allow 2^k/-2^31 as a valid fraction (where k>0) + if (denominator == Integer.MIN_VALUE && (numerator & 1) == 0) { + numerator /= 2; + denominator /= 2; + } + if (denominator < 0) { + if (numerator == Integer.MIN_VALUE || denominator == Integer.MIN_VALUE) { + throw new ArithmeticException("overflow: can't negate"); + } + numerator = -numerator; + denominator = -denominator; + } + // simplify fraction. + final int gcd = MathUtil.gcd(numerator, denominator); + numerator /= gcd; + denominator /= gcd; + return new Fraction(numerator, denominator); + } + + /** + * 分子数部分(三个七分之三的 3)。 + */ + private final int numerator; + /** + * 分母是分数的一部分(三个七分之一中的 7)。 + */ + private final int denominator; + /** + * 缓存输出 hashCode(类是不可变的)。 + */ + private transient int hashCode; + /** + * 缓存输出 toString(类是不可变的)。 + */ + private transient String toString; + /** + * 缓存输出到 ProperString(类是不可变的)。 + */ + private transient String toProperString; + + /** + * 使用分数的两个部分构造一个 {@code Fraction} 实例,例如 Y/Z。 + * + * @param numerator 分子,例如在“七分之三”中的三 + * @param denominator 分母,例如在“七分之三”中的七 + */ + private Fraction(final int numerator, final int denominator) { + this.numerator = numerator; + this.denominator = denominator; + } + + /** + * 获取与此分数等值的正分数。 + *

更精确地说:{@code (fraction >= 0 ? this : -fraction)}

+ *

返回的分数不会被约简。

+ * + * @return 如果此分数为正,则返回 {@code this};否则返回一个新的正分数实例,其分子符号相反 + */ + public Fraction abs() { + if (numerator >= 0) { + return this; + } + return negate(); + } + + /** + * 将此分数与另一个分数相加,并返回约简后的结果。 + * 该算法遵循 Knuth 的 4.5.1 节。 + * + * @param fraction 要添加的分数,不能为空 + * @return 包含结果值的 {@code Fraction} 实例 + * @throws NullPointerException 如果传入的分数为 {@code null} + * @throws ArithmeticException 如果结果的分子或分母超出 {@code Integer.MAX_VALUE} + */ + public Fraction add(final Fraction fraction) { + return addSub(fraction, true /* add */); + } + + /** + * 使用 Knuth 4.5.1 节中描述的算法实现加法和减法。 + * + * @param fraction 要加或减的分数,不能为空 + * @param isAdd 如果为 true,则执行加法;如果为 false,则执行减法 + * @return 包含结果值的 {@code Fraction} 实例 + * @throws IllegalArgumentException 如果传入的分数为 {@code null} + * @throws ArithmeticException 如果结果的分子或分母无法表示为 {@code int} + */ + private Fraction addSub(final Fraction fraction, final boolean isAdd) { + Objects.requireNonNull(fraction, "fraction"); + // zero is identity for addition. + if (numerator == 0) { + return isAdd ? fraction : fraction.negate(); + } + if (fraction.numerator == 0) { + return this; + } + // if denominators are randomly distributed, d1 will be 1 about 61% + // of the time. + final int d1 = MathUtil.gcd(denominator, fraction.denominator); + if (d1 == 1) { + // result is ( (u*v' +/- u'v) / u'v') + final int uvp = mulAndCheck(numerator, fraction.denominator); + final int upv = mulAndCheck(fraction.numerator, denominator); + return new Fraction(isAdd ? addAndCheck(uvp, upv) : subAndCheck(uvp, upv), mulPosAndCheck(denominator, + fraction.denominator)); + } + // the quantity 't' requires 65 bits of precision; see knuth 4.5.1 + // exercise 7. we're going to use a BigInteger. + // t = u(v'/d1) +/- v(u'/d1) + final BigInteger uvp = BigInteger.valueOf(numerator).multiply(BigInteger.valueOf(fraction.denominator / d1)); + final BigInteger upv = BigInteger.valueOf(fraction.numerator).multiply(BigInteger.valueOf(denominator / d1)); + final BigInteger t = isAdd ? uvp.add(upv) : uvp.subtract(upv); + // but d2 doesn't need extra precision because + // d2 = gcd(t,d1) = gcd(t mod d1, d1) + final int tmodd1 = t.mod(BigInteger.valueOf(d1)).intValue(); + final int d2 = tmodd1 == 0 ? d1 : MathUtil.gcd(tmodd1, d1); + + // result is (t/d2) / (u'/d1)(v'/d2) + final BigInteger w = t.divide(BigInteger.valueOf(d2)); + if (w.bitLength() > 31) { + throw new ArithmeticException("overflow: numerator too large after multiply"); + } + return new Fraction(w.intValue(), mulPosAndCheck(denominator / d1, fraction.denominator / d2)); + } + + /** + * 将此分数除以另一个分数。 + * + * @param fraction 要除以的分数,不能为空 + * @return 包含结果值的 {@code Fraction} 实例 + * @throws NullPointerException 如果传入的分数为 {@code null} + * @throws ArithmeticException 如果要除以的分数为零 + * @throws ArithmeticException 如果结果的分子或分母超出 {@code Integer.MAX_VALUE} + */ + public Fraction divideBy(final Fraction fraction) { + Objects.requireNonNull(fraction, "fraction"); + if (fraction.numerator == 0) { + throw new ArithmeticException("The fraction to divide by must not be zero"); + } + return multiplyBy(fraction.invert()); + } + + /** + * 获取分数的分母部分。 + * + * @return 分数的分母部分 + */ + public int getDenominator() { + return denominator; + } + + /** + * 获取分数的分子部分。 + * + *

此方法可能返回一个大于分母的值,即一个假分数,例如 7/4 中的七。

+ * + * @return 分数的分子部分 + */ + public int getNumerator() { + return numerator; + } + + /** + * 获取真分数的分子部分,始终为正数。 + * + *

一个假分数 7/4 可以分解为真分数 1 3/4。 + * 此方法返回真分数中的 3。

+ * + *

如果分数为负数,例如 -7/4,可以分解为 -1 3/4, + * 因此此方法返回正数的真分数分子,即 3。

+ * + * @return 真分数的分子部分,始终为正数 + */ + public int getProperNumerator() { + return Math.abs(numerator % denominator); + } + + /** + * 获取真分数的整数部分,包括符号。 + * + *

一个假分数 7/4 可以分解为真分数 1 3/4。 + * 此方法返回真分数中的 1。

+ * + *

如果分数为负数,例如 -7/4,可以分解为 -1 3/4, + * 因此此方法返回真分数的整数部分 -1。

+ * + * @return 真分数的整数部分,包括符号 + */ + public int getProperWhole() { + return numerator / denominator; + } + + /** + * 获取此分数的倒数 (1/fraction)。 + * + *

返回的分数不会被约简。

+ * + * @return 一个新分数实例,其分子和分母互换 + * @throws ArithmeticException 如果分数表示零 + */ + public Fraction invert() { + if (numerator == 0) { + throw new ArithmeticException("Unable to invert zero."); + } + if (numerator == Integer.MIN_VALUE) { + throw new ArithmeticException("overflow: can't negate numerator"); + } + if (numerator < 0) { + return new Fraction(-denominator, -numerator); + } + return new Fraction(denominator, numerator); + } + + /** + * 将此分数与另一个分数相乘,并返回约简后的结果。 + * + * @param fraction 要乘以的分数,不能为空 + * @return 包含结果值的 {@code Fraction} 实例 + * @throws NullPointerException 如果传入的分数为 {@code null} + * @throws ArithmeticException 如果结果的分子或分母超出 {@code Integer.MAX_VALUE} + */ + public Fraction multiplyBy(final Fraction fraction) { + Objects.requireNonNull(fraction, "fraction"); + if (numerator == 0 || fraction.numerator == 0) { + return ZERO; + } + // knuth 4.5.1 + // make sure we don't overflow unless the result *must* overflow. + final int d1 = MathUtil.gcd(numerator, fraction.denominator); + final int d2 = MathUtil.gcd(fraction.numerator, denominator); + return ofReduced(mulAndCheck(numerator / d1, fraction.numerator / d2), mulPosAndCheck(denominator / d2, fraction.denominator / d1)); + } + + /** + * 获取此分数的负数 (-fraction)。 + * + *

返回的分数不会被约简。

+ * + * @return 一个新分数实例,其分子符号相反 + */ + public Fraction negate() { + // the positive range is one smaller than the negative range of an int. + if (numerator == Integer.MIN_VALUE) { + throw new ArithmeticException("overflow: too large to negate"); + } + return new Fraction(-numerator, denominator); + } + + /** + * 获取此分数的指定幂次。 + * + *

返回的分数已约简。

+ * + * @param power 要将分数提升到的幂次 + * @return 如果幂次为一,则返回 {@code this};如果幂次为零(即使分数等于零),则返回 {@link #ONE}; + * 否则返回一个新的分数实例,提升到相应的幂次 + * @throws ArithmeticException 如果结果的分子或分母超出 {@code Integer.MAX_VALUE} + */ + public Fraction pow(final int power) { + if (power == 1) { + return this; + } + if (power == 0) { + return ONE; + } + if (power < 0) { + if (power == Integer.MIN_VALUE) { // MIN_VALUE can't be negated. + return this.invert().pow(2).pow(-(power / 2)); + } + return this.invert().pow(-power); + } + final Fraction f = this.multiplyBy(this); + if (power % 2 == 0) { // if even... + return f.pow(power / 2); + } + return f.pow(power / 2).multiplyBy(this); + } + + /** + * 将分数约简为分子和分母的最小值,并返回结果。 + * + *

例如,如果此分数表示 2/4,则结果将是 1/2。

+ * + * @return 一个新的约简后的分数实例,如果无法进一步简化,则返回此实例 + */ + public Fraction reduce() { + if (numerator == 0) { + return equals(ZERO) ? this : ZERO; + } + final int gcd = MathUtil.gcd(Math.abs(numerator), denominator); + if (gcd == 1) { + return this; + } + return of(numerator / gcd, denominator / gcd); + } + + /** + * 从此分数中减去另一个分数的值,并返回约简后的结果。 + * + * @param fraction 要减去的分数,不能为空 + * @return 包含结果值的 {@code Fraction} 实例 + * @throws NullPointerException 如果传入的分数为 {@code null} + * @throws ArithmeticException 如果结果的分子或分母无法表示为 {@code int} + */ + public Fraction subtract(final Fraction fraction) { + return addSub(fraction, false /* subtract */); + } + + /** + * 获取此分数的真分数形式的字符串表示,格式为 X Y/Z。 + * + *

使用的格式为 '整数部分 分子/分母'。 + * 如果整数部分为零,则省略整数部分。如果分子为零,则仅返回整数部分。

+ * + * @return 分数的字符串表示形式 + */ + public String toProperString() { + if (toProperString == null) { + if (numerator == 0) { + toProperString = "0"; + } else if (numerator == denominator) { + toProperString = "1"; + } else if (numerator == -1 * denominator) { + toProperString = "-1"; + } else if ((numerator > 0 ? -numerator : numerator) < -denominator) { + // note that we do the magnitude comparison test above with + // NEGATIVE (not positive) numbers, since negative numbers + // have a larger range. otherwise numerator == Integer.MIN_VALUE + // is handled incorrectly. + final int properNumerator = getProperNumerator(); + if (properNumerator == 0) { + toProperString = Integer.toString(getProperWhole()); + } else { + toProperString = getProperWhole() + " " + properNumerator + "/" + getDenominator(); + } + } else { + toProperString = getNumerator() + "/" + getDenominator(); + } + } + return toProperString; + } + + @Override + public double doubleValue() { + return (double) numerator / (double) denominator; + } + + @Override + public float floatValue() { + return (float) numerator / (float) denominator; + } + + @Override + public int intValue() { + return numerator / denominator; + } + + @Override + public long longValue() { + return (long) numerator / denominator; + } + + @Override + public int compareTo(final Fraction other) { + if (equals(other)) { + return 0; + } + + // otherwise see which is less + final long first = (long) numerator * (long) other.denominator; + final long second = (long) other.numerator * (long) denominator; + return Long.compare(first, second); + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Fraction)) { + return false; + } + final Fraction other = (Fraction) obj; + return getNumerator() == other.getNumerator() && getDenominator() == other.getDenominator(); + } + + @Override + public int hashCode() { + if (hashCode == 0) { + // hash code update should be atomic. + hashCode = 37 * (37 * 17 + getNumerator()) + getDenominator(); + } + return hashCode; + } + + @Override + public String toString() { + if (toString == null) { + toString = getNumerator() + "/" + getDenominator(); + } + return toString; + } + + // region ----- private methods + + /** + * 添加两个整数,检查溢出。 + * + * @param x 加数 + * @param y 加数 + * @return 和 {@code x+y} + * @throws ArithmeticException 如果结果不能表示为 int + */ + private static int addAndCheck(final int x, final int y) { + final long s = (long) x + (long) y; + if (s < Integer.MIN_VALUE || s > Integer.MAX_VALUE) { + throw new ArithmeticException("overflow: add"); + } + return (int) s; + } + + /** + * 乘以两个整数,并检查是否溢出。 + * + * @param x 一个乘数 + * @param y 另一个乘数 + * @return 乘积 {@code x*y} + * @throws ArithmeticException 如果结果不能表示为 int 类型 + */ + private static int mulAndCheck(final int x, final int y) { + final long m = (long) x * (long) y; + if (m < Integer.MIN_VALUE || m > Integer.MAX_VALUE) { + throw new ArithmeticException("overflow: mul"); + } + return (int) m; + } + + /** + * 乘以两个正整数,并检查是否溢出。 + * + * @param x 一个正乘数 + * @param y 另一个正乘数 + * @return 乘积 {@code x*y} + * @throws ArithmeticException 如果结果不能表示为 int 类型 或者任意一个参数不是正数 + */ + private static int mulPosAndCheck(final int x, final int y) { + /* assert x>=0 && y>=0; */ + final long m = (long) x * (long) y; + if (m > Integer.MAX_VALUE) { + throw new ArithmeticException("overflow: mulPos"); + } + return (int) m; + } + + /** + * 从一个整数中减去另一个整数,并检查是否溢出。 + * + * @param x 被减数 + * @param y 减数 + * @return 差值 {@code x - y} + * @throws ArithmeticException 如果结果不能表示为 int 类型 + */ + private static int subAndCheck(final int x, final int y) { + final long s = (long) x - (long) y; + if (s < Integer.MIN_VALUE || s > Integer.MAX_VALUE) { + throw new ArithmeticException("overflow: add"); + } + return (int) s; + } + // endregion +} +