1. Mathematics

Integers and floating point numbers already appeared in the first exercises; we were using the common mathematical operators to crunch numbers. But Java does not have an operator for everything, and so the class Math offers further methods and constants; early on we used Math.random(), for example. In the following tasks of this chapter, we will focus on various roundings in particular, and we will look at methods of the Math class. In addition, the java.math package provides two classes that can be used to represent arbitrarily large numbers — there are tasks for these as well.

Prerequisites

  • know rounding methods of the class `Math

  • know BigInteger and `BigDecimal

Data types used in this chapter:

1.1. The class Math

The class Math contains a large number of mathematical functions, but it is important to also look at wrapper classes, for example, or at the Scanner or Formatter when it comes to converting a number to a string or parsing a string so that there is a primitive type at the end again.

1.1.1. Quiz: Rule of thumb ⭐

There are different approaches and different kinds of problems for rounding numerical values:

  • On the one hand, a floating point number can be converted to an integer,

  • on the other hand, the number of decimal places can be reduced for a floating point number.

The Java library supports different possibilities for conversion and rounding, among other things with

  • the explicit type conversion (int) doubleint

  • Math.floor(double)double

  • Math.ceil(double)double

  • Math.round(double)long

  • Math.rint(double)double

  • BigDecimal and setScale(..)BigDecimal

  • NumberFormat, configured with setMaximumFractionDigits(…​), then format(…​)String

  • DecimalFormat, configured with setRoundingMode(…​), then format(…​)String

  • Formatter with a format string, then format(…​)String.

Four of the methods originate from Math, but when it comes to very large and precise floating point numbers or string representation, other classes are involved. Since the methods differ slightly in the result, the following task should make these differences clear once again.

Task:

  • Fill in the following table with the results of the roundings:

    double value d(int) d(int) floor(d)(int) ceil(d)round(d)(int) rint(d)

    -2.5

    -1.9

    -1.6

    -1.5

    -1.1

    1.1

    1.5

    1.6

    1.9

    2.5

1.1.2. Check if Tin Tin cheated on rounding. ⭐

The accountant Tin Tin does the accounting for Captain CiaoCiao of income and expenses. She gets positive and negative floating point values and writes down the total as a rounded integer for a summary at the end. Captain CiaoCiao suspects that Tin Tin is not being completely honest and is pocketing the penny amounts, when she should use commercially rounding. As a reminder, if the number at the first omitted decimal place is a 0, 1, 2, 3, or 4, round down; if it is 5, 6, 7, 8, or 9, round up.

To test his guess, Captain CiaoCiao needs a program that can sum and check different rounding modes.

Task:

  • Given an array of floating point numbers (positive and negative) and the sum converted to an integer by Tin Tin.

  • Captain CiaoCiao wants to find out which rounding was used to form the integer of the sum. Therefore, the elements in the array are to be summed and compared to Tin Tin’s sum. The rounding is done after the numbers are added.

  • Implement a method RoundingMode detectRoundingMode(double[] numbers, int sum) that gets a double array of numbers and the sum of Tin Tin and checks which rounding mode was used.

    • To allow the rounding mode to be represented, introduce an enumeration type:

      enum RoundingMode {
        CAST, ROUND, FLOOR, CEIL, RINT, UNKNOWN;
      }
    • The enumeration elements represent different rounding modes:

    • (int), that is, a type conversion.

    • (int) Math.floor(double)

    • (int) Math.ceil(double)

    • (int) Math.rint(double)

    • (int) Math.round(double)

  • Which rounding is bad for Captain CiaoCiao and good for Tin Tin? Which variation could Tin Tin use to cheat?

Example:

  • The call might look like this:

    double[] numbers = { 199.99 };
    System.out.println( detectRoundingMode( numbers, 200 ) );

Notes:

  • There is an enumeration type RoundingMode in the java.math package, but for our case it does not fit the task.

  • It may well happen that more than one rounding mode fits — such as when the sum of the floating point values itself gives an integer — then the method is free to choose one of the rounding modes.

1.2. Huge and very precise numbers

The classes java.math.BigInteger and java.math.BigDecimal can be used to represent arbitrarily large integer and floating point numbers.

1.2.1. Calculate arithmetic mean of a large integer ⭐

To calculate the arithmetic mean of two numbers, they are added together and divided by two. This works well if the sum of the two values does not exceed the largest number that can be represented. If there is an overflow, the result is wrong. There are some algorithms in computer science that can handle this problem as well, but we can make it a little easier and take the higher data type in each case. For example, if we want to calculate the mean value of two int variables, we convert the two int to a long, add the numbers, divide and convert back to the int. With the long data type, there is no larger primitive data type, but the BigInteger type can be used well for the case.

Task:

  • Calculate the arithmetic mean of two long values so that there is no overflow and wrong results. The result should be a long again.

1.2.2. Number by number over the phone ⭐

A new deal has made Bonny Brain a lot of money. The number is so big that you can’t just announce it over the phone at all; it has to be conveyed in chunks.

Task:

  • Write a new method BigInteger completeNumber(int... parts) that gets a variable number of numbers and returns the big total number at the end.

Example:

  • completeNumber(123, 22, 989, 77, 9) returns a BigInteger with the value 12322989779.

1.2.3. Develop class for fractions and truncate fractions ⭐⭐

Bonny Brain is trying a new recipe for a rum punch. Fractions like "1/4 liter grape juice" or "1/2 liter rum" keep appearing in the directions for making it. For the party, she prepares 100 servings, and fractions like "100/4 liters" occur. The fractions can be shortened so that Bonny Brain knows, for example, that 25 liters of grape juice must be purchased.

Task:

  1. Create a new class Fraction.

  2. Let there be a constructor Fraction(int numerator, int denominator) that stores numerator and denominator in public final variables.

    • Consider whether there can be faulty arguments, which we should report through exceptions.

    • Every created fraction should be simplified automatically. Use the gcd(…​) method of BigInteger, which calculates the greatest common divisor (ggT)_. Remember: The ggT of numerator and denominator is the largest number by which both are divisible. You can simplify the fraction by dividing both numerator and denominator by this number.

    • Both numerator and denominator can be negative, but then you can flip the sign so that both values become positive.

    • The objects are all supposed to be immutable, and therefore the variables can be public, since they are not supposed to be changed after initialization via the constructor. In other words, the class Fraction does not contain any setters.

  3. Add the constructor Fraction(int value), where the denominator automatically becomes 1.

  4. Implement a method to multiply fractions and detect overflows.

  5. Implement a method reciprocal() that returns the inverse of a fraction, that is, swaps the numerator and denominator. With the help of this method, the division of fractions can be implemented.

  6. Fraction shall extend java.lang.Number and implement all mandatory methods.

  7. Fraction shall implement Comparable, because fractions can be converted to a decimal number, and decimal numbers have a natural order.

  8. Fraction shall implement equals(…​) and hashCode() correctly.

  9. Implement a toString() method that returns a string as bare as possible.

Fraction UML
Figure 1. UML diagram of Fraction.

1.3. Suggested solutions

1.3.1. Quiz: Rule of thumb

Table 1. Different roundings of a floating point number d in comparison
value d(int) d(int) floor(d)(int) ceiling(d)round(d)(int) rint(d)

-2.5

-2

-3

-2

-2

-2

-1.9

-1

-2

-1

-2

-2

-1.6

-1

-2

-1

-2

-2

-1.5

-1

-2

-1

-1

-2

-1.1

-1

-2

-1

-1

-1

1.1

1

1

2

1

1

1.5

1

1

2

2

2

1.6

1

1

2

2

2

1.9

1

1

2

2

2

2.5

2

2

3

3

2

Math.round(double) and Math.rint(double) differ for .5 numbers "on the edge":

  • round(double) rounds commercially. As can be seen in the table, round(-2.5), round(-1.5), round(1.5), and round(2.5) round up; the numbers always become larger. If one always rounds up only commercially, this produces small errors, because the procedure is not symmetrical. To be fair, some .5 would also have to be rounded down; this is where another method comes into play.

  • rint(double) behaves like round(double) for numbers not ending in .5, but when ending in .5 the following rule applies: the last digit to be retained becomes even. This behavior is called symmetric (also mathematical, scientific) rounding, because it rounds up or down in half the cases, respectively.

In summary:

Table 2. rounding method
procedureresult

type conversion (int) d

Truncates decimal places of a floating point number.

floor(d)

Rounds to the next smaller number against -∞.

ceil(d)

Round to the next larger number against +∞.

round(d)

Rounds the value according to commercial rules.

rint(d)

Rounds double values symmetrically/mathematically/scientifically.

1.3.2. Check if Tin Tin cheated on rounding.

Now we come to the question of which rounding is bad for Captain CiaoCiao and good for Tin Tin. The difference between the rounded value and the sum is the difference Tin Tin can pocket. Example: If the total is 222.22 and Tin Tin rounds to 222, there is a difference of 0.22 that Captain CiaoCiao misses and goes into Tin Tin’s pocket. What do the differences look like for the example numbers?

Table 3. Difference from rounding, sum in foot
value dd - (int) dd - floor(d)d - ceil(d)d - round(d)d - rint(d)

-2.5

-0.5

0.5

-0.5

-0.5

-0.5

-1.9

-0.9

0.1

-0.9

0.1

0.1

-1.6

-0.6

0.4

-0.6

0.4

0.4

-1.5

-0.5

0.5

-0.5

-0.5

0.5

-1.1

-0.1

0.9

-0.1

-0.1

-0.1

1.1

0.1

0.1

-0.9

0.1

0.1

1.5

0.5

0.5

-0.5

-0.5

-0.5

1.6

0.6

0.6

-0.4

-0.4

-0.4

1.9

0.9

0.9

-0.1

-0.1

-0.1

2.5

0.5

0.5

-0.5

-0.5

0.5

Total

0

5

-5

-2

0

We can read that for the numbers Tin Tin chose, she gets the most out of rounding off all the sums, because that makes the numbers smaller, and she gets the difference in each case, whether the numbers are negative or positive.

To the proposed solution:

com/tutego/exercise/math/RoundingModeDetector.java
public enum RoundingMode {
  CAST, ROUND, FLOOR, CEIL, RINT, UNKNOWN
}

private static RoundingMode detectRoundingMode( double value, int rounded ) {
  return rounded == (int) value         ? RoundingMode.CAST :
         rounded == Math.round( value ) ? RoundingMode.ROUND :
         rounded == Math.floor( value ) ? RoundingMode.FLOOR :
         rounded == Math.ceil( value )  ? RoundingMode.CEIL :
         rounded == Math.rint( value )  ? RoundingMode.RINT :
         RoundingMode.UNKNOWN;
}

public static RoundingMode detectRoundingMode( double[] numbers, int sum ) {
  double realSum = 0;
  for ( double number : numbers )
    realSum += number;
  return detectRoundingMode( realSum, sum );
}

The proposed solution is composed of two methods. The public method detectRoundingMode(…​) sums the elements of the array and calls the private method detectRoundingMode(double, int) to determine the actual rounding mode. The method sequentially compares the integer value with the different rounded variants, and if there is a match, the enumeration element is returned. If the totals and the rounded value do not match at all, the return is RoundingMode.UNKNOWN.

1.3.3. Calculate arithmetic mean of a large integer

com/tutego/exercise/math/BigIntegerMean.java
private final static BigDecimal TWO = BigDecimal.valueOf( 2 );

public static long meanExact( long x, long y ) {
  BigInteger bigSum  = BigInteger.valueOf( x ).add( BigInteger.valueOf( y ) );
  BigInteger bigMean = bigSum.divide( BigInteger.TWO );
  return bigMean.longValue();
}

In the first step we build a new object of type BigInteger for the parameters x and y using the factory method valueOf(long). In the second step the sum is built. In the third step the sum is divided by 2. For the division we also have to use a BigInteger object for the divisor. Since the divisor is constant, the solution defines its own constant — the class BigInteger has constants for 0, 1 and 10, but not for 2.

When averaging integers, there is always the question of rounding. The addition of two even or two odd numbers always results in an even number, but the sum of an even number with an odd number is odd. If odd numbers are divided by two, there is a remainder, and the question is whether to round up or down, round the result to 0, or round to plus or minus infinity — there are quite a few possibilities. The BigInteger method divide(…​) behaves like the division of two integers, it is rounded towards 0.

The end result is a number, not greater than Long.MAX_VALUE and not smaller than Long.MIN_VALUE, so converting the number from the BigInteger with longValue() gives no loss.

1.3.4. Number by number over the phone

com/tutego/exercise/math/SequentialNumbersToOneNumber.java
static BigInteger completeNumber( int... parts ) {
  StringBuilder bigNumber = new StringBuilder( parts.length * 2 );
  for ( int part : parts )
    bigNumber.append( part );

  return new BigInteger( bigNumber.toString() );
}

We can solve the task using strings or math. Using a temporary string the task is easy to solve.

In the first step we run the array, take all the numbers one by one and append them to a StringBuilder. If we assume that each number consists on average of two digits, we can estimate approximately how big the result will be; therefore we use the parameterized constructor of StringBuilder with a capacity.

After appending all the digits, StringBuilder is converted to a String and this feeds the constructor of BigInteger. Here the actual conversion of the string into a BigInteger begins.

Of course, the task can also be solved without a temporary StringBuilder. Let us take the call as an example

completeNumber(123, 22, 989, 77, 9);

If at the end we want to create the BigInteger with 12322989779, we could first build the BigInteger with 123, then multiply the number with 100 and add 22, resulting in 12322. In the next step, we take the result, multiply it by 1000 and add 989. Then we multiply again by 100, add 77, multiply the result by 10 and add 9. That is, we always multiply the old value by 10n, where n is the number of digits in the coming number.

1.3.5. Develop class for fractions and truncate fractions

com/tutego/exercise/math/Fraction.java
import java.math.BigInteger;

public final class Fraction extends Number implements Comparable<Fraction> {

  public final int numerator;
  public final int denominator;

  public Fraction( int numerator, int denominator ) {
    if ( denominator == 0 )
      throw new ArithmeticException( "denominator of a fraction can't be 0" );

    // denominator always positive
    if ( denominator < 0 ) {
      numerator   = -numerator;
      denominator = -denominator;
    }

    // shortcut if denominator == 1
    if ( denominator == 1 ) {
      this.numerator   = numerator;
      this.denominator = 1;
    }
    else {
      // try to simplify every fraction
      int gcd = gcd( numerator, denominator );
      // might be 1, but divide anyway
      this.numerator   = numerator / gcd;
      this.denominator = denominator / gcd;
    }
  }

  private static int gcd( int a, int b ) {
    return BigInteger.valueOf( a )
                     .gcd( BigInteger.valueOf( b ) )
                     .intValue();
  }

  public Fraction( int value ) {
    this( value, 1 );
  }

  public Fraction reciprocal() {
    return new Fraction( denominator, numerator );
  }

  public Fraction multiply( Fraction other ) {
    return new Fraction( Math.multiplyExact( numerator, other.numerator ),
                         Math.multiplyExact( denominator, other.denominator ) );
  }

  @Override
  public int intValue() {
    return numerator / denominator;
  }

  @Override
  public long longValue() {
    return (long) numerator / denominator;
  }

  @Override
  public double doubleValue() {
    return (double) numerator / denominator;
  }

  @Override
  public float floatValue() {
    return (float) numerator / denominator;
  }

  @Override
  public int compareTo( Fraction other ) {
    return Double.compare( doubleValue(), other.doubleValue() );
  }

  @Override
  public boolean equals( Object other ) {
    if ( other == this )
      return true;
    if ( ! (other instanceof Fraction otherFraction) )
      return false;

    return    numerator   == otherFraction.numerator
           && denominator == otherFraction.denominator;
  }

  @Override
  public int hashCode() {
    return numerator + Integer.reverse( denominator );
  }

  @Override
  public String toString() {
    return numerator   == 0 ? "0" :
           denominator == 1 ? "" + numerator :
           numerator + " / " + denominator;
  }
}

Let’s start with the constructor Fraction, which takes the numerator and denominator. The denominator must never be 0, this is punished with an exception. We could in principle build our own Exception class, but an ArithmeticException fits well.

Numerator and denominator can be negative or positive, there are four cases:

  • numerator > 0, denominator > 0.

  • numerator < 0, denominator > 0

  • numerator > 0, denominator < 0

  • numerator < 0, denominator < 0

Let us consider two cases:

  • If the numerator and denominator are both negative, we can reverse the sign so that both values become positive.

  • If the denominator is negative and the numerator is positive, we can shift the sign to the numerator.

Both cases can be handled with a control flow statement that if the denominator is negative, we flip both signs. A negative denominator then becomes positive. If the numerator was also negative, the numerator becomes positive, so a negative numerator and negative denominator thus both become positive. If the denominator was negative and the numerator was positive, we thus shift the sign to the numerator, because the numerator becomes negative.

Numerator and denominator are now prepared, and if the denominator is 1, then the fraction does not need to be truncated, and we save the following work and leave the constructor, otherwise we continue in the else block.

The last part of the constructor deals with the reduction of the fraction. The task briefly explained how to proceed here: we need the greatest common divisor. The constructor delegates here to its own method gcd(int, int), which first builds a BigInteger for two numbers, then lets it calculate the greatest common divisor and then returns it as an integer. In our scenario, the parameter types are int so the divisor will be smaller in any case, with intValue() we read from the BigInteger. After the constructor asks for the greatest common divisor, we divide the numerator and denominator by it, initializing our object variables. In principle, gcd could also be 1, so we don`t need a division, but we save this special case distinction here, because a division by 1 is not expensive.

The second constructor takes only one numerator, and in that case the denominator is 1. We delegate to our own constructor, but in general we can do the storing ourselves, since we don`t need the special treatment for a negative denominator, and the denominator is not 0 either.

The first of the two methods reciprocal() creates a new Fraction object by turning the denominator into the numerator and the numerator into the denominator. The method multiply(…​) multiplies its own numerator with the numerator of the passed object and symmetrically does the same for the denominator. Multiplication quickly produces large numbers and the value range of int could be blown up, so Math.multiplyExact(…​) makes sure that the product fits into an int; otherwise an exception follows.

The next category of methods comes from the base type Number. We divide numerator by denominator and return the result at the different methods.

The next method compareTo(…​) comes from the Comparable interface. We make it simple and calculate the quotient and compare our quotient with the passed fraction.

In the next step we override the method equals(…​) and hashCode(). A good hash code will return a different hash code if the numerator or denominator changes. The realization of hashCode() does this by flipping the bits of the denominator, so if the number of required bits of the numerator and denominator together does not exceed 32 (the number of bits of an int), we can detect each change by a different hashcode.

The last method is toString(). If the numerator is 0, we don`t even have to look at the denominator and return the string 0. If the denominator is 1, we can return only the numerator. Otherwise, we return the numerator and denominator separated by a fraction bar.

Java 8 Backport

Java 16 introduces pattern variables, which the proposed solution uses at:

if ( ! (other instanceof Fraction otherFraction) )

Users of earlier Java versions must write instead:

if ( ! (other instanceof Fraction) )
  return false;

Fraction otherFraction = (Fraction) other;
return numerator == otherFraction.numerator
       && denominator == otherFraction.denominator;