001// Rational.java
002import java.util.Scanner;
003import java.util.Locale;
004import java.util.NoSuchElementException;
005import java.util.Formatter;
006import java.util.Formattable;
007import java.util.regex.Pattern;
008import static java.util.FormattableFlags.ALTERNATE;
009import static java.util.FormattableFlags.LEFT_JUSTIFY;
010import java.util.FormatFlagsConversionMismatchException;
011import java.util.IllegalFormatPrecisionException;
012import java.util.IllegalFormatException;
013
014/**
015 * A rational number is stored as an integer pair (numerator, denominator).
016 * Rational is a value-based class.
017 * @author H.Drachenfels
018 * @version 30.6.2025
019 */
020public final class Rational extends Number
021    implements Comparable<Rational>, Formattable {
022
023    /**
024     * The number 0 as Rational.
025     */
026    public static final Rational ZERO = new Rational(0, 1);
027
028    /**
029     * The number 1 as Rational.
030     */
031    public static final Rational ONE = new Rational(1, 1);
032
033    /**
034     * The largest positive value of type Rational, Integer.MAX_VALUE.
035     */
036    public static final Rational MAX_VALUE
037        = new Rational(Integer.MAX_VALUE, 1);
038
039    /**
040     * The smallest positive nonzero value of type Rational,
041     * 1 / Integer.MAX_VALUE.
042     */
043    public static final Rational MIN_VALUE
044        = new Rational(1, Integer.MAX_VALUE);
045
046    /**
047     * The numerator part of the rational number.
048     */
049    private final int numerator;
050
051    /**
052     * The denominator part of the rational number.
053     * Always greater or equal 1.
054     * The denominator is 1 if numerator is 0.
055     */
056    private final int denominator;
057
058    /**
059     * Rational number with numerator n and denominator d.
060     * For denominator 0 an IllegalArgumentException is thrown.
061     * @param n the numerator
062     * @param d the denominator (must not be 0)
063     * @return a Rational with the specified value
064     */
065    public static Rational valueOf(int n, int d) {
066        return new Rational(n, d);
067    }
068
069    /**
070     * Converts an integer to a rational number.
071     * This factory method is provided in preference to an (int) constructor
072     * because it allows for reuse of frequently used Rationals.
073     * @param n the integer to convert
074     * @return a Rational with the specified value
075     */
076    public static Rational valueOf(int n) {
077        switch (n) {
078        case 0:
079            return ZERO;
080        case 1:
081            return ONE;
082        case Integer.MAX_VALUE:
083            return MAX_VALUE;
084        default:
085            return new Rational(n, 1);
086        }
087    }
088
089    /**
090     * Converts a string to a rational number.
091     * The string must have the format specified by toString.
092     * Otherwise, NumberFormatException is thrown.
093     * @param s the string to convert
094     * @return a Rational with the specified value
095     */
096    public static Rational valueOf(String s) {
097        Scanner sc = new Scanner(s);
098        if (!sc.hasNext(FORMAT)) {
099            throw new NumberFormatException(s);
100        }
101
102        sc.useDelimiter("/");
103        return new Rational(sc.nextInt(), sc.nextInt());
104    }
105
106    private static final Pattern FORMAT = Pattern.compile("^[+-]?\\d+/\\d+$");
107
108    private Rational(long n, long d) {
109        if (d == 0) {
110            throw new IllegalArgumentException("invalid denominator 0");
111        }
112
113        if (n == 0) {
114            this.numerator = 0;
115            this.denominator = 1;
116            return;
117        }
118
119        int sign = Long.signum(n) * Long.signum(d);
120        long nn = Math.abs(n);
121        long dd = Math.abs(d);
122
123        if (dd != 1) {
124            long f = gcd(nn, dd);
125            nn /= f;
126            dd /= f;
127        }
128
129        if (nn > Integer.MAX_VALUE) {
130            throw new ArithmeticException(
131                String.format("numerator overflow: %d/%d", sign * nn, dd));
132        }
133
134        if (dd > Integer.MAX_VALUE) {
135            throw new ArithmeticException(
136                String.format("denominator overflow: %d/%d", sign * nn, dd));
137        }
138
139        this.numerator = sign * (int) nn;
140        this.denominator = (int) dd;
141    }
142
143    /**
144     * Computes the greates common divisor using Euclid's algorithm.
145     * For negative numbers, the result is undefined.
146     * @param a a positive number
147     * @param b a positive number
148     * @return the gcd of a and b
149     */
150    private static long gcd(long a, long b) {
151        while (a > 0 &&  b > 0) {
152            if (a > b) {
153                a = a % b;
154            } else {
155                b = b % a;
156            }
157        }
158
159        return Math.max(a, b);
160    }
161
162    /**
163     * Returns the numerator part of the reduced rational number.
164     * @return the numerator
165     */
166    public int getNumerator() {
167        return this.numerator;
168    }
169
170    /**
171     * Returns the denominator part of the reduced rational number.
172     * @return the denominator
173     */
174    public int getDenominator() {
175        return this.denominator;
176    }
177
178    /**
179     * Returns the signum function of this Rational.
180     * @return -1, 0 or 1 as the value of this Rational is
181     *         negative, zero or positive.
182     */
183    public int signum() {
184        return Integer.signum(this.numerator);
185    }
186
187    /**
188     * Returns a Rational with inverted sign.
189     * @return -this
190     */
191    public Rational negate() {
192        return new Rational(-this.numerator, this.denominator);
193    }
194
195    /**
196     * Returns a Rational with numerator and denominator swapped.
197     * @return 1/this
198     */
199    public Rational invert() {
200        return new Rational(this.denominator, this.numerator);
201    }
202
203    /**
204     * Returns a Rational whose value is (this + r).
205     * @param r value to be added to this Rational
206     * @return this + r
207     */
208    public Rational add(Rational r) {
209        if (this.denominator == r.denominator) {
210            return new Rational((long) this.numerator + r.numerator,
211                                this.denominator);
212        }
213
214        long n = (long) this.numerator * r.denominator
215                 + (long) r.numerator * this.denominator;
216        long d = (long) this.denominator * r.denominator;
217        return new Rational(n, d);
218    }
219
220    /**
221     * Returns a Rational whose value is (this - r).
222     * @param r value to be subtracted from this Rational
223     * @return this - r
224     */
225    public Rational subtract(Rational r) {
226        if (this.denominator == r.denominator) {
227            return new Rational((long) this.numerator - r.numerator,
228                                this.denominator);
229        }
230
231        long n = (long) this.numerator * r.denominator
232                 - (long) r.numerator * this.denominator;
233        long d = (long) this.denominator * r.denominator;
234        return new Rational(n, d);
235    }
236
237    /**
238     * Returns a Rational whose value is (this * r).
239     * @param r value to be multiplied by this Rational
240     * @return this * r
241     */
242    public Rational multiply(Rational r) {
243        long n = (long) this.numerator * r.numerator;
244        long d = (long) this.denominator * r.denominator;
245        return new Rational(n, d);
246    }
247
248    /**
249     * Returns a Rational whose value is (this / r).
250     * @param r value by which this Rational is to be divided
251     * @return this / r
252     */
253    public Rational divide(Rational r) {
254        if (r.numerator == 0) {
255            throw new IllegalArgumentException("invalid divisor: " + r);
256        }
257
258        long n = (long) this.numerator * r.denominator;
259        long d = (long) this.denominator * r.numerator;
260        return new Rational(n, d);
261    }
262
263    /**
264     * Returns a string representation of this Rational.
265     * The string representation of the numerator, including the sign, 
266     * is followed by '/' and the string representation of the denominator.
267     * @return string representation of this Rational.
268     */
269    @Override
270    public String toString() {
271        return new StringBuilder()
272                   .append(this.numerator)
273                   .append('/')
274                   .append(this.denominator)
275                   .toString();
276    }
277
278    @Override
279    public boolean equals(Object o) {
280        if (o instanceof Rational) {
281            Rational r = (Rational) o;
282            return this.numerator == r.numerator
283                   && this.denominator == r.denominator;
284        }
285
286        return false;
287    }
288
289    @Override
290    public int hashCode() {
291        return this.numerator * 31 + this.denominator;
292    }
293
294    @Override
295    public int intValue() {
296        return (int) ((double) this.numerator / this.denominator);
297    }
298
299    @Override
300    public long longValue() {
301        return (long) ((double) this.numerator / this.denominator);
302    }
303
304    @Override
305    public double doubleValue() {
306        return (double) this.numerator / this.denominator;
307    }
308
309    @Override
310    public float floatValue() {
311        return (float) this.doubleValue();
312    }
313
314    @Override
315    public int compareTo(Rational r) {
316        if (this.denominator == r.denominator) {
317            return Integer.compare(this.numerator, r.numerator);
318        }
319
320        return Long.compare((long) this.numerator * r.denominator,
321                            (long) r.numerator * this.denominator);
322    }
323
324    @Override
325    public void formatTo(Formatter f, int flags, int width, int precision) {
326
327        if (precision != -1) {
328            throw new IllegalFormatPrecisionException(precision);
329        }
330
331        if ((flags & ALTERNATE) == ALTERNATE) {
332            throw new FormatFlagsConversionMismatchException("#", 's');
333        }
334
335        String s = this.toString();
336
337        if (width <= s.length()) {
338            f.format(s);
339            return;
340        }
341
342        if ((flags & LEFT_JUSTIFY) == LEFT_JUSTIFY) {
343            f.format("%-" + width + "s", s);
344            return;
345        }
346
347        f.format("%" + width + "s", s);
348    }
349}
350
351/**
352 * Test driver.
353 */
354final class RationalTest {
355    private RationalTest() { }
356
357    /**
358     * Evaluates an arithmetic postfix expression with rational numbers.
359     * Example: java RationalTest 1/2 3/4 add 5/6 multiply
360     * @param args the arithmetic postfix expression with rational numbers
361     */
362    public static void main(String[] args) {
363        Rational[] stack = new Rational[args.length];
364        int top = 0;
365
366        Locale.setDefault(Locale.ENGLISH);
367        Scanner input = new Scanner(System.in);
368
369        for (String arg : args) {
370            try {
371                switch (arg) {
372                case "stackdump":
373                    for (int j = 0; j < top; ++j) {
374                        System.out.printf("%d: %s (%.15e)%n",
375                                          j, stack[j], stack[j].doubleValue());
376                    }
377                    continue;
378                case "negate":
379                    stack[top - 1] = stack[top - 1].negate();
380                    break;
381                case "invert":
382                    stack[top - 1] = stack[top - 1].invert();
383                    break;
384                case "add":
385                    stack[top - 2] = stack[top - 2].add(stack[top - 1]);
386                    --top;
387                    break;
388                case "subtract":
389                    stack[top - 2] = stack[top - 2].subtract(stack[top - 1]);
390                    --top;
391                    break;
392                case "multiply":
393                    stack[top - 2] = stack[top - 2].multiply(stack[top - 1]);
394                    --top;
395                    break;
396                case "divide":
397                    stack[top - 2] = stack[top - 2].divide(stack[top - 1]);
398                    --top;
399                    break;
400
401                case "getNumerator":
402                    System.out.printf("%s: %d%n",
403                                      arg, stack[top - 1].getNumerator());
404                    continue;
405                case "getDenominator":
406                    System.out.printf("%s: %d%n",
407                                      arg, stack[top - 1].getDenominator());
408                    continue;
409                case "signum":
410                    System.out.printf("%s: %d%n",
411                                      arg, stack[top - 1].signum());
412                    continue;
413
414                case "toString":
415                    System.out.printf("%s: %s%n",
416                                      arg, stack[top - 1].toString());
417                    continue;
418                case "hashCode":
419                    System.out.printf("%s: %d%n",
420                                      arg, stack[top - 1].hashCode());
421                    continue;
422                case "equals":
423                    System.out.printf("%s: %b%n",
424                                      arg,
425                                      stack[top - 2].equals(stack[top - 1]));
426                    continue;
427
428                case "intValue":
429                    System.out.printf("%s: %d%n",
430                                      arg, stack[top - 1].intValue());
431                    continue;
432                case "longValue":
433                    System.out.printf("%s: %d%n",
434                                      arg, stack[top - 1].longValue());
435                    continue;
436                case "doubleValue":
437                    System.out.printf("%s: %.15e%n",
438                                      arg, stack[top - 1].doubleValue());
439                    continue;
440                case "floatValue":
441                    System.out.printf("%s: %.7e%n",
442                                      arg, stack[top - 1].floatValue());
443                    continue;
444
445                case "compareTo":
446                    System.out.printf("%s: %d%n",
447                                      arg,
448                                      stack[top - 2].compareTo(stack[top - 1]));
449                    continue;
450
451                case "formatTo":
452                    System.err.print("Enter format string: ");
453                    try {
454                        System.out.printf("%s: " + input.next() + "%n",
455                                          arg, stack[top - 1]);
456                    } catch (NoSuchElementException x) {
457                        System.err.println("no input");
458                        System.exit(1);
459                    }
460                    continue;
461
462                case "ZERO":
463                    stack[top++] = Rational.ZERO;
464                    continue;
465                case "ONE":
466                    stack[top++] = Rational.ONE;
467                    continue;
468                case "MAX_VALUE":
469                    stack[top++] = Rational.MAX_VALUE;
470                    continue;
471                case "MIN_VALUE":
472                    stack[top++] = Rational.MIN_VALUE;
473                    continue;
474
475                case "int":
476                    System.err.print("Enter integer: ");
477                    try {
478                        stack[top] = Rational.valueOf(input.nextInt());
479                        ++top;
480                    } catch (NoSuchElementException x) {
481                        System.err.println(input.next() + ": invalid input");
482                        System.exit(1);
483                    }
484                    continue;
485                case "intint":
486                    System.err.print("Enter numerator and denominator: ");
487                    try {
488                        stack[top] = Rational.valueOf(input.nextInt(),
489                                                      input.nextInt());
490                        ++top;
491                    } catch (NoSuchElementException x) {
492                        System.err.println(input.next() + ": invalid input");
493                        System.exit(1);
494                    }
495                    continue;
496                default:
497                    stack[top] = Rational.valueOf(arg);
498                    ++top;
499                    continue;
500                }
501
502                System.out.println(arg);
503
504            } catch (ArrayIndexOutOfBoundsException x) {
505                System.err.println(arg + ": missing operand");
506                System.exit(1);
507            } catch (NumberFormatException x) {
508                System.err.println(x.getMessage() + ": not a rational number");
509            } catch (ArithmeticException x) {
510                System.err.println(x.getMessage());
511            } catch (IllegalFormatException x) {
512                System.err.printf("%s: %s%n", arg, x);
513            } catch (IllegalArgumentException x) {
514                System.err.println(x.getMessage());
515            }
516        }
517
518        for (int j = 0; j < top; ++j) {
519            System.out.printf("%d: %s (%.15e)%n",
520                              j, stack[j], stack[j].doubleValue());
521        }
522    }
523}
524