Avatar of ksnortum

ksnortum's solution

to Wordy in the Java Track

Published at Oct 19 2019 · 1 comment
Instructions
Test suite
Solution

Parse and evaluate simple math word problems returning the answer as an integer.

Iteration 0 — Numbers

Problems with no operations simply evaluate to the number given.

What is 5?

Evaluates to 5.

Iteration 1 — Addition

Add two numbers together.

What is 5 plus 13?

Evaluates to 18.

Handle large numbers and negative numbers.

Iteration 2 — Subtraction, Multiplication and Division

Now, perform the other three operations.

What is 7 minus 5?

2

What is 6 multiplied by 4?

24

What is 25 divided by 5?

5

Iteration 3 — Multiple Operations

Handle a set of operations, in sequence.

Since these are verbal word problems, evaluate the expression from left-to-right, ignoring the typical order of operations.

What is 5 plus 13 plus 6?

24

What is 3 plus 2 multiplied by 3?

15 (i.e. not 9)

Iteration 4 — Errors

The parser should reject:

  • Unsupported operations ("What is 52 cubed?")
  • Non-math questions ("Who is the President of the United States")
  • Word problems with invalid syntax ("What is 1 plus plus 2?")

Bonus — Exponentials

If you'd like, handle exponentials.

What is 2 raised to the 5th power?

32

Setup

Go through the setup instructions for Java to install the necessary dependencies:

https://exercism.io/tracks/java/installation

Running the tests

You can run all the tests for an exercise by entering the following in your terminal:

$ gradle test

Use gradlew.bat if you're on Windows

In the test suites all tests but the first have been skipped.

Once you get a test passing, you can enable the next one by removing the @Ignore("Remove to run test") annotation.

Source

Inspired by one of the generated questions in the Extreme Startup game. https://github.com/rchatley/extreme_startup

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

WordProblemSolverTest.java

import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Before;
import org.junit.rules.ExpectedException;

import static org.junit.Assert.assertEquals;

public class WordProblemSolverTest {

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    WordProblemSolver solver;

    @Before
    public void setup() {
        solver = new WordProblemSolver();
    }

    @Test
    public void testJustANumber() {
        assertEquals(5, solver.solve("What is 5?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testSingleAddition1() {
        assertEquals(2, solver.solve("What is 1 plus 1?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testSingleAddition2() {
        assertEquals(55, solver.solve("What is 53 plus 2?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testSingleAdditionWithNegativeNumbers() {
        assertEquals(-11, solver.solve("What is -1 plus -10?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testSingleAdditionOfLargeNumbers() {
        assertEquals(45801, solver.solve("What is 123 plus 45678?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testSingleSubtraction() {
        assertEquals(16, solver.solve("What is 4 minus -12?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testSingleMultiplication() {
        assertEquals(-75, solver.solve("What is -3 multiplied by 25?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testSingleDivision() {
        assertEquals(-11, solver.solve("What is 33 divided by -3?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testMultipleAdditions() {
        assertEquals(3, solver.solve("What is 1 plus 1 plus 1?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAdditionThenSubtraction() {
        assertEquals(8, solver.solve("What is 1 plus 5 minus -2?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testMultipleSubtractions() {
        assertEquals(3, solver.solve("What is 20 minus 4 minus 13?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testSubtractionThenAddition() {
        assertEquals(14, solver.solve("What is 17 minus 6 plus 3?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testMultipleMultiplications() {
        assertEquals(-12, solver.solve("What is 2 multiplied by -2 multiplied by 3?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testAdditionThenMultiplication() {
        assertEquals(-8, solver.solve("What is -3 plus 7 multiplied by -2?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testMultipleDivisions() {
        assertEquals(2, solver.solve("What is -12 divided by 2 divided by -3?"));
    }

    @Ignore("Remove to run test")
    @Test
    public void testUnknownOperation() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is 52 cubed?");
    }

    @Ignore("Remove to run test")
    @Test
    public void testNonMathQuestion() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        // See https://en.wikipedia.org/wiki/President_of_the_United_States if you really need to know!
        solver.solve("Who is the President of the United States?");
    }

    @Ignore("Remove to run test")
    @Test
    public void testMissingAnOperand() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is 1 plus?");
    }

    @Ignore("Remove to run test")
    @Test
    public void testNoOperandsOrOperators() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is?");
    }

    @Ignore("Remove to run test")
    @Test
    public void testTwoOperationsInARow() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is 1 plus plus 2?");
    }

    @Ignore("Remove to run test")
    @Test
    public void testTwoNumbersAfterOperation() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is 1 plus 2 1?");
    }

    @Ignore("Remove to run test")
    @Test
    public void testPostfixNotation() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is 1 2 plus?");
    }

    @Ignore("Remove to run test")
    @Test
    public void testPrefixNotation() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is plus 1 2?");
    }
}

src/main/java/WordProblemSolver.java

class WordProblemSolver {

	enum State { BEGIN, SET, NUMBER, OPERATION, BY, TO, THE, POWER, TERMINATION }
	enum Operation { NONE, PLUS, MINUS, MULTIPLY, DIVIDE, EXPONENT }
	
	private static final String INVALID_MESSAGE = "I'm sorry, I don't understand the question!";
	private static final String ONE_OR_MORE_WHITE_SPACES = "\\s+";
	private static final String ENDS_WITH_ST = "st$";
	private static final String ENDS_WITH_ND = "nd$";
	private static final String ENDS_WITH_RD = "rd$";
	private static final String ENDS_WITH_TH = "th$";
	
	int solve(String question) {
		Integer result = null;
		State state = State.BEGIN;
		Operation operation = Operation.NONE;
		
		for (String token : question.split(ONE_OR_MORE_WHITE_SPACES)) {
			switch (state) {
			case BEGIN:
				if (! "What".equalsIgnoreCase(token)) {
					throw new IllegalArgumentException(INVALID_MESSAGE);
				}
				
				state = State.SET;
				break;
				
			case SET:
				if (! "is".equalsIgnoreCase(token)) {
					throw new IllegalArgumentException(INVALID_MESSAGE);
				}
				
				state = State.NUMBER;
				break;
				
			case NUMBER:
				boolean terminate = false;
				
				if (token.endsWith("?")) {
					terminate = true;
					token = token.substring(0, token.length() - 1);
				}
				
				String originalToken = token;
				token = token.replaceFirst(ENDS_WITH_ST, "")
							 .replaceFirst(ENDS_WITH_ND, "")
							 .replaceFirst(ENDS_WITH_RD, "")
							 .replaceFirst(ENDS_WITH_TH, "");
				boolean ordinal = ! token.equals(originalToken);

				int number;
				try {
					number = Integer.parseInt(token);
				} catch (NumberFormatException e) {
					throw new IllegalArgumentException(INVALID_MESSAGE);
				}
				
				result = evaluate(number, operation, result);
				
				if (terminate) {
					state = State.TERMINATION;
				} else if (ordinal) {
					state = State.POWER;
				} else {
					state = State.OPERATION;
				}
				
				break;
				
			case OPERATION:
				token = token.toLowerCase();
				
				switch (token) {
				case "plus": 
					operation = Operation.PLUS; 
					state = State.NUMBER;
					break;
					
				case "minus": 
					operation = Operation.MINUS; 
					state = State.NUMBER;
					break;
					
				case "multiplied":
					operation = Operation.MULTIPLY;
					state = State.BY;
					break;
					
				case "divided":
					operation = Operation.DIVIDE;
					state = State.BY;
					break;
					
				case "raised":
					operation = Operation.EXPONENT;
					state = State.TO;
					break;
					
				default: 
					throw new IllegalArgumentException(INVALID_MESSAGE);
				}

				break;
				
			case BY:
				if (! "by".equalsIgnoreCase(token)) {
					throw new IllegalArgumentException(INVALID_MESSAGE);
				}
				
				state = State.NUMBER;
				break;
				
			case TO:
				if (! "to".equalsIgnoreCase(token)) {
					throw new IllegalArgumentException(INVALID_MESSAGE);
				}
				
				state = State.THE;
				break;
				
			case THE:
				if (! "the".equalsIgnoreCase(token)) {
					throw new IllegalArgumentException(INVALID_MESSAGE);
				}
				
				state = State.NUMBER;
				break;
				
			case POWER:
				if (! "power?".equalsIgnoreCase(token)) {
					throw new IllegalArgumentException(INVALID_MESSAGE);
				}
				
				state = State.TERMINATION;
				break;
				
			case TERMINATION:
				throw new IllegalArgumentException(INVALID_MESSAGE);
			}
		}
	
		if (state != State.TERMINATION || result == null) {
			throw new IllegalArgumentException(INVALID_MESSAGE);
		}
			
		return result;
	}

	private Integer evaluate(int number, Operation operation, Integer result) {
		if (result == null && operation != Operation.NONE) {
			throw new IllegalArgumentException(INVALID_MESSAGE);
		}
		
		switch (operation) {
		case NONE:     result = number; break;
		case PLUS:     result = result + number; break;
		case MINUS:    result = result - number; break;
		case MULTIPLY: result = result * number; break;
		case DIVIDE: 
			if (number == 0) {
				throw new IllegalArgumentException("Divide by zero error");
			}
			result = result / number;
			break;
		case EXPONENT: result = (int) Math.pow(result, number);
		}
		
		return result;
	}
	
}

src/test/java/WordProblemSolverTest.java

import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Before;
import org.junit.rules.ExpectedException;

import static org.junit.Assert.assertEquals;

public class WordProblemSolverTest {

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    WordProblemSolver solver;

    @Before
    public void setup() {
        solver = new WordProblemSolver();
    }

    @Test
    public void testJustANumber() {
        assertEquals(5, solver.solve("What is 5?"));
    }

    @Test
    public void testSingleAddition1() {
        assertEquals(2, solver.solve("What is 1 plus 1?"));
    }

    @Test
    public void testSingleAddition2() {
        assertEquals(55, solver.solve("What is 53 plus 2?"));
    }

    @Test
    public void testSingleAdditionWithNegativeNumbers() {
        assertEquals(-11, solver.solve("What is -1 plus -10?"));
    }

    @Test
    public void testSingleAdditionOfLargeNumbers() {
        assertEquals(45801, solver.solve("What is 123 plus 45678?"));
    }

    @Test
    public void testSingleSubtraction() {
        assertEquals(16, solver.solve("What is 4 minus -12?"));
    }

    @Test
    public void testSingleMultiplication() {
        assertEquals(-75, solver.solve("What is -3 multiplied by 25?"));
    }

    @Test
    public void testSingleDivision() {
        assertEquals(-11, solver.solve("What is 33 divided by -3?"));
    }

    @Test
    public void testMultipleAdditions() {
        assertEquals(3, solver.solve("What is 1 plus 1 plus 1?"));
    }

    @Test
    public void testAdditionThenSubtraction() {
        assertEquals(8, solver.solve("What is 1 plus 5 minus -2?"));
    }

    @Test
    public void testMultipleSubtractions() {
        assertEquals(3, solver.solve("What is 20 minus 4 minus 13?"));
    }

    @Test
    public void testSubtractionThenAddition() {
        assertEquals(14, solver.solve("What is 17 minus 6 plus 3?"));
    }

    @Test
    public void testMultipleMultiplications() {
        assertEquals(-12, solver.solve("What is 2 multiplied by -2 multiplied by 3?"));
    }

    @Test
    public void testAdditionThenMultiplication() {
        assertEquals(-8, solver.solve("What is -3 plus 7 multiplied by -2?"));
    }

    @Test
    public void testMultipleDivisions() {
        assertEquals(2, solver.solve("What is -12 divided by 2 divided by -3?"));
    }
    
    @Test
    public void testExponentsFifth() {
    	assertEquals(32, solver.solve("What is 2 raised to the 5th power?"));
    }
    
    @Test
    public void testExponentsFirst() {
    	assertEquals(2, solver.solve("What is 2 raised to the 1st power?"));
    }
    
    @Test
    public void testExponentsSecond() {
    	assertEquals(4, solver.solve("What is 2 raised to the 2nd power?"));
    }
    
    @Test
    public void testExponentsThird() {
    	assertEquals(8, solver.solve("What is 2 raised to the 3rd power?"));
    }
    
    @Test
    public void testExponentsPowerOptional() {
    	assertEquals(32, solver.solve("What is 2 raised to the 5th?"));
    }
    
    @Test
    public void testUnknownOperation() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is 52 cubed?");
    }

    @Test
    public void testNonMathQuestion() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        // See https://en.wikipedia.org/wiki/President_of_the_United_States if you really need to know!
        solver.solve("Who is the President of the United States?");
    }

    @Test
    public void testMissingAnOperand() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is 1 plus?");
    }

    @Test
    public void testNoOperandsOrOperators() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is?");
    }

    @Test
    public void testTwoOperationsInARow() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is 1 plus plus 2?");
    }

    @Test
    public void testTwoNumbersAfterOperation() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is 1 plus 2 1?");
    }

    @Test
    public void testPostfixNotation() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is 1 2 plus?");
    }

    @Test
    public void testPrefixNotation() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is plus 1 2?");
    }
    
    @Test
    public void testWrongOrdinal() {
        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("I'm sorry, I don't understand the question!");

        solver.solve("What is 2 raised to the 5xx power?");
    }
}

Community comments

Find this solution interesting? Ask the author a question to learn more.
Avatar of ksnortum

I included raising to a power in my calculations so I created some more tests.

ksnortum's Reflection

I know I how a big monolithic solve() method, but I wasn't able to refactor it successfully.