Created
January 31, 2017 19:26
-
-
Save grossws/b37cef5f24489dd63ac58cce718c79c1 to your computer and use it in GitHub Desktop.
calc demo project
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() + ")"; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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