• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.util;
2 
3 import java.math.BigDecimal;
4 import java.math.BigInteger;
5 import java.math.MathContext;
6 import java.util.ArrayList;
7 import java.util.HashMap;
8 import java.util.LinkedHashMap;
9 import java.util.List;
10 import java.util.Locale;
11 import java.util.Map;
12 import java.util.Objects;
13 import java.util.regex.Pattern;
14 
15 import com.google.common.base.Splitter;
16 import com.google.common.collect.ImmutableList;
17 import com.google.common.collect.ImmutableMap;
18 import com.ibm.icu.number.LocalizedNumberFormatter;
19 import com.ibm.icu.number.Notation;
20 import com.ibm.icu.number.NumberFormatter;
21 import com.ibm.icu.number.Precision;
22 import com.ibm.icu.text.UnicodeSet;
23 import com.ibm.icu.util.Freezable;
24 import com.ibm.icu.util.ICUException;
25 import com.ibm.icu.util.Output;
26 
27 /**
28  * Very basic class for rational numbers. No attempt to optimize, since it will just
29  * be used for testing within CLDR.
30  *
31  * @author markdavis
32  *
33  */
34 public final class Rational implements Comparable<Rational> {
35     private static final Pattern INT_POWER_10 = Pattern.compile("10*");
36     public final BigInteger numerator;
37     public final BigInteger denominator;
38 
39     // Constraints:
40     //   always stored in normalized form.
41     //   no common factor > 1 (reduced)
42     //   denominator never negative
43     //   if numerator is zero, denominator is 1 or 0
44     //   if denominator is zero, numerator is 1, -1, or 0
45 
46     public static final Rational ZERO = Rational.of(0);
47     public static final Rational ONE = Rational.of(1);
48     public static final Rational NEGATIVE_ONE = ONE.negate();
49 
50     public static final Rational INFINITY = Rational.of(1,0);
51     public static final Rational NEGATIVE_INFINITY = INFINITY.negate();
52     public static final Rational NaN = Rational.of(0,0);
53 
54     public static final Rational TEN = Rational.of(10, 1);
55     public static final Rational TENTH = TEN.reciprocal();
56 
57     public static final char REPTEND_MARKER = '˙';
58 
59     public static class RationalParser implements Freezable<RationalParser>{
60 
61         public static RationalParser BASIC = new RationalParser().freeze();
62 
63         private static Splitter slashSplitter = Splitter.on('/').trimResults();
64         private static Splitter starSplitter = Splitter.on('*').trimResults();
65 
66         private Map<String,Rational> constants = new LinkedHashMap<>();
67         private Map<String,String> constantStatus = new LinkedHashMap<>();
68 
addConstant(String id, String value, String status)69         public RationalParser addConstant(String id, String value, String status) {
70             if (constants.put(id, parse(value)) != null) {
71                 throw new IllegalArgumentException("Can't reset constant " + id + " = " + value);
72             }
73             if (status != null) {
74                 constantStatus.put(id, status);
75             }
76             return this;
77         }
78 
79         /*
80          * input = comp (/ comp)?
81          * comp = comp2 (* comp2)*
82          * comp2 = digits (. digits)? | constant
83          * */
84 
parse(String input)85         public Rational parse(String input) {
86             switch (input) {
87             case "NaN" : return NaN;
88             case "INF" : return INFINITY;
89             case "-INF": return NEGATIVE_INFINITY;
90             }
91             List<String> comps = slashSplitter.splitToList(input.replace(",", "")); // allow commas anywhere
92             try {
93                 switch (comps.size()) {
94                 case 1: return process(comps.get(0));
95                 case 2: return process(comps.get(0)).divide(process(comps.get(1)));
96                 default: throw new IllegalArgumentException("too many slashes in " + input);
97                 }
98             } catch (Exception e) {
99                 throw new ICUException("bad input: " + input, e);
100             }
101         }
102 
process(String string)103         private  Rational process(String string) {
104             Rational result = null;
105             for (String comp : starSplitter.split(string)) {
106                 Rational ratComp = process2(comp);
107                 result = result == null ? ratComp : result.multiply(ratComp);
108             }
109             return result;
110         }
111 
112         static final UnicodeSet ALLOWED_CHARS = new UnicodeSet("[-A-Za-z0-9_]").add(REPTEND_MARKER).freeze();
113 
process2(String input)114         private Rational process2(String input) {
115             final char firstChar = input.charAt(0);
116             if (firstChar == '-' || (firstChar >= '0' && firstChar <= '9')) {
117                 int pos = input.indexOf(REPTEND_MARKER);
118                 if (pos < 0) {
119                     return Rational.of(new BigDecimal(input));
120                 }
121                 // handle repeating fractions
122                 String reptend = input.substring(pos+1);
123                 int rlen = reptend.length();
124                 input = input.substring(0,pos) + reptend;
125 
126                 BigDecimal rf = new BigDecimal(input);
127                 BigDecimal rfPow = new BigDecimal(input+reptend).scaleByPowerOfTen(rlen);
128                 BigDecimal num = rfPow.subtract(rf);
129 
130                 Rational result = Rational.of(num);
131                 Rational den = Rational.of(BigDecimal.ONE.scaleByPowerOfTen(rlen).subtract(BigDecimal.ONE)); // could optimize
132                 return result.divide(den);
133             } else {
134                 if (!ALLOWED_CHARS.containsAll(input)) {
135                     throw new IllegalArgumentException("Bad characters in: " + input);
136                 }
137                 Rational result = constants.get(input);
138                 if (result == null) {
139                     throw new IllegalArgumentException("Constant not defined: " + input);
140                 }
141                 return result;
142             }
143         }
144 
145         boolean frozen = false;
146 
147         @Override
isFrozen()148         public boolean isFrozen() {
149             return frozen;
150         }
151 
152         @Override
freeze()153         public RationalParser freeze() {
154             if (!frozen) {
155                 frozen = true;
156                 constants = ImmutableMap.copyOf(constants);
157                 constantStatus = ImmutableMap.copyOf(constantStatus);
158             }
159             return this;
160         }
161 
162         @Override
cloneAsThawed()163         public RationalParser cloneAsThawed() {
164             throw new UnsupportedOperationException();
165         }
166 
getConstants()167         public Map<String, Rational> getConstants() {
168             return constants;
169         }
170     }
171 
of(long numerator, long denominator)172     public static Rational of(long numerator, long denominator) {
173         return new Rational(BigInteger.valueOf(numerator), BigInteger.valueOf(denominator));
174     }
175 
of(long numerator)176     public static Rational of(long numerator) {
177         return new Rational(BigInteger.valueOf(numerator), BigInteger.ONE);
178     }
179 
of(BigInteger numerator, BigInteger denominator)180     public static Rational of(BigInteger numerator, BigInteger denominator) {
181         return new Rational(numerator, denominator);
182     }
183 
of(BigInteger numerator)184     public static Rational of(BigInteger numerator) {
185         return new Rational(numerator, BigInteger.ONE);
186     }
187 
of(String simple)188     public static Rational of(String simple) {
189         return RationalParser.BASIC.parse(simple);
190     }
191 
Rational(BigInteger numerator, BigInteger denominator)192     private Rational(BigInteger numerator, BigInteger denominator) {
193         if (denominator.compareTo(BigInteger.ZERO) < 0) {
194             numerator = numerator.negate();
195             denominator = denominator.negate();
196         }
197         BigInteger gcd = numerator.gcd(denominator);
198         if (gcd.compareTo(BigInteger.ONE) > 0) {
199             numerator = numerator.divide(gcd);
200             denominator = denominator.divide(gcd);
201         }
202         this.numerator = numerator;
203         this.denominator = denominator;
204     }
205 
add(Rational other)206     public Rational add(Rational other) {
207         BigInteger gcd_den = denominator.gcd(other.denominator);
208         return new Rational(
209             numerator.multiply(other.denominator).divide(gcd_den)
210             .add(other.numerator.multiply(denominator).divide(gcd_den)),
211             denominator.multiply(other.denominator).divide(gcd_den)
212             );
213     }
214 
subtract(Rational other)215     public Rational subtract(Rational other) {
216         BigInteger gcd_den = denominator.gcd(other.denominator);
217         return new Rational(
218             numerator.multiply(other.denominator).divide(gcd_den)
219             .subtract(other.numerator.multiply(denominator).divide(gcd_den)),
220             denominator.multiply(other.denominator).divide(gcd_den)
221             );
222     }
223 
multiply(Rational other)224     public Rational multiply(Rational other) {
225         BigInteger gcd_num_oden = numerator.gcd(other.denominator);
226         boolean isZero = gcd_num_oden.equals(BigInteger.ZERO);
227         BigInteger smallNum = isZero ? numerator : numerator.divide(gcd_num_oden);
228         BigInteger smallODen = isZero ? other.denominator : other.denominator.divide(gcd_num_oden);
229 
230         BigInteger gcd_den_onum = denominator.gcd(other.numerator);
231         isZero = gcd_den_onum.equals(BigInteger.ZERO);
232         BigInteger smallONum = isZero ? other.numerator : other.numerator.divide(gcd_den_onum);
233         BigInteger smallDen = isZero ? denominator : denominator.divide(gcd_den_onum);
234 
235         return new Rational(smallNum.multiply(smallONum), smallDen.multiply(smallODen));
236     }
237 
pow(int i)238     public Rational pow(int i) {
239         return new Rational(numerator.pow(i), denominator.pow(i));
240     }
241 
pow10(int i)242     public static Rational pow10(int i) {
243         return i > 0 ? TEN.pow(i) : TENTH.pow(-i);
244     }
245 
divide(Rational other)246     public Rational divide(Rational other) {
247         return multiply(other.reciprocal());
248     }
249 
reciprocal()250     public Rational reciprocal() {
251         return new Rational(denominator, numerator);
252     }
253 
negate()254     public Rational negate() {
255         return new Rational(numerator.negate(), denominator);
256     }
257 
toBigDecimal(MathContext mathContext)258     public BigDecimal toBigDecimal(MathContext mathContext) {
259         try {
260             return new BigDecimal(numerator).divide(new BigDecimal(denominator), mathContext);
261         } catch (Exception e) {
262             throw new IllegalArgumentException("Wrong math context for divide: " + this + ", " + mathContext);
263         }
264     }
265 
doubleValue()266     public double doubleValue() {
267         if (denominator.equals(BigInteger.ZERO) && numerator.equals(BigInteger.ZERO)) {
268             return Double.NaN;
269         }
270         return new BigDecimal(numerator).divide(new BigDecimal(denominator), MathContext.DECIMAL64).doubleValue();
271     }
272 
273 
toBigDecimal()274     public BigDecimal toBigDecimal() {
275         return toBigDecimal(MathContext.UNLIMITED);
276     }
277 
of(BigDecimal bigDecimal)278     public static Rational of(BigDecimal bigDecimal) {
279         // scale()
280         // If zero or positive, the scale is the number of digits to the right of the decimal point.
281         // If negative, the unscaled value of the number is multiplied by ten to the power of the negation of the scale.
282         // For example, a scale of -3 means the unscaled value is multiplied by 1000.
283         final int scale = bigDecimal.scale();
284         final BigInteger unscaled = bigDecimal.unscaledValue();
285         if (scale == 0) {
286             return new Rational(unscaled, BigInteger.ONE);
287         } else if (scale >= 0) {
288             return new Rational(unscaled, BigDecimal.ONE.movePointRight(scale).toBigInteger());
289         } else {
290             return new Rational(unscaled.multiply(BigDecimal.ONE.movePointLeft(scale).toBigInteger()), BigInteger.ONE);
291         }
292     }
293 
294     public enum FormatStyle {plain, simple, repeating, html}
295 
296     @Override
toString()297     public String toString() {
298         // could also return as "exact" decimal, if only factors of the denominator are 2 and 5
299         return toString(FormatStyle.plain);
300     }
301 
302 
toString(FormatStyle style)303     public String toString(FormatStyle style) {
304         switch (style) {
305         case plain:
306             return numerator + (denominator.equals(BigInteger.ONE) ? "" : " / " + denominator);
307         }
308         Output<BigDecimal> newNumerator = new Output<>(new BigDecimal(numerator));
309         final BigInteger newDenominator = minimalDenominator(newNumerator, denominator);
310         final String numStr = format(newNumerator.value);
311         final String denStr = nf.format(newDenominator).toString();
312         final boolean denIsOne = newDenominator.equals(BigInteger.ONE);
313 
314         switch (style) {
315         case repeating:
316             String result = toRepeating(30); // limit of 30 on the repeating length, so we don't get unreasonable
317             if (result != null) {
318                 return result;
319             }
320             // otherwise drop through to simple
321         case simple: {
322             return denIsOne ? numStr : numStr + "/" + denStr;
323         }
324         case html: {
325             return denIsOne ? numStr : "<sup>" + numStr + "</sup>" + "/<sub>" + denStr + "<sub>";
326         }
327         default: throw new UnsupportedOperationException();
328         }
329     }
330 
331     static final LocalizedNumberFormatter nf = NumberFormatter.with().precision(Precision.unlimited()).locale(Locale.ENGLISH);
332     static final LocalizedNumberFormatter snf = NumberFormatter.with().precision(Precision.unlimited()).notation(Notation.engineering()).locale(Locale.ENGLISH);
333 
format(BigDecimal value)334     static String format(BigDecimal value) {
335         return nf.format(value).toString();
336         /*
337          * TODO Change 1000000 to 1×10<sup>-6</sup>, etc.
338          * Only for large/small numbers, eg:
339          * ≥ 1,000,000.0, => 1×10<sup>6</sup> — more than 6 trailing 0's
340          * ≤ 0.0000001 — more than 6 leading zeros
341          *
342          * relevant BigDecimal APIs:
343          * 123.4567 scale: 4 bigint: 1234567
344          * 123456700 scale: 0 bigint: 123456700
345          * 1234567 scale: 0 bigint: 1234567
346          * 0.1234567 scale: 7 bigint: 1234567
347          */
348         // return (Math.abs(newNumerator.scale()) > 10 ? snf : nf).format(newNumerator).toString();
349         //      Only do this for trailing zeros (above test isn't right)
350     }
351 
352     @Override
compareTo(Rational other)353     public int compareTo(Rational other) {
354         return numerator.multiply(other.denominator).compareTo(other.numerator.multiply(denominator));
355     }
356 
357     @Override
equals(Object that)358     public boolean equals(Object that) {
359         return equals((Rational)that); // TODO fix later
360     }
361 
equals(Rational that)362     public boolean equals(Rational that) {
363         return numerator.equals(that.numerator)
364             && denominator.equals(that.denominator);
365     }
366 
367     @Override
hashCode()368     public int hashCode() {
369         return Objects.hash(numerator, denominator);
370     }
371 
abs()372     public Rational abs() {
373         return numerator.signum() >= 0 ? this : this.negate();
374     }
375 
376     static final BigInteger BI_TWO = BigInteger.valueOf(2);
377     static final BigInteger BI_FIVE = BigInteger.valueOf(5);
378     static final BigInteger BI_MINUS_ONE = BigInteger.valueOf(-1);
379 
380     static final BigDecimal BD_TWO = BigDecimal.valueOf(2);
381     static final BigDecimal BD_FIVE = BigDecimal.valueOf(5);
382 
383 
384     /**
385      * Goal is to be able to display rationals in a short but exact form, like 1,234,567/3 or 1.234567E21/3.
386      * To do this, find the smallest denominator (excluding powers of 2 and 5), and modify the numerator in the same way.
387      * @param denominator TODO
388      * @param current
389      * @return
390      */
minimalDenominator(Output<BigDecimal> outNumerator, BigInteger denominator)391     public static BigInteger minimalDenominator(Output<BigDecimal> outNumerator, BigInteger denominator) {
392         if (denominator.equals(BigInteger.ONE) || denominator.equals(BigInteger.ZERO)) {
393             return denominator;
394         }
395         BigInteger newDenominator = denominator;
396         while (newDenominator.mod(BI_TWO).equals(BigInteger.ZERO)) {
397             newDenominator = newDenominator.divide(BI_TWO);
398             outNumerator.value = outNumerator.value.divide(BD_TWO);
399         }
400         BigInteger outDenominator = newDenominator;
401         while (newDenominator.mod(BI_FIVE).equals(BigInteger.ZERO)) {
402             newDenominator = newDenominator.divide(BI_FIVE);
403             outNumerator.value = outNumerator.value.divide(BD_FIVE);
404         }
405         return newDenominator;
406     }
407 
408 
409     public static class MutableLong {
410         public long value;
411         @Override
toString()412         public String toString() {
413             return String.valueOf(value);
414         }
415     }
416 
417     public static class ContinuedFraction {
418         public final List<BigInteger> sequence;
419 
ContinuedFraction(Rational source)420         public ContinuedFraction(Rational source) {
421             List<BigInteger> _sequence = new ArrayList<>();
422             while (true) {
423                 BigInteger floor = source.floor();
424                 if (floor.compareTo(BigInteger.ZERO) < 0) {
425                     floor = floor.subtract(BigInteger.ONE);
426                 }
427                 Rational remainder = source.subtract(Rational.of(floor, BigInteger.ONE));
428                 _sequence.add(floor);
429                 if (remainder.equals(Rational.ZERO)) {
430                     break;
431                 }
432                 source = remainder.reciprocal();
433             }
434             sequence = ImmutableList.copyOf(_sequence);
435         }
436 
ContinuedFraction(long... items)437         public ContinuedFraction(long... items) {
438             List<BigInteger> _sequence = new ArrayList<>();
439             int count = 0;
440             for (long item : items) {
441                 if (count != 0 && item < 0) {
442                     throw new IllegalArgumentException("Only first item can be negative");
443                 }
444                 _sequence.add(BigInteger.valueOf(item));
445                 count++;
446             }
447             sequence = ImmutableList.copyOf(_sequence);
448         }
449 
toRational(List<Rational> intermediates)450         public Rational toRational(List<Rational> intermediates) {
451             if (intermediates != null) {
452                 intermediates.clear();
453             }
454             BigInteger h0 = BigInteger.ZERO;
455             BigInteger h1 = BigInteger.ONE;
456             BigInteger k0 = BigInteger.ONE;
457             BigInteger k1 = BigInteger.ZERO;
458             for (BigInteger item : sequence) {
459                 BigInteger h2 = item.multiply(h1).add(h0);
460                 BigInteger k2 = item.multiply(k1).add(k0);
461                 if (intermediates != null) {
462                     intermediates.add(Rational.of(h2, k2));
463                 }
464                 h0 = h1;
465                 h1 = h2;
466                 k0 = k1;
467                 k1 = k2;
468             }
469             if (intermediates != null) {
470                 Rational last = intermediates.get(intermediates.size()-1);
471                 intermediates.remove(intermediates.size()-1);
472                 return last;
473             } else {
474                 return Rational.of(h1, k1);
475             }
476         }
477         @Override
toString()478         public String toString() {
479             return sequence.toString();
480         }
481 
482         @Override
equals(Object obj)483         public boolean equals(Object obj) {
484             return sequence.equals(((ContinuedFraction)obj).sequence);
485         }
486         @Override
hashCode()487         public int hashCode() {
488             return sequence.hashCode();
489         }
490     }
491 
floor()492     public BigInteger floor() {
493         return numerator.divide(denominator);
494     }
495 
symmetricDiff(Rational b)496     public Rational symmetricDiff(Rational b) {
497         return this.subtract(b)
498             .divide(this.abs().add(b.abs()))
499             .multiply(Rational.of(2))
500             ;
501     }
502 
503     /** Return repeating fraction, as long as the length is reasonable */
toRepeating(int stringLimit)504     private String toRepeating(int stringLimit) {
505         BigInteger p = numerator;
506         BigInteger q = denominator;
507         StringBuilder s = new StringBuilder();
508 
509         // Edge cases
510         final int pTo0 = p.compareTo(BigInteger.ZERO);
511         if (q.compareTo(BigInteger.ZERO) == 0) {
512             return pTo0 == 0 ? "NaN" : pTo0 > 0 ? "INF" : "-INF";
513         }
514         if (pTo0 == 0) {
515             return "0";
516         } else if (pTo0 < 0) {
517             p = p.negate();
518             s.append('-');
519         }
520         final int pToq = p.compareTo(q);
521         if (pToq == 0) {
522             s.append('1');
523             return s.toString();
524         } else if (pToq > 0) {
525             BigInteger intPart = p.divide(q);
526             s.append(nf.format(intPart));
527             p = p.remainder(q);
528             if (p.compareTo(BigInteger.ZERO) == 0) {
529                 return s.toString();
530             }
531         } else {
532             s.append('0');
533         }
534 
535         // main loop
536         s.append(".");
537         int pos = -1; // all places are right to the radix point
538         Map<BigInteger, Integer> occurs = new HashMap<>();
539         while (!occurs.containsKey(p)) {
540             occurs.put(p, pos);  // the position of the place with remainder p
541             BigInteger p10 = p.multiply(BigInteger.TEN);
542             BigInteger z = p10.divide(q); // index z of digit within: 0 ≤ z ≤ b-1
543             p = p10.subtract(z.multiply(q));    // 0 ≤ p < q
544             s = s.append(((char)('0' + z.intValue()))); // append the character of the digit
545             if (p.equals(BigInteger.ZERO)) {
546                 return s.toString();
547             } else if (s.length() > stringLimit) {
548                 return null;
549             }
550             pos -= 1;
551         }
552         int L = occurs.get(p)-pos; // the length of the reptend (being < q)
553         s.insert(s.length()-L, REPTEND_MARKER);
554         return s.toString();
555     }
556 
isPowerOfTen()557     public boolean isPowerOfTen() {
558         Output<BigDecimal> newNumerator = new Output<>(new BigDecimal(numerator));
559         final BigInteger newDenominator = minimalDenominator(newNumerator, denominator);
560         if (!newDenominator.equals(BigInteger.ONE)) {
561             return false;
562         }
563         // hack, figure out later
564         String str = newNumerator.value.unscaledValue().toString();
565         if (INT_POWER_10.matcher(str).matches()) {
566             return true;
567         }
568         return false;
569     }
570 }