Skip to content

Instantly share code, notes, and snippets.

@grossws
Created January 31, 2017 19:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grossws/b37cef5f24489dd63ac58cce718c79c1 to your computer and use it in GitHub Desktop.
Save grossws/b37cef5f24489dd63ac58cce718c79c1 to your computer and use it in GitHub Desktop.
calc demo project
package test;
public class Calc {
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("usage: java -jar ...jar expr");
}
String expr = String.join(" ", args);
System.out.println(expr + " = " + compute(expr));
}
static double compute(String expr) {
return Parser.parse(Tokenizer.tokenize(expr)).value();
}
}
package test;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CalcTest {
private static final double DELTA = 0.001;
@Test
public void testTrivialExpr() {
assertEquals(0, Calc.compute("0"), DELTA);
assertEquals(0, Calc.compute("0.0"), DELTA);
assertEquals(0, Calc.compute("-0"), DELTA);
assertEquals(0, Calc.compute("-0.0"), DELTA);
assertEquals(1.7, Calc.compute("1.7000"), DELTA);
}
@Test
public void testSimpleExpr() {
assertEquals(1.2, Calc.compute("1+.2"), DELTA);
assertEquals(1.2, Calc.compute("2-0.8"), DELTA);
assertEquals(1.2, Calc.compute(".6*2"), DELTA);
assertEquals(1.2, Calc.compute("60/50.0000000001"), DELTA);
}
@Test
public void testUnaryExpr() {
assertEquals(-0.7, Calc.compute("-.7"), DELTA);
assertEquals(0.7, Calc.compute("+0.70"), DELTA);
}
}
package test;
import java.util.Collections;
import java.util.List;
public class Parser {
public static Expr parse(List<Tokenizer.Token> tokens) {
if (tokens.size() == 0) {
throw new IllegalStateException();
}
Expr left = null;
Tokenizer.Token current = tokens.get(0);
if (current instanceof Tokenizer.Number) {
left = new NumberExpr((Tokenizer.Number) current);
} else if (current instanceof Tokenizer.Operator) {
Tokenizer.Operator unaryOp = (Tokenizer.Operator) current;
if (unaryOp.unary() && tokens.size() > 1) {
left = new UnaryOpExpr(unaryOp, parse(Collections.singletonList(tokens.get(1))));
}
} else {
throw new IllegalStateException();
}
// if unary op present skip 2 tokens
int offset = left instanceof UnaryOpExpr ? 2 : 1;
if (tokens.size() < offset + 1) {
return left;
}
if (!(tokens.get(offset) instanceof Tokenizer.Operator)) {
throw new IllegalStateException("operator expected");
}
Tokenizer.Operator binaryOp = (Tokenizer.Operator) tokens.get(offset);
return new BinaryOpExpr(left, binaryOp, parse(tokens.subList(offset + 1, tokens.size())));
}
interface Expr {
double value();
}
public static class NumberExpr implements Expr {
private final Tokenizer.Number number;
public NumberExpr(Tokenizer.Number number) {
this.number = number;
}
@Override
public double value() {
return number.value();
}
}
public static class UnaryOpExpr implements Expr {
private final Expr target;
private final Tokenizer.Operator op;
public UnaryOpExpr(Tokenizer.Operator op, Expr target) {
this.target = target;
this.op = op;
if (!op.unary()) {
throw new IllegalStateException();
}
}
@Override
public double value() {
switch (op) {
case Plus:
return target.value();
case Minus:
return -target.value();
default:
throw new UnsupportedOperationException();
}
}
}
public static class BinaryOpExpr implements Expr {
private final Expr left;
private final Expr right;
private final Tokenizer.Operator op;
public BinaryOpExpr(Expr left, Tokenizer.Operator op, Expr right) {
this.left = left;
this.op = op;
this.right = right;
}
@Override
public double value() {
switch (op) {
case Plus:
return left.value() + right.value();
case Minus:
return left.value() - right.value();
case Mul:
return left.value() * right.value();
case Div:
return left.value() / right.value();
default:
throw new UnsupportedOperationException();
}
}
}
}
package test;
import org.junit.Test;
import java.util.Collections;
import static org.junit.Assert.*;
public class ParserTest {
public static final double DELTA = 0.001;
@Test(expected = IllegalStateException.class)
public void testEmpty() {
Parser.parse(Collections.emptyList());
}
@Test
public void testNumber() {
Parser.Expr expr = Parser.parse(Collections.singletonList(new Tokenizer.Number(1.7)));
assertTrue(expr instanceof Parser.NumberExpr);
assertEquals(1.7, expr.value(), DELTA);
}
@Test
public void testUnaryExpr() {
Parser.Expr expr = Parser.parse(Tokenizer.tokenize("-1.7"));
assertTrue(expr instanceof Parser.UnaryOpExpr);
assertEquals(-1.7, expr.value(), DELTA);
}
@Test
public void testSimpleExpr() {
Parser.Expr expr = Parser.parse(Tokenizer.tokenize("1+2.7"));
assertTrue(expr instanceof Parser.BinaryOpExpr);
assertEquals(3.7, expr.value(), DELTA);
}
@Test
public void testWithUnaryExpr() {
Parser.Expr expr = Parser.parse(Tokenizer.tokenize("+1.7*-.2"));
assertTrue(expr instanceof Parser.BinaryOpExpr);
assertEquals(-0.34, expr.value(), DELTA);
}
@Test
public void testComplexExpr() {
Parser.Expr expr = Parser.parse(Tokenizer.tokenize("-0.2 + -0.3 *5.00"));
assertTrue(expr instanceof Parser.BinaryOpExpr);
assertEquals(-1.7, expr.value(), DELTA);
expr = Parser.parse(Tokenizer.tokenize("5. * -0.3 - 0.2"));
assertTrue(expr instanceof Parser.BinaryOpExpr);
assertEquals(-2.5, expr.value(), DELTA);
}
}
<?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>test</groupId>
<artifactId>calc</artifactId>
<version>0.1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifestEntries>
<mainClass>test.Calc</mainClass>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
package test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Tokenizer {
public static List<Token> tokenize(String expr) {
TokenizerState state = TokenizerState.Initial;
List<Token> tokens = new ArrayList<>();
int spanStart = 0;
for (int pos = 0; pos < expr.length(); pos++) {
char current = expr.charAt(pos);
switch (state) {
case Initial:
if (Character.isWhitespace(current)) {
// skip
} else if (Operator.operators.containsKey(current)) {
emitOperator(tokens, current);
} else if (Character.isDigit(current) || current == '.') {
state = TokenizerState.Number;
spanStart = pos;
} else {
throw new IllegalStateException("unknown token at " + pos + ": " + expr.substring(pos));
}
break;
case Number:
if (!Character.isDigit(current) && current != '.') {
state = TokenizerState.Initial;
emitNumber(tokens, expr.substring(spanStart, pos));
pos--;
}
break;
default:
throw new IllegalStateException("unknown token at " + pos + ": " + expr.substring(pos));
}
}
// finalize state on EOS
switch (state) {
case Number:
emitNumber(tokens, expr.substring(spanStart));
break;
case Initial:
default:
// do nothing
}
return tokens;
}
private static void emitNumber(List<Token> tokens, String subExpr) {
if (subExpr.indexOf('.') != subExpr.lastIndexOf('.')) {
throw new IllegalStateException("only one point is allowed in number " + subExpr);
} else if (".".equals(subExpr)) {
throw new IllegalStateException("only point ins't a number");
}
tokens.add(new Number(Double.valueOf(subExpr)));
}
private static void emitOperator(List<Token> tokens, char op) {
tokens.add(Operator.operators.get(op));
}
enum TokenizerState {
Initial,
Number
}
interface Token {
}
enum Operator implements Token {
Plus('+'),
Minus('-'),
Mul('*'),
Div('/');
private final char op;
Operator(char op) {
this.op = op;
}
public static final Map<Character, Operator> operators = Arrays.stream(Operator.values()).collect(Collectors.toMap(x -> x.op, Function.identity()));
public boolean unary() {
return this == Plus || this == Minus;
}
@Override
public String toString() {
return "Op(" + op + ")";
}
}
static class Number implements Token {
private final double value;
Number(double value) {
this.value = value;
}
public double value() {
return value;
}
@Override
public String toString() {
return "Number(" + value() + ")";
}
}
}
package test;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class TokenizerTest {
public static final double DELTA = 0.001;
@Test
public void testNumberTokenize() {
List<Tokenizer.Token> tokens = Tokenizer.tokenize(" 123.7");
assertEquals(1, tokens.size());
assertTrue(tokens.get(0) instanceof Tokenizer.Number);
Tokenizer.Number number = (Tokenizer.Number) tokens.get(0);
assertEquals(number.value(), 123.7, DELTA);
}
@Test
public void testOperatorTokenize() {
List<Tokenizer.Token> tokens = Tokenizer.tokenize("+ -*/");
assertEquals(4, tokens.size());
assertTrue(tokens.stream().allMatch(x -> x instanceof Tokenizer.Operator));
assertEquals(Tokenizer.Operator.Plus, tokens.get(0));
assertEquals(Tokenizer.Operator.Minus, tokens.get(1));
assertEquals(Tokenizer.Operator.Mul, tokens.get(2));
assertEquals(Tokenizer.Operator.Div, tokens.get(3));
}
@Test
public void testSimpleTokenize() {
List<Tokenizer.Token> tokens = Tokenizer.tokenize("1+2.7");
assertEquals(3, tokens.size());
assertTrue(tokens.get(0) instanceof Tokenizer.Number);
assertTrue(tokens.get(1) instanceof Tokenizer.Operator);
assertTrue(tokens.get(2) instanceof Tokenizer.Number);
}
@Test
public void testMixedTokenize() {
List<Tokenizer.Token> tokens = Tokenizer.tokenize("123.6 + 77");
assertEquals(3, tokens.size());
assertTrue(tokens.get(0) instanceof Tokenizer.Number);
assertTrue(tokens.get(1) instanceof Tokenizer.Operator);
assertTrue(tokens.get(2) instanceof Tokenizer.Number);
}
@Test(expected = IllegalStateException.class)
public void testInvalidToken() {
Tokenizer.tokenize("123.7 ? x");
}
@Test(expected = IllegalStateException.class)
public void testDoublePoint() {
Tokenizer.tokenize("123.7.2");
}
@Test(expected = IllegalStateException.class)
public void testJustPoint() {
Tokenizer.tokenize("+ .");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment