xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/util/Rational.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
1 package org.unicode.cldr.util;
2 
3 import com.google.common.base.Splitter;
4 import com.google.common.collect.ImmutableList;
5 import com.google.common.collect.ImmutableMap;
6 import com.google.common.math.BigIntegerMath;
7 import com.ibm.icu.number.LocalizedNumberFormatter;
8 import com.ibm.icu.number.Notation;
9 import com.ibm.icu.number.NumberFormatter;
10 import com.ibm.icu.number.NumberFormatter.GroupingStrategy;
11 import com.ibm.icu.number.Precision;
12 import com.ibm.icu.text.UnicodeSet;
13 import com.ibm.icu.util.Freezable;
14 import com.ibm.icu.util.ICUException;
15 import com.ibm.icu.util.Output;
16 import java.math.BigDecimal;
17 import java.math.BigInteger;
18 import java.math.MathContext;
19 import java.math.RoundingMode;
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.LinkedHashMap;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.Map;
26 import java.util.Objects;
27 import java.util.regex.Pattern;
28 
29 /**
30  * Basic class for rational numbers. There is little attempt to optimize, since it will just be used
31  * for testing and data production within CLDR.
32  *
33  * @author markdavis
34  */
35 public final class Rational extends Number implements Comparable<Rational> {
36     private static final long serialVersionUID = 1L;
37     private static final Pattern INT_POWER_10 = Pattern.compile("10*");
38     public final BigInteger numerator;
39     public final BigInteger denominator;
40 
41     static final BigInteger BI_TWO = BigInteger.valueOf(2);
42     static final BigInteger BI_FIVE = BigInteger.valueOf(5);
43     static final BigInteger BI_MINUS_ONE = BigInteger.valueOf(-1);
44     static final BigInteger BI_TEN = BigInteger.valueOf(10);
45 
46     static final BigDecimal BD_TWO = BigDecimal.valueOf(2);
47     static final BigDecimal BD_FIVE = BigDecimal.valueOf(5);
48 
49     // Constraints:
50     //   always stored in normalized form.
51     //   no common factor > 1 (reduced)
52     //   denominator never negative
53     //   if numerator is zero, denominator is 1 or 0
54     //   if denominator is zero, numerator is 1, -1, or 0
55 
56     // NOTE, the constructor doesn't do any checking, so everything other than these goes
57     // through Rational.of(...)
58     public static final Rational ZERO = new Rational(BigInteger.ZERO, BigInteger.ONE);
59     public static final Rational ONE = new Rational(BigInteger.ONE, BigInteger.ONE);
60     public static final Rational NaN = new Rational(BigInteger.ZERO, BigInteger.ZERO);
61     public static final Rational INFINITY = new Rational(BigInteger.ONE, BigInteger.ZERO);
62 
63     public static final Rational NEGATIVE_ONE = ONE.negate();
64     public static final Rational NEGATIVE_INFINITY = INFINITY.negate();
65 
66     public static final Rational TWO = new Rational(BI_TWO, BigInteger.ONE);
67     public static final Rational TEN = new Rational(BI_TEN, BigInteger.ONE);
68     public static final Rational TENTH = TEN.reciprocal();
69 
70     public static final char REPTEND_MARKER = '˙';
71     public static final String APPROX = "~";
72 
73     public static class RationalParser implements Freezable<RationalParser> {
74 
75         public static final RationalParser BASIC = new RationalParser().freeze();
76 
77         private static final Splitter slashSplitter = Splitter.on('/').trimResults();
78         private static final Splitter starSplitter = Splitter.on('*').trimResults();
79 
80         private Map<String, Rational> constants;
81         private Map<String, String> constantStatus;
82 
RationalParser()83         public RationalParser() {
84             constants = new LinkedHashMap<>();
85             constantStatus = new LinkedHashMap<>();
86         }
87 
RationalParser( Map<String, Rational> constants2, Map<String, String> constantStatus2)88         public RationalParser(
89                 Map<String, Rational> constants2, Map<String, String> constantStatus2) {
90             frozen = false;
91             constants = new LinkedHashMap<>(constants2);
92             constantStatus = new LinkedHashMap<>(constantStatus2);
93         }
94 
addConstant(String id, String value, String status)95         public RationalParser addConstant(String id, String value, String status) {
96             final Rational parsed = parse(value);
97             if (constants.put(id, parsed) != null) {
98                 throw new IllegalArgumentException("Can't reset constant " + id + " = " + value);
99             }
100             if (status != null) {
101                 constantStatus.put(id, status);
102             }
103             return this;
104         }
105 
106         /*
107          * input = comp (/ comp)?
108          * comp = comp2 (* comp2)*
109          * comp2 = digits (. digits)? | constant
110          * */
111 
parse(String input)112         public Rational parse(String input) {
113             switch (input) {
114                 case "NaN":
115                     return NaN;
116                 case "INF":
117                     return INFINITY;
118                 case "-INF":
119                     return NEGATIVE_INFINITY;
120             }
121             if (input.startsWith(APPROX)) {
122                 input = input.substring(1);
123             }
124             input = input.replace(",", ""); // allow commas anywhere
125             List<String> comps = slashSplitter.splitToList(input); // get num/den
126             try {
127                 switch (comps.size()) {
128                     case 1:
129                         return process(comps.get(0));
130                     case 2:
131                         return process(comps.get(0)).divide(process(comps.get(1)));
132                     default:
133                         throw new IllegalArgumentException("too many slashes in " + input);
134                 }
135             } catch (Exception e) {
136                 throw new ICUException("bad input: " + input, e);
137             }
138         }
139 
process(String string)140         private Rational process(String string) {
141             Rational result = null;
142             string = string.replace(HUMAN_EXPONENT, "E");
143             for (String comp : starSplitter.split(string)) {
144                 Rational ratComp = process2(comp);
145                 result = result == null ? ratComp : result.multiply(ratComp);
146             }
147             return result;
148         }
149 
150         static final UnicodeSet ALLOWED_CHARS =
151                 new UnicodeSet("[-A-Za-z0-9_]").add(REPTEND_MARKER).freeze();
152 
process2(String input)153         private Rational process2(String input) {
154             final char firstChar = input.charAt(0);
155             if (firstChar == '-' || (firstChar >= '0' && firstChar <= '9')) {
156                 int pos = input.indexOf(REPTEND_MARKER);
157                 if (pos < 0) {
158                     return Rational.of(new BigDecimal(input));
159                 }
160                 // handle repeating fractions
161                 String reptend = input.substring(pos + 1);
162                 int rlen = reptend.length();
163                 input = input.substring(0, pos) + reptend;
164 
165                 BigDecimal rf = new BigDecimal(input);
166                 BigDecimal rfPow = new BigDecimal(input + reptend).scaleByPowerOfTen(rlen);
167                 BigDecimal num = rfPow.subtract(rf);
168 
169                 Rational result = Rational.of(num);
170                 Rational den =
171                         Rational.of(
172                                 BigDecimal.ONE
173                                         .scaleByPowerOfTen(rlen)
174                                         .subtract(BigDecimal.ONE)); // could optimize
175                 return result.divide(den);
176             } else {
177                 if (!ALLOWED_CHARS.containsAll(input)) {
178                     throw new IllegalArgumentException("Bad characters in: " + input);
179                 }
180                 Rational result = constants.get(input);
181                 if (result == null) {
182                     throw new IllegalArgumentException("Constant not defined: " + input);
183                 }
184                 return result;
185             }
186         }
187 
188         boolean frozen = false;
189 
190         @Override
isFrozen()191         public boolean isFrozen() {
192             return frozen;
193         }
194 
195         @Override
freeze()196         public RationalParser freeze() {
197             if (!frozen) {
198                 frozen = true;
199                 constants = ImmutableMap.copyOf(constants);
200                 constantStatus = ImmutableMap.copyOf(constantStatus);
201             }
202             return this;
203         }
204 
205         @Override
cloneAsThawed()206         public RationalParser cloneAsThawed() {
207             return new RationalParser(constants, constantStatus);
208         }
209 
getConstants()210         public Map<String, Rational> getConstants() {
211             return constants;
212         }
213     }
214 
of(long numerator, long denominator)215     public static Rational of(long numerator, long denominator) {
216         return Rational.of(BigInteger.valueOf(numerator), BigInteger.valueOf(denominator));
217     }
218 
of(long numerator)219     public static Rational of(long numerator) {
220         return Rational.of(BigInteger.valueOf(numerator), BigInteger.ONE);
221     }
222 
of(BigInteger numerator, BigInteger denominator)223     public static Rational of(BigInteger numerator, BigInteger denominator) {
224         int dComparison = denominator.compareTo(BigInteger.ZERO);
225         if (dComparison == 0) {
226             // catch equivalents to NaN, -INF, +INF
227             // 0/0 => NaN
228             // +/0 => INF
229             // -/0 => -INF
230             int nComparison = numerator.compareTo(BigInteger.ZERO);
231             return nComparison < 0 ? NEGATIVE_INFINITY : nComparison > 0 ? INFINITY : NaN;
232         } else {
233             // reduce to lowest form
234             BigInteger gcd = numerator.gcd(denominator);
235             if (gcd.compareTo(BigInteger.ONE) > 0) {
236                 numerator = numerator.divide(gcd);
237                 denominator = denominator.divide(gcd);
238             }
239             if (dComparison < 0) {
240                 // ** NOTE: is already reduced, so safe to use constructor
241                 return new Rational(numerator, denominator);
242             } else {
243                 // ** NOTE: is already reduced, so safe to use constructor
244                 return new Rational(numerator.negate(), denominator.negate());
245             }
246         }
247     }
248 
of(BigInteger numerator)249     public static Rational of(BigInteger numerator) {
250         // ** NOTE: is already reduced, so safe to use constructor
251         return new Rational(numerator, BigInteger.ONE);
252     }
253 
of(String simple)254     public static Rational of(String simple) {
255         return RationalParser.BASIC.parse(simple);
256     }
257 
Rational(BigInteger numerator, BigInteger denominator)258     private Rational(BigInteger numerator, BigInteger denominator) {
259         if (denominator.compareTo(BigInteger.ZERO) < 0) {
260             numerator = numerator.negate();
261             denominator = denominator.negate();
262         }
263         BigInteger gcd = numerator.gcd(denominator);
264         if (gcd.compareTo(BigInteger.ONE) > 0) {
265             numerator = numerator.divide(gcd);
266             denominator = denominator.divide(gcd);
267         }
268         this.numerator = numerator;
269         this.denominator = denominator;
270     }
271 
add(Rational other)272     public Rational add(Rational other) {
273         BigInteger newNumerator =
274                 numerator.multiply(other.denominator).add(other.numerator.multiply(denominator));
275         BigInteger newDenominator = denominator.multiply(other.denominator);
276         return Rational.of(newNumerator, newDenominator);
277     }
278 
subtract(Rational other)279     public Rational subtract(Rational other) {
280         BigInteger newNumerator =
281                 numerator
282                         .multiply(other.denominator)
283                         .subtract(other.numerator.multiply(denominator));
284         BigInteger newDenominator = denominator.multiply(other.denominator);
285         return Rational.of(newNumerator, newDenominator);
286     }
287 
multiply(Rational other)288     public Rational multiply(Rational other) {
289         BigInteger newNumerator = numerator.multiply(other.numerator);
290         BigInteger newDenominator = denominator.multiply(other.denominator);
291         return Rational.of(newNumerator, newDenominator);
292     }
293 
divide(Rational other)294     public Rational divide(Rational other) {
295         BigInteger newNumerator = numerator.multiply(other.denominator);
296         BigInteger newDenominator = denominator.multiply(other.numerator);
297         return Rational.of(newNumerator, newDenominator);
298     }
299 
pow(int i)300     public Rational pow(int i) {
301         return Rational.of(numerator.pow(i), denominator.pow(i));
302     }
303 
pow10(int i)304     public static Rational pow10(int i) {
305         return i > 0 ? TEN.pow(i) : TENTH.pow(-i);
306     }
307 
reciprocal()308     public Rational reciprocal() {
309         return Rational.of(denominator, numerator);
310     }
311 
negate()312     public Rational negate() {
313         // ** NOTE: is already reduced, so safe to use constructor
314         return new Rational(numerator.negate(), denominator);
315     }
316 
toBigDecimal(MathContext mathContext)317     public BigDecimal toBigDecimal(MathContext mathContext) {
318         try {
319             return new BigDecimal(numerator).divide(new BigDecimal(denominator), mathContext);
320         } catch (Exception e) {
321             throw new IllegalArgumentException(
322                     "Wrong math context for divide: " + this + ", " + mathContext);
323         }
324     }
325 
toBigDecimal()326     public BigDecimal toBigDecimal() {
327         return toBigDecimal(MathContext.DECIMAL128); // prevent failures due to repeating fractions
328     }
329 
330     @Override
doubleValue()331     public double doubleValue() {
332         if (denominator.equals(BigInteger.ZERO) && numerator.equals(BigInteger.ZERO)) {
333             return Double.NaN;
334         }
335         return toBigDecimal(MathContext.DECIMAL64).doubleValue();
336     }
337 
338     @Override
floatValue()339     public float floatValue() {
340         if (denominator.equals(BigInteger.ZERO) && numerator.equals(BigInteger.ZERO)) {
341             return Float.NaN;
342         }
343         return toBigDecimal(MathContext.DECIMAL32).floatValue();
344     }
345 
of(BigDecimal bigDecimal)346     public static Rational of(BigDecimal bigDecimal) {
347         // scale()
348         // If zero or positive, the scale is the number of digits to the right of the decimal point.
349         // If negative, the unscaled value of the number is multiplied by ten to the power of the
350         // negation of the scale.
351         // For example, a scale of -3 means the unscaled value is multiplied by 1000.
352         final int scale = bigDecimal.scale();
353         final BigInteger unscaled = bigDecimal.unscaledValue();
354         if (scale == 0) {
355             // ** NOTE: is already reduced, so safe to use constructor
356             return new Rational(unscaled, BigInteger.ONE);
357         } else if (scale >= 0) {
358             return Rational.of(unscaled, BigDecimal.ONE.movePointRight(scale).toBigInteger());
359         } else {
360             // ** NOTE: is already reduced, so safe to use constructor
361             return new Rational(
362                     unscaled.multiply(BigDecimal.ONE.movePointLeft(scale).toBigInteger()),
363                     BigInteger.ONE);
364         }
365     }
366 
of(double doubleValue)367     public static Rational of(double doubleValue) {
368         return of(new BigDecimal(doubleValue));
369     }
370 
371     public enum FormatStyle {
372         /**
373          * Simple numerator / denominator, plain BigInteger.toString(), dropping " / 1". <br>
374          * The spaces are thin space (2009).
375          */
376         plain,
377         /**
378          * Approximate value if small number of digits, dropping " / 1" <br>
379          * The spaces are thin space (2009).
380          */
381         approx,
382         /**
383          * Repeating decimal where possible (otherwise = simple). The limit is 30 repeating digits.
384          */
385         repeating,
386         /**
387          * Repeating decimal where possible (otherwise = simple). The limit is 1000 repeating
388          * digits.
389          */
390         repeatingAll,
391         /**
392          * Formatted numerator / denominator, dropping " / 1" <br>
393          * The spaces are thin space (2009).
394          */
395         formatted,
396         /** HTML Formatted numerator / denominator, using sup/sub, dropping "/1" */
397         html
398     }
399 
400     @Override
toString()401     public String toString() {
402         // could also return as "exact" decimal, if only factors of the denominator are 2 and 5
403         return toString(FormatStyle.plain);
404     }
405 
406     private static final BigInteger NUM_OR_DEN_LIMIT = BigInteger.valueOf(10000000);
407     private static final BigInteger NUM_TIMES_DEN_LIMIT = BigInteger.valueOf(10000000);
408     private static final BigInteger BIG_HIGH = BigInteger.valueOf(10000000);
409     private static final double DOUBLE_HIGH = 10000000d;
410     private static final double DOUBLE_LOW = 0.001d;
411 
412     static final String THIN_SPACE = "\u2009";
413     static final String DIVIDER = "/";
414 
toString(FormatStyle style)415     public String toString(FormatStyle style) {
416         boolean denIsOne = denominator.equals(BigInteger.ONE);
417         BigInteger absNumerator = numerator.abs();
418         String result;
419         switch (style) {
420             case plain:
421                 result = numerator + (denIsOne ? "" : DIVIDER + denominator);
422                 break;
423             case approx:
424                 if (denIsOne) {
425                     if (absNumerator.compareTo(BIG_HIGH) < 0) {
426                         result = formatGroup.format(numerator).toString();
427                     } else {
428                         result = replaceE(formatSciSigDig5.format(numerator).toString());
429                     }
430                 } else {
431                     int log10num = BigIntegerMath.log10(absNumerator, RoundingMode.UP);
432                     int log10den = BigIntegerMath.log10(denominator, RoundingMode.UP);
433                     if ((log10num <= 1 && log10den <= 4) || (log10num <= 4 && log10den <= 1)) {
434                         result =
435                                 formatGroup.format(numerator)
436                                         + DIVIDER
437                                         + formatNoGroup.format(denominator);
438                     } else {
439                         final double doubleValue =
440                                 numerator.doubleValue() / denominator.doubleValue();
441                         double absDoubleValue = Math.abs(doubleValue);
442                         if (DOUBLE_LOW < absDoubleValue && absDoubleValue < DOUBLE_HIGH) {
443                             result = formatSigDig5.format(doubleValue).toString();
444                         } else {
445                             result = formatSciSigDig5.format(doubleValue).toString();
446                         }
447                     }
448                 }
449                 break;
450             default:
451                 Output<BigDecimal> newNumerator = new Output<>(new BigDecimal(numerator));
452                 final BigInteger newDenominator = minimalDenominator(newNumerator, denominator);
453                 final String numStr = formatGroup.format(newNumerator.value).toString();
454                 final String denStr = formatNoGroup.format(newDenominator).toString();
455                 denIsOne = newDenominator.equals(BigInteger.ONE);
456                 int limit = 1000;
457 
458                 switch (style) {
459                     case repeating:
460                         limit = 30;
461                         // fall through with smaller limit
462                     case repeatingAll:
463                         // if we come directly here, the limit is huge
464                         result = toRepeating(limit);
465                         if (result != null) { // null is returned if we can't fit into the limit
466                             break;
467                         }
468                         // otherwise drop through to simple
469                     case formatted:
470                         // skip approximate test
471                         result = denIsOne ? numStr : numStr + DIVIDER + denStr;
472                         break;
473                     case html:
474                         // skip approximate test
475                         return denIsOne
476                                 ? numStr
477                                 : "<sup>" + numStr + "</sup>" + "/<sub>" + denStr + "<sub>";
478                     default:
479                         throw new UnsupportedOperationException();
480                 }
481         }
482         Rational roundtrip = Rational.of(result);
483         return replaceE(roundtrip.equals(this) ? result : APPROX + result);
484     }
485 
486     static final String HUMAN_EXPONENT = "×10ˆ";
487 
replaceE(String format2)488     private String replaceE(String format2) {
489         return format2.replace("E", HUMAN_EXPONENT);
490     }
491 
492     private static final LocalizedNumberFormatter formatGroup =
493             NumberFormatter.with().precision(Precision.unlimited()).locale(Locale.ENGLISH);
494 
495     private static final LocalizedNumberFormatter formatNoGroup =
496             NumberFormatter.with()
497                     .precision(Precision.unlimited())
498                     .grouping(GroupingStrategy.OFF)
499                     .locale(Locale.ENGLISH);
500 
501     private static final LocalizedNumberFormatter formatSigDig5 =
502             NumberFormatter.with()
503                     .precision(Precision.maxSignificantDigits(5))
504                     .locale(Locale.ENGLISH);
505 
506     private static final LocalizedNumberFormatter formatSciSigDig5 =
507             NumberFormatter.with()
508                     .precision(Precision.maxSignificantDigits(5))
509                     .notation(Notation.engineering())
510                     .locale(Locale.ENGLISH);
511 
512     @Override
compareTo(Rational other)513     public int compareTo(Rational other) {
514         return numerator
515                 .multiply(other.denominator)
516                 .compareTo(other.numerator.multiply(denominator));
517     }
518 
519     @Override
equals(Object that)520     public boolean equals(Object that) {
521         return equals((Rational) that); // TODO fix later
522     }
523 
equals(Rational that)524     public boolean equals(Rational that) {
525         return numerator.equals(that.numerator) && denominator.equals(that.denominator);
526     }
527 
528     @Override
hashCode()529     public int hashCode() {
530         return Objects.hash(numerator, denominator);
531     }
532 
abs()533     public Rational abs() {
534         return numerator.signum() >= 0 ? this : this.negate();
535     }
536 
537     /**
538      * Goal is to be able to display rationals in a short but exact form, like 1,234,567/3 or
539      * 1.234567E21/3. To do this, find the smallest denominator (excluding powers of 2 and 5), and
540      * modify the numerator in the same way.
541      *
542      * @param denominator TODO
543      * @param current
544      * @return
545      */
minimalDenominator( Output<BigDecimal> outNumerator, BigInteger denominator)546     public static BigInteger minimalDenominator(
547             Output<BigDecimal> outNumerator, BigInteger denominator) {
548         if (denominator.equals(BigInteger.ONE) || denominator.equals(BigInteger.ZERO)) {
549             return denominator;
550         }
551         BigInteger newDenominator = denominator;
552         while (newDenominator.mod(BI_TWO).equals(BigInteger.ZERO)) {
553             newDenominator = newDenominator.divide(BI_TWO);
554             outNumerator.value = outNumerator.value.divide(BD_TWO);
555         }
556         BigInteger outDenominator = newDenominator;
557         while (newDenominator.mod(BI_FIVE).equals(BigInteger.ZERO)) {
558             newDenominator = newDenominator.divide(BI_FIVE);
559             outNumerator.value = outNumerator.value.divide(BD_FIVE);
560         }
561         return newDenominator;
562     }
563 
564     public static class MutableLong {
565         public long value;
566 
567         @Override
toString()568         public String toString() {
569             return String.valueOf(value);
570         }
571     }
572 
573     public static class ContinuedFraction {
574         public final List<BigInteger> sequence;
575 
ContinuedFraction(Rational source)576         public ContinuedFraction(Rational source) {
577             List<BigInteger> _sequence = new ArrayList<>();
578             while (true) {
579                 BigInteger floor = source.floor();
580                 if (floor.compareTo(BigInteger.ZERO) < 0) {
581                     floor = floor.subtract(BigInteger.ONE);
582                 }
583                 Rational remainder = source.subtract(Rational.of(floor, BigInteger.ONE));
584                 _sequence.add(floor);
585                 if (remainder.equals(Rational.ZERO)) {
586                     break;
587                 }
588                 source = remainder.reciprocal();
589             }
590             sequence = ImmutableList.copyOf(_sequence);
591         }
592 
ContinuedFraction(long... items)593         public ContinuedFraction(long... items) {
594             List<BigInteger> _sequence = new ArrayList<>();
595             int count = 0;
596             for (long item : items) {
597                 if (count != 0 && item < 0) {
598                     throw new IllegalArgumentException("Only first item can be negative");
599                 }
600                 _sequence.add(BigInteger.valueOf(item));
601                 count++;
602             }
603             sequence = ImmutableList.copyOf(_sequence);
604         }
605 
toRational(List<Rational> intermediates)606         public Rational toRational(List<Rational> intermediates) {
607             if (intermediates != null) {
608                 intermediates.clear();
609             }
610             BigInteger h0 = BigInteger.ZERO;
611             BigInteger h1 = BigInteger.ONE;
612             BigInteger k0 = BigInteger.ONE;
613             BigInteger k1 = BigInteger.ZERO;
614             for (BigInteger item : sequence) {
615                 BigInteger h2 = item.multiply(h1).add(h0);
616                 BigInteger k2 = item.multiply(k1).add(k0);
617                 if (intermediates != null) {
618                     intermediates.add(Rational.of(h2, k2));
619                 }
620                 h0 = h1;
621                 h1 = h2;
622                 k0 = k1;
623                 k1 = k2;
624             }
625             if (intermediates != null) {
626                 Rational last = intermediates.get(intermediates.size() - 1);
627                 intermediates.remove(intermediates.size() - 1);
628                 return last;
629             } else {
630                 return Rational.of(h1, k1);
631             }
632         }
633 
634         @Override
toString()635         public String toString() {
636             return sequence.toString();
637         }
638 
639         @Override
equals(Object obj)640         public boolean equals(Object obj) {
641             return sequence.equals(((ContinuedFraction) obj).sequence);
642         }
643 
644         @Override
hashCode()645         public int hashCode() {
646             return sequence.hashCode();
647         }
648     }
649 
floor()650     public BigInteger floor() {
651         return numerator.divide(denominator);
652     }
653 
654     /** The symmetric difference of a and b is 2 * abs(a - b) / (a + b) */
symmetricDiff(Rational b)655     public Rational symmetricDiff(Rational b) {
656         return equals(b) ? ZERO : subtract(b).abs().multiply(TWO).divide(add(b));
657     }
658 
659     /** Return repeating fraction, as long as the length is reasonable */
toRepeating(int stringLimit)660     private String toRepeating(int stringLimit) {
661         BigInteger p = numerator;
662         BigInteger q = denominator;
663         StringBuilder s = new StringBuilder();
664 
665         // Edge cases
666         final int pTo0 = p.compareTo(BigInteger.ZERO);
667         if (q.compareTo(BigInteger.ZERO) == 0) {
668             return pTo0 == 0 ? "NaN" : pTo0 > 0 ? "INF" : "-INF";
669         }
670         if (pTo0 == 0) {
671             return "0";
672         } else if (pTo0 < 0) {
673             p = p.negate();
674             s.append('-');
675         }
676         final int pToq = p.compareTo(q);
677         if (pToq == 0) {
678             s.append('1');
679             return s.toString();
680         } else if (pToq > 0) {
681             BigInteger intPart = p.divide(q);
682             s.append(formatNoGroup.format(intPart));
683             p = p.remainder(q);
684             if (p.compareTo(BigInteger.ZERO) == 0) {
685                 return s.toString();
686             }
687         } else {
688             s.append('0');
689         }
690 
691         // main loop
692         s.append(".");
693         int pos = -1; // all places are right to the radix point
694         Map<BigInteger, Integer> occurs = new HashMap<>();
695         while (!occurs.containsKey(p)) {
696             occurs.put(p, pos); // the position of the place with remainder p
697             BigInteger p10 = p.multiply(BigInteger.TEN);
698             BigInteger z = p10.divide(q); // index z of digit within: 0 ≤ z ≤ b-1
699             p = p10.subtract(z.multiply(q)); // 0 ≤ p < q
700             s = s.append(((char) ('0' + z.intValue()))); // append the character of the digit
701             if (p.equals(BigInteger.ZERO)) {
702                 return s.toString();
703             } else if (s.length() > stringLimit) {
704                 return null;
705             }
706             pos -= 1;
707         }
708         int L = occurs.get(p) - pos; // the length of the reptend (being < q)
709         s.insert(s.length() - L, REPTEND_MARKER);
710         return s.toString();
711     }
712 
isPowerOfTen()713     public boolean isPowerOfTen() {
714         Output<BigDecimal> newNumerator = new Output<>(new BigDecimal(numerator));
715         final BigInteger newDenominator = minimalDenominator(newNumerator, denominator);
716         if (!newDenominator.equals(BigInteger.ONE)) {
717             return false;
718         }
719         // hack, figure out later
720         String str = newNumerator.value.unscaledValue().toString();
721         if (INT_POWER_10.matcher(str).matches()) {
722             return true;
723         }
724         return false;
725     }
726 
getIntPowerOfTen()727     public String getIntPowerOfTen() {
728         // HACK, figure out better later
729         if (numerator.compareTo(BigInteger.ZERO) < 0) {
730             throw new IllegalArgumentException("Prefix label must be positive: " + this);
731         }
732         if (!numerator.equals(BigInteger.ONE) && !denominator.equals(BigInteger.ONE)) {
733             throw new IllegalArgumentException("Prefix label must be power of 10: " + this);
734         }
735         BigInteger value;
736         int sign;
737         if (numerator.equals(BigInteger.ONE)) {
738             value = denominator;
739             sign = -1;
740         } else {
741             value = numerator;
742             sign = 1;
743         }
744         String str = value.toString();
745         if (!INT_POWER_10.matcher(str).matches()) {
746             throw new IllegalArgumentException("Prefix label must be power of 10: " + this);
747         }
748         int result = str.length() - 1;
749         return String.valueOf(result * sign);
750     }
751 
752     public static final Rational EPSILON = Rational.of(1, 1000000);
753 
754     /** Approximately equal when symmetric difference of a and b < epsilon (default EPSILON) */
approximatelyEquals(Rational b, Rational epsilon)755     public boolean approximatelyEquals(Rational b, Rational epsilon) {
756         return symmetricDiff(b).compareTo(epsilon) < 0;
757     }
758 
approximatelyEquals(Rational b)759     public boolean approximatelyEquals(Rational b) {
760         return approximatelyEquals(b, EPSILON);
761     }
762 
approximatelyEquals(Number b)763     public boolean approximatelyEquals(Number b) {
764         return approximatelyEquals(Rational.of(b.doubleValue()), EPSILON);
765     }
766 
767     @Override
intValue()768     public int intValue() {
769         return toBigDecimal().intValue();
770     }
771 
772     @Override
longValue()773     public long longValue() {
774         return toBigDecimal().longValue();
775     }
776 }
777