Requirement – complex operations

Complex operations are difficult to address, because mixing operations makes it really difficult for the non-trained human eye to understand in which order the operations should take place. Also, different evaluation orders usually lead to different results. To solve that, the computation of reverse polish expressions is backed up by the implementation of a queue. These are some tests for our next functionality:

@Test
public void multipleAddOperationsReturnCorrectValue() {
assertThat(reversePolishNotation.compute("1 2 5 + +"))
.isEqualTo(8);
}

@Test
public void multipleDifferentOperationsReturnCorrectValue() {
assertThat(reversePolishNotation.compute("5 12 + 3 -"))
.isEqualTo(14);
}

@Test
public void aComplexTest() {
assertThat(reversePolishNotation.compute("5 1 2 + 4 * + 3 -"))
.isEqualTo(14);
}

The computation should pile up the numbers or operands in the expression sequentially, from left to right, in a queue or stack in Java. If at any point an operator is found then the pile replaces the two elements on top with the result of applying that operator to those values. For better comprehension, the logic is going to be separated in to different functions.

First of all, we are going to define a function that takes a stack and an operation and applies the function to the first two items on the top. Note that the second operand is retrieved in the first instance due to the implementation of stack:

private static void applyOperation(
Stack<Integer> stack,
BinaryOperator<Integer> operation
) {
int b = stack.pop(), a = stack.pop();
stack.push(operation.apply(a, b));
}

The next step is to create all the functions that our program must handle. For each operator a function is defined as an object. This has some advantages, such as better isolation for testing. In this case it might not make sense to test functions separately because they are trivial, but there are some other scenarios where testing the logic of these functions in isolation can be very useful:

static BinaryOperator<Integer> ADD = (a, b) -> a + b;
static BinaryOperator<Integer> SUBTRACT = (a, b) -> a - b;
static BinaryOperator<Integer> MULTIPLY = (a, b) -> a * b;
static BinaryOperator<Integer> DIVIDE = (a, b) -> a / b;

And now, putting all the pieces together. Depending on the operator we find, the proper operation is applied:

int compute(String expression) {
Stack<Integer> stack = new Stack<>();
for (String elem : expression.trim().split(" ")) {
if ("+".equals(elem))
applyOperation(stack, ADD);
else if ("-".equals(elem))
applyOperation(stack, SUBTRACT);
else if ("*".equals(elem))
applyOperation(stack, MULTIPLY);
else if ("/".equals(elem))
applyOperation(stack, DIVIDE);
else {
stack.push(parseInt(elem));
}
}
if (stack.size() == 1) return stack.pop();
else throw new NotReversePolishNotationError();
}

The code is readable and very easy to understand. Moreover, this design allows the extending of the functionality by adding support to other different operations with ease.

It can be a good exercise for readers to add a modulus (%) operation to the provided solution.

Another good example where lambdas fit perfectly is Streams API, since most functions have a self-explanatory name like filter, map or reduce, among others. Let's explore this more deeply in the next section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.133.160