Skip to content

Commit

Permalink
add expression evaluator
Browse files Browse the repository at this point in the history
  • Loading branch information
iryndin committed Apr 6, 2017
1 parent 1861c16 commit 968c080
Show file tree
Hide file tree
Showing 10 changed files with 452 additions and 0 deletions.
30 changes: 30 additions & 0 deletions expreval/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>net.iryndin</groupId>
<artifactId>expreval</artifactId>
<version>1.0</version>

<name>Simple arithmetic expression evaluator</name>

<properties>
<project.build.source>UTF-8</project.build.source>

<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.fork>true</maven.compiler.fork>

<junit.version>4.12</junit.version>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
16 changes: 16 additions & 0 deletions expreval/src/main/java/net/iryndin/expreval/EvalException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.iryndin.expreval;

/**
* @author iryndin
* @since 06/04/17
*/
public class EvalException extends Exception{

public EvalException(String s) {
super(s);
}

public EvalException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package net.iryndin.expreval;

import java.util.EmptyStackException;
import java.util.Stack;

/**
* @author iryndin
* @since 06/04/17
*/
public class ExpressionEvaluator {

private final ITokenEmitter tokenEmitter;
private Stack<Token> stack = new Stack<>();

public ExpressionEvaluator(ITokenEmitter tokenEmitter) {
this.tokenEmitter = tokenEmitter;
}

public long eval() throws EvalException {
Token nextToken;

while ((nextToken = tokenEmitter.emit()) != null) {
if (nextToken.isOperation()) {
if (stack.isEmpty()) {
throw new EvalException("No first operand for operation: '" + nextToken + "'");
}
if (stack.peek().isOperation()) {
throw new EvalException("Two operations in a row: '"
+ stack.peek() + "' and '" + nextToken + "'");
}
} else {
if (!stack.isEmpty() && !stack.peek().isOperation()) {
throw new EvalException("Two numbers in a row: '"
+ stack.peek() + "' and '" + nextToken + "'");
}
}
if (nextToken.getType() == Token.TYPE_ADD) {
evalStack();
}
stack.push(nextToken);
}
evalStack();
Token result = stack.pop();
if (result.isOperation()) {
throw new EvalException("Smth goes wrong!");
}
return result.getValue();
}

private void evalStack() throws EvalException {
while (!stack.isEmpty()) {
Token operand2 = stack.pop();
if (stack.isEmpty()) {
stack.push(operand2);
break;
} else {
try {
Token operation = stack.pop();
Token operand1 = stack.pop();
if (!operation.isOperation() || operand1.isOperation() || operand2.isOperation()) {
throw new EvalException("Wrong!");
}
switch (operation.getType()) {
case Token.TYPE_ADD: {
long res = operand1.getValue() + operand2.getValue();
stack.push(Token.createNumber(res));
break;
}
case Token.TYPE_MULTIPLY: {
long res = operand1.getValue() * operand2.getValue();
stack.push(Token.createNumber(res));
break;
}
}
} catch (EmptyStackException ese) {
throw new EvalException("No operation or operand1", ese);
}
}
}
}

public static void main(String[] args) throws EvalException {
StringTokenEmitter ste = new StringTokenEmitter("1+2*4*6+9");
ExpressionEvaluator ee = new ExpressionEvaluator(ste);
System.out.println(ee.eval());
}

}

/*
1+2*5*6*7*8*9+5*6*7*8=
5*4 +3 +3 +5 +6*8*7*9*90
number - no tokens ?
1 2 5 6 7 8 9 5 6 7 8
+ * * * * * + * * *
*/
20 changes: 20 additions & 0 deletions expreval/src/main/java/net/iryndin/expreval/ITokenEmitter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package net.iryndin.expreval;

/**
* Emit tokens. Can emit tokens from string, or from infinite incoming stream.
*
* @author iryndin
* @since 06/04/17
*/
public interface ITokenEmitter {
/**
* Emit tokens in the order they income to this emitter.
* Token may be an integer number, or "+" or "*".
* Return null when tokens are ended.
* The call to this method may block current thread until new token comes or end of list/stream of tokens is reached.
*
* @return next emitted token or null when no tokens anymore
* @throws EvalException when evaluation error occurs
*/
Token emit() throws EvalException;
}
12 changes: 12 additions & 0 deletions expreval/src/main/java/net/iryndin/expreval/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package net.iryndin.expreval;

/**
* @author iryndin
* @since 06/04/17
*/
public class Main {
public static void main(String[] args) throws EvalException {
ExpressionEvaluator ee = new ExpressionEvaluator(new StringTokenEmitter("1 +2*3+7"));
System.out.println(ee.eval());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package net.iryndin.expreval;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
* Token emitter which takes tokens from a string expression
*
* @author iryndin
* @since 06/04/17
*/
public class StringTokenEmitter implements ITokenEmitter {

private final List<String> list;
private Iterator<String> iter;

public StringTokenEmitter(String expression) {
this.list = parseExpression(expression);
this.iter = list.iterator();
}

static List<String> parseExpression(String expression) {
List<String> result = new ArrayList<>();
StringBuilder sb = new StringBuilder(16);
for (char c : expression.toCharArray()) {
if (Character.isWhitespace(c)) {
if (sb.length() > 0) {
result.add(sb.toString());
sb.setLength(0);
}
} else if (c == '+') {
if (sb.length() > 0) {
result.add(sb.toString());
sb.setLength(0);
}
result.add("+");
} else if (c == '*') {
if (sb.length() > 0) {
result.add(sb.toString());
sb.setLength(0);
}
result.add("*");
} else if (Character.isDigit(c)) {
sb.append(c);
}
}
if (sb.length() > 0) {
result.add(sb.toString());
sb.setLength(0);
}
return result;
}

@Override
public String toString() {
return "StringTokenEmitter{" +
"list=" + list +
'}';
}

@Override
public Token emit() throws EvalException {
if (iter.hasNext()) {
return Token.fromString(iter.next());
}
return null;
}

public static void main(String[] args) {
StringTokenEmitter e = new StringTokenEmitter(" 1 + 2* 5* 6* 7*8 *9+ \t5*6*\t7*8+6+7+8*7");
System.out.println(e);
}
}
95 changes: 95 additions & 0 deletions expreval/src/main/java/net/iryndin/expreval/Token.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package net.iryndin.expreval;

import static java.lang.String.format;

/**
* Token class.
* Token can be either integer positive number, or "+" operation, or "*" operation.
*
* @author iryndin
* @since 06/04/17
*/
public class Token {

public static final int TYPE_NUMBER = 1;
public static final int TYPE_ADD = 2;
public static final int TYPE_MULTIPLY = 3;

private final long value;
private final int type;

private Token(long value, int type) {
this.value = value;
this.type = type;
}

public long getValue() {
return value;
}

public int getType() {
return type;
}

public boolean isOperation() {
return type == TYPE_ADD || type == TYPE_MULTIPLY;
}

@Override
public String toString() {
switch (type) {
case TYPE_ADD:
return "+";
case TYPE_MULTIPLY:
return "*";
default:
return Long.toString(value);
}
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Token token = (Token) o;

if (value != token.value) return false;
return type == token.type;

}

@Override
public int hashCode() {
int result = (int) (value ^ (value >>> 32));
result = 31 * result + type;
return result;
}

public static Token createNumber(long n) {
return new Token(n, TYPE_NUMBER);
}

public static Token createAddOperation() {
return new Token(0, TYPE_ADD);
}

public static Token createMultiplyOperation() {
return new Token(0, TYPE_MULTIPLY);
}

public static Token fromString(String s) throws EvalException {
s = s.trim();
if (s.equals("*")) {
return createMultiplyOperation();
} else if (s.equals("+")) {
return createAddOperation();
} else {
try {
return createNumber(Long.parseLong(s));
} catch (NumberFormatException nfe) {
throw new EvalException(format("Cannot evaluate token '%s'", s), nfe);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package net.iryndin.expreval;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

/**
* @author iryndin
* @since 06/04/17
*/
public class ExpressionEvaluatorTest {

@Test
public void testEval() throws EvalException {
ExpressionEvaluator ee = new ExpressionEvaluator(new StringTokenEmitter("1+2*3+7*8+1+2+3"));
assertEquals(1+2*3+7*8+1+2+3, ee.eval());
}

@Test(expected = EvalException.class)
public void testEvalFail1() throws EvalException {
new ExpressionEvaluator(new StringTokenEmitter("+1")).eval();
}

@Test(expected = EvalException.class)
public void testEvalFail2() throws EvalException {
new ExpressionEvaluator(new StringTokenEmitter("1 * ")).eval();
}

@Test(expected = EvalException.class)
public void testEvalFail3() throws EvalException {
new ExpressionEvaluator(new StringTokenEmitter("1 2")).eval();
}

@Test(expected = EvalException.class)
public void testEvalFail4() throws EvalException {
new ExpressionEvaluator(new StringTokenEmitter("1+2 3*")).eval();
}
}
Loading

0 comments on commit 968c080

Please sign in to comment.