blob: c5e74bfa6ccffcaedaf729677521c2495e6b7e30 [file] [log] [blame]
package org.unicode.cldr.util;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.math.BigIntegerMath;
import com.ibm.icu.number.LocalizedNumberFormatter;
import com.ibm.icu.number.Notation;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
import com.ibm.icu.number.Precision;
import com.ibm.icu.text.UnicodeSet;
import com.ibm.icu.util.Freezable;
import com.ibm.icu.util.ICUException;
import com.ibm.icu.util.Output;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* Basic class for rational numbers. There is little attempt to optimize, since it will just be used
* for testing and data production within CLDR.
*
* @author markdavis
*/
public final class Rational extends Number implements Comparable<Rational> {
private static final long serialVersionUID = 1L;
private static final Pattern INT_POWER_10 = Pattern.compile("10*");
public final BigInteger numerator;
public final BigInteger denominator;
static final BigInteger BI_TWO = BigInteger.valueOf(2);
static final BigInteger BI_FIVE = BigInteger.valueOf(5);
static final BigInteger BI_MINUS_ONE = BigInteger.valueOf(-1);
static final BigInteger BI_TEN = BigInteger.valueOf(10);
static final BigDecimal BD_TWO = BigDecimal.valueOf(2);
static final BigDecimal BD_FIVE = BigDecimal.valueOf(5);
// Constraints:
// always stored in normalized form.
// no common factor > 1 (reduced)
// denominator never negative
// if numerator is zero, denominator is 1 or 0
// if denominator is zero, numerator is 1, -1, or 0
// NOTE, the constructor doesn't do any checking, so everything other than these goes
// through Rational.of(...)
public static final Rational ZERO = new Rational(BigInteger.ZERO, BigInteger.ONE);
public static final Rational ONE = new Rational(BigInteger.ONE, BigInteger.ONE);
public static final Rational NaN = new Rational(BigInteger.ZERO, BigInteger.ZERO);
public static final Rational INFINITY = new Rational(BigInteger.ONE, BigInteger.ZERO);
public static final Rational NEGATIVE_ONE = ONE.negate();
public static final Rational NEGATIVE_INFINITY = INFINITY.negate();
public static final Rational TWO = new Rational(BI_TWO, BigInteger.ONE);
public static final Rational TEN = new Rational(BI_TEN, BigInteger.ONE);
public static final Rational TENTH = TEN.reciprocal();
public static final char REPTEND_MARKER = '˙';
public static final String APPROX = "~";
public static class RationalParser implements Freezable<RationalParser> {
public static final RationalParser BASIC = new RationalParser().freeze();
private static final Splitter slashSplitter = Splitter.on('/').trimResults();
private static final Splitter starSplitter = Splitter.on('*').trimResults();
private Map<String, Rational> constants;
private Map<String, String> constantStatus;
public RationalParser() {
constants = new LinkedHashMap<>();
constantStatus = new LinkedHashMap<>();
}
public RationalParser(
Map<String, Rational> constants2, Map<String, String> constantStatus2) {
frozen = false;
constants = new LinkedHashMap<>(constants2);
constantStatus = new LinkedHashMap<>(constantStatus2);
}
public RationalParser addConstant(String id, String value, String status) {
final Rational parsed = parse(value);
if (constants.put(id, parsed) != null) {
throw new IllegalArgumentException("Can't reset constant " + id + " = " + value);
}
if (status != null) {
constantStatus.put(id, status);
}
return this;
}
/*
* input = comp (/ comp)?
* comp = comp2 (* comp2)*
* comp2 = digits (. digits)? | constant
* */
public Rational parse(String input) {
switch (input) {
case "NaN":
return NaN;
case "INF":
return INFINITY;
case "-INF":
return NEGATIVE_INFINITY;
}
if (input.startsWith(APPROX)) {
input = input.substring(1);
}
input = input.replace(",", ""); // allow commas anywhere
List<String> comps = slashSplitter.splitToList(input); // get num/den
try {
switch (comps.size()) {
case 1:
return process(comps.get(0));
case 2:
return process(comps.get(0)).divide(process(comps.get(1)));
default:
throw new IllegalArgumentException("too many slashes in " + input);
}
} catch (Exception e) {
throw new ICUException("bad input: " + input, e);
}
}
private Rational process(String string) {
Rational result = null;
string = string.replace(HUMAN_EXPONENT, "E");
for (String comp : starSplitter.split(string)) {
Rational ratComp = process2(comp);
result = result == null ? ratComp : result.multiply(ratComp);
}
return result;
}
static final UnicodeSet ALLOWED_CHARS =
new UnicodeSet("[-A-Za-z0-9_]").add(REPTEND_MARKER).freeze();
private Rational process2(String input) {
final char firstChar = input.charAt(0);
if (firstChar == '-' || (firstChar >= '0' && firstChar <= '9')) {
int pos = input.indexOf(REPTEND_MARKER);
if (pos < 0) {
return Rational.of(new BigDecimal(input));
}
// handle repeating fractions
String reptend = input.substring(pos + 1);
int rlen = reptend.length();
input = input.substring(0, pos) + reptend;
BigDecimal rf = new BigDecimal(input);
BigDecimal rfPow = new BigDecimal(input + reptend).scaleByPowerOfTen(rlen);
BigDecimal num = rfPow.subtract(rf);
Rational result = Rational.of(num);
Rational den =
Rational.of(
BigDecimal.ONE
.scaleByPowerOfTen(rlen)
.subtract(BigDecimal.ONE)); // could optimize
return result.divide(den);
} else {
if (!ALLOWED_CHARS.containsAll(input)) {
throw new IllegalArgumentException("Bad characters in: " + input);
}
Rational result = constants.get(input);
if (result == null) {
throw new IllegalArgumentException("Constant not defined: " + input);
}
return result;
}
}
boolean frozen = false;
@Override
public boolean isFrozen() {
return frozen;
}
@Override
public RationalParser freeze() {
if (!frozen) {
frozen = true;
constants = ImmutableMap.copyOf(constants);
constantStatus = ImmutableMap.copyOf(constantStatus);
}
return this;
}
@Override
public RationalParser cloneAsThawed() {
return new RationalParser(constants, constantStatus);
}
public Map<String, Rational> getConstants() {
return constants;
}
}
public static Rational of(long numerator, long denominator) {
return Rational.of(BigInteger.valueOf(numerator), BigInteger.valueOf(denominator));
}
public static Rational of(long numerator) {
return Rational.of(BigInteger.valueOf(numerator), BigInteger.ONE);
}
public static Rational of(BigInteger numerator, BigInteger denominator) {
int dComparison = denominator.compareTo(BigInteger.ZERO);
if (dComparison == 0) {
// catch equivalents to NaN, -INF, +INF
// 0/0 => NaN
// +/0 => INF
// -/0 => -INF
int nComparison = numerator.compareTo(BigInteger.ZERO);
return nComparison < 0 ? NEGATIVE_INFINITY : nComparison > 0 ? INFINITY : NaN;
} else {
// reduce to lowest form
BigInteger gcd = numerator.gcd(denominator);
if (gcd.compareTo(BigInteger.ONE) > 0) {
numerator = numerator.divide(gcd);
denominator = denominator.divide(gcd);
}
if (dComparison < 0) {
// ** NOTE: is already reduced, so safe to use constructor
return new Rational(numerator, denominator);
} else {
// ** NOTE: is already reduced, so safe to use constructor
return new Rational(numerator.negate(), denominator.negate());
}
}
}
public static Rational of(BigInteger numerator) {
// ** NOTE: is already reduced, so safe to use constructor
return new Rational(numerator, BigInteger.ONE);
}
public static Rational of(String simple) {
return RationalParser.BASIC.parse(simple);
}
private Rational(BigInteger numerator, BigInteger denominator) {
if (denominator.compareTo(BigInteger.ZERO) < 0) {
numerator = numerator.negate();
denominator = denominator.negate();
}
BigInteger gcd = numerator.gcd(denominator);
if (gcd.compareTo(BigInteger.ONE) > 0) {
numerator = numerator.divide(gcd);
denominator = denominator.divide(gcd);
}
this.numerator = numerator;
this.denominator = denominator;
}
public Rational add(Rational other) {
BigInteger newNumerator =
numerator.multiply(other.denominator).add(other.numerator.multiply(denominator));
BigInteger newDenominator = denominator.multiply(other.denominator);
return Rational.of(newNumerator, newDenominator);
}
public Rational subtract(Rational other) {
BigInteger newNumerator =
numerator
.multiply(other.denominator)
.subtract(other.numerator.multiply(denominator));
BigInteger newDenominator = denominator.multiply(other.denominator);
return Rational.of(newNumerator, newDenominator);
}
public Rational multiply(Rational other) {
BigInteger newNumerator = numerator.multiply(other.numerator);
BigInteger newDenominator = denominator.multiply(other.denominator);
return Rational.of(newNumerator, newDenominator);
}
public Rational divide(Rational other) {
BigInteger newNumerator = numerator.multiply(other.denominator);
BigInteger newDenominator = denominator.multiply(other.numerator);
return Rational.of(newNumerator, newDenominator);
}
public Rational pow(int i) {
return Rational.of(numerator.pow(i), denominator.pow(i));
}
public static Rational pow10(int i) {
return i > 0 ? TEN.pow(i) : TENTH.pow(-i);
}
public Rational reciprocal() {
return Rational.of(denominator, numerator);
}
public Rational negate() {
// ** NOTE: is already reduced, so safe to use constructor
return new Rational(numerator.negate(), denominator);
}
public BigDecimal toBigDecimal(MathContext mathContext) {
try {
return new BigDecimal(numerator).divide(new BigDecimal(denominator), mathContext);
} catch (Exception e) {
throw new IllegalArgumentException(
"Wrong math context for divide: " + this + ", " + mathContext);
}
}
public BigDecimal toBigDecimal() {
return toBigDecimal(MathContext.DECIMAL128); // prevent failures due to repeating fractions
}
@Override
public double doubleValue() {
if (denominator.equals(BigInteger.ZERO) && numerator.equals(BigInteger.ZERO)) {
return Double.NaN;
}
return toBigDecimal(MathContext.DECIMAL64).doubleValue();
}
@Override
public float floatValue() {
if (denominator.equals(BigInteger.ZERO) && numerator.equals(BigInteger.ZERO)) {
return Float.NaN;
}
return toBigDecimal(MathContext.DECIMAL32).floatValue();
}
public static Rational of(BigDecimal bigDecimal) {
// scale()
// If zero or positive, the scale is the number of digits to the right of the decimal point.
// If negative, the unscaled value of the number is multiplied by ten to the power of the
// negation of the scale.
// For example, a scale of -3 means the unscaled value is multiplied by 1000.
final int scale = bigDecimal.scale();
final BigInteger unscaled = bigDecimal.unscaledValue();
if (scale == 0) {
// ** NOTE: is already reduced, so safe to use constructor
return new Rational(unscaled, BigInteger.ONE);
} else if (scale >= 0) {
return Rational.of(unscaled, BigDecimal.ONE.movePointRight(scale).toBigInteger());
} else {
// ** NOTE: is already reduced, so safe to use constructor
return new Rational(
unscaled.multiply(BigDecimal.ONE.movePointLeft(scale).toBigInteger()),
BigInteger.ONE);
}
}
public static Rational of(double doubleValue) {
return of(new BigDecimal(doubleValue));
}
public enum FormatStyle {
/**
* Simple numerator / denominator, plain BigInteger.toString(), dropping " / 1". <br>
* The spaces are thin space (2009).
*/
plain,
/**
* Approximate value if small number of digits, dropping " / 1" <br>
* The spaces are thin space (2009).
*/
approx,
/**
* Repeating decimal where possible (otherwise = simple). The limit is 30 repeating digits.
*/
repeating,
/**
* Repeating decimal where possible (otherwise = simple). The limit is 1000 repeating
* digits.
*/
repeatingAll,
/**
* Formatted numerator / denominator, dropping " / 1" <br>
* The spaces are thin space (2009).
*/
formatted,
/** HTML Formatted numerator / denominator, using sup/sub, dropping "/1" */
html
}
@Override
public String toString() {
// could also return as "exact" decimal, if only factors of the denominator are 2 and 5
return toString(FormatStyle.plain);
}
private static final BigInteger NUM_OR_DEN_LIMIT = BigInteger.valueOf(10000000);
private static final BigInteger NUM_TIMES_DEN_LIMIT = BigInteger.valueOf(10000000);
private static final BigInteger BIG_HIGH = BigInteger.valueOf(10000000);
private static final double DOUBLE_HIGH = 10000000d;
private static final double DOUBLE_LOW = 0.001d;
static final String THIN_SPACE = "\u2009";
static final String DIVIDER = "/";
public String toString(FormatStyle style) {
boolean denIsOne = denominator.equals(BigInteger.ONE);
BigInteger absNumerator = numerator.abs();
String result;
switch (style) {
case plain:
result = numerator + (denIsOne ? "" : DIVIDER + denominator);
break;
case approx:
if (denIsOne) {
if (absNumerator.compareTo(BIG_HIGH) < 0) {
result = formatGroup.format(numerator).toString();
} else {
result = replaceE(formatSciSigDig5.format(numerator).toString());
}
} else {
int log10num = BigIntegerMath.log10(absNumerator, RoundingMode.UP);
int log10den = BigIntegerMath.log10(denominator, RoundingMode.UP);
if ((log10num <= 1 && log10den <= 4) || (log10num <= 4 && log10den <= 1)) {
result =
formatGroup.format(numerator)
+ DIVIDER
+ formatNoGroup.format(denominator);
} else {
final double doubleValue =
numerator.doubleValue() / denominator.doubleValue();
double absDoubleValue = Math.abs(doubleValue);
if (DOUBLE_LOW < absDoubleValue && absDoubleValue < DOUBLE_HIGH) {
result = formatSigDig5.format(doubleValue).toString();
} else {
result = formatSciSigDig5.format(doubleValue).toString();
}
}
}
break;
default:
Output<BigDecimal> newNumerator = new Output<>(new BigDecimal(numerator));
final BigInteger newDenominator = minimalDenominator(newNumerator, denominator);
final String numStr = formatGroup.format(newNumerator.value).toString();
final String denStr = formatNoGroup.format(newDenominator).toString();
denIsOne = newDenominator.equals(BigInteger.ONE);
int limit = 1000;
switch (style) {
case repeating:
limit = 30;
// fall through with smaller limit
case repeatingAll:
// if we come directly here, the limit is huge
result = toRepeating(limit);
if (result != null) { // null is returned if we can't fit into the limit
break;
}
// otherwise drop through to simple
case formatted:
// skip approximate test
result = denIsOne ? numStr : numStr + DIVIDER + denStr;
break;
case html:
// skip approximate test
return denIsOne
? numStr
: "<sup>" + numStr + "</sup>" + "/<sub>" + denStr + "<sub>";
default:
throw new UnsupportedOperationException();
}
}
Rational roundtrip = Rational.of(result);
return replaceE(roundtrip.equals(this) ? result : APPROX + result);
}
static final String HUMAN_EXPONENT = "×10ˆ";
private String replaceE(String format2) {
return format2.replace("E", HUMAN_EXPONENT);
}
private static final LocalizedNumberFormatter formatGroup =
NumberFormatter.with().precision(Precision.unlimited()).locale(Locale.ENGLISH);
private static final LocalizedNumberFormatter formatNoGroup =
NumberFormatter.with()
.precision(Precision.unlimited())
.grouping(GroupingStrategy.OFF)
.locale(Locale.ENGLISH);
private static final LocalizedNumberFormatter formatSigDig5 =
NumberFormatter.with()
.precision(Precision.maxSignificantDigits(5))
.locale(Locale.ENGLISH);
private static final LocalizedNumberFormatter formatSciSigDig5 =
NumberFormatter.with()
.precision(Precision.maxSignificantDigits(5))
.notation(Notation.engineering())
.locale(Locale.ENGLISH);
@Override
public int compareTo(Rational other) {
return numerator
.multiply(other.denominator)
.compareTo(other.numerator.multiply(denominator));
}
@Override
public boolean equals(Object that) {
return equals((Rational) that); // TODO fix later
}
public boolean equals(Rational that) {
return numerator.equals(that.numerator) && denominator.equals(that.denominator);
}
@Override
public int hashCode() {
return Objects.hash(numerator, denominator);
}
public Rational abs() {
return numerator.signum() >= 0 ? this : this.negate();
}
/**
* Goal is to be able to display rationals in a short but exact form, like 1,234,567/3 or
* 1.234567E21/3. To do this, find the smallest denominator (excluding powers of 2 and 5), and
* modify the numerator in the same way.
*
* @param denominator TODO
* @param current
* @return
*/
public static BigInteger minimalDenominator(
Output<BigDecimal> outNumerator, BigInteger denominator) {
if (denominator.equals(BigInteger.ONE) || denominator.equals(BigInteger.ZERO)) {
return denominator;
}
BigInteger newDenominator = denominator;
while (newDenominator.mod(BI_TWO).equals(BigInteger.ZERO)) {
newDenominator = newDenominator.divide(BI_TWO);
outNumerator.value = outNumerator.value.divide(BD_TWO);
}
BigInteger outDenominator = newDenominator;
while (newDenominator.mod(BI_FIVE).equals(BigInteger.ZERO)) {
newDenominator = newDenominator.divide(BI_FIVE);
outNumerator.value = outNumerator.value.divide(BD_FIVE);
}
return newDenominator;
}
public static class MutableLong {
public long value;
@Override
public String toString() {
return String.valueOf(value);
}
}
public static class ContinuedFraction {
public final List<BigInteger> sequence;
public ContinuedFraction(Rational source) {
List<BigInteger> _sequence = new ArrayList<>();
while (true) {
BigInteger floor = source.floor();
if (floor.compareTo(BigInteger.ZERO) < 0) {
floor = floor.subtract(BigInteger.ONE);
}
Rational remainder = source.subtract(Rational.of(floor, BigInteger.ONE));
_sequence.add(floor);
if (remainder.equals(Rational.ZERO)) {
break;
}
source = remainder.reciprocal();
}
sequence = ImmutableList.copyOf(_sequence);
}
public ContinuedFraction(long... items) {
List<BigInteger> _sequence = new ArrayList<>();
int count = 0;
for (long item : items) {
if (count != 0 && item < 0) {
throw new IllegalArgumentException("Only first item can be negative");
}
_sequence.add(BigInteger.valueOf(item));
count++;
}
sequence = ImmutableList.copyOf(_sequence);
}
public Rational toRational(List<Rational> intermediates) {
if (intermediates != null) {
intermediates.clear();
}
BigInteger h0 = BigInteger.ZERO;
BigInteger h1 = BigInteger.ONE;
BigInteger k0 = BigInteger.ONE;
BigInteger k1 = BigInteger.ZERO;
for (BigInteger item : sequence) {
BigInteger h2 = item.multiply(h1).add(h0);
BigInteger k2 = item.multiply(k1).add(k0);
if (intermediates != null) {
intermediates.add(Rational.of(h2, k2));
}
h0 = h1;
h1 = h2;
k0 = k1;
k1 = k2;
}
if (intermediates != null) {
Rational last = intermediates.get(intermediates.size() - 1);
intermediates.remove(intermediates.size() - 1);
return last;
} else {
return Rational.of(h1, k1);
}
}
@Override
public String toString() {
return sequence.toString();
}
@Override
public boolean equals(Object obj) {
return sequence.equals(((ContinuedFraction) obj).sequence);
}
@Override
public int hashCode() {
return sequence.hashCode();
}
}
public BigInteger floor() {
return numerator.divide(denominator);
}
/** The symmetric difference of a and b is 2 * abs(a - b) / (a + b) */
public Rational symmetricDiff(Rational b) {
return equals(b) ? ZERO : subtract(b).abs().multiply(TWO).divide(add(b));
}
/** Return repeating fraction, as long as the length is reasonable */
private String toRepeating(int stringLimit) {
BigInteger p = numerator;
BigInteger q = denominator;
StringBuilder s = new StringBuilder();
// Edge cases
final int pTo0 = p.compareTo(BigInteger.ZERO);
if (q.compareTo(BigInteger.ZERO) == 0) {
return pTo0 == 0 ? "NaN" : pTo0 > 0 ? "INF" : "-INF";
}
if (pTo0 == 0) {
return "0";
} else if (pTo0 < 0) {
p = p.negate();
s.append('-');
}
final int pToq = p.compareTo(q);
if (pToq == 0) {
s.append('1');
return s.toString();
} else if (pToq > 0) {
BigInteger intPart = p.divide(q);
s.append(formatNoGroup.format(intPart));
p = p.remainder(q);
if (p.compareTo(BigInteger.ZERO) == 0) {
return s.toString();
}
} else {
s.append('0');
}
// main loop
s.append(".");
int pos = -1; // all places are right to the radix point
Map<BigInteger, Integer> occurs = new HashMap<>();
while (!occurs.containsKey(p)) {
occurs.put(p, pos); // the position of the place with remainder p
BigInteger p10 = p.multiply(BigInteger.TEN);
BigInteger z = p10.divide(q); // index z of digit within: 0 ≤ z ≤ b-1
p = p10.subtract(z.multiply(q)); // 0 ≤ p < q
s = s.append(((char) ('0' + z.intValue()))); // append the character of the digit
if (p.equals(BigInteger.ZERO)) {
return s.toString();
} else if (s.length() > stringLimit) {
return null;
}
pos -= 1;
}
int L = occurs.get(p) - pos; // the length of the reptend (being < q)
s.insert(s.length() - L, REPTEND_MARKER);
return s.toString();
}
public boolean isPowerOfTen() {
Output<BigDecimal> newNumerator = new Output<>(new BigDecimal(numerator));
final BigInteger newDenominator = minimalDenominator(newNumerator, denominator);
if (!newDenominator.equals(BigInteger.ONE)) {
return false;
}
// hack, figure out later
String str = newNumerator.value.unscaledValue().toString();
if (INT_POWER_10.matcher(str).matches()) {
return true;
}
return false;
}
public String getIntPowerOfTen() {
// HACK, figure out better later
if (numerator.compareTo(BigInteger.ZERO) < 0) {
throw new IllegalArgumentException("Prefix label must be positive: " + this);
}
if (!numerator.equals(BigInteger.ONE) && !denominator.equals(BigInteger.ONE)) {
throw new IllegalArgumentException("Prefix label must be power of 10: " + this);
}
BigInteger value;
int sign;
if (numerator.equals(BigInteger.ONE)) {
value = denominator;
sign = -1;
} else {
value = numerator;
sign = 1;
}
String str = value.toString();
if (!INT_POWER_10.matcher(str).matches()) {
throw new IllegalArgumentException("Prefix label must be power of 10: " + this);
}
int result = str.length() - 1;
return String.valueOf(result * sign);
}
public static final Rational EPSILON = Rational.of(1, 1000000);
/** Approximately equal when symmetric difference of a and b < epsilon (default EPSILON) */
public boolean approximatelyEquals(Rational b, Rational epsilon) {
return symmetricDiff(b).compareTo(epsilon) < 0;
}
public boolean approximatelyEquals(Rational b) {
return approximatelyEquals(b, EPSILON);
}
public boolean approximatelyEquals(Number b) {
return approximatelyEquals(Rational.of(b.doubleValue()), EPSILON);
}
@Override
public int intValue() {
return toBigDecimal().intValue();
}
@Override
public long longValue() {
return toBigDecimal().longValue();
}
}