first commit
This commit is contained in:
29
java/expression/exceptions/CheckedAdd.java
Normal file
29
java/expression/exceptions/CheckedAdd.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package expression.exceptions;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
|
||||
import expression.*;
|
||||
|
||||
/**
|
||||
* @author Doschennikov Nikita (me@fymio.us)
|
||||
*/
|
||||
public class CheckedAdd extends AbstractBinaryOperation {
|
||||
public CheckedAdd(AbstractExpression l, AbstractExpression r) { super(l, r); }
|
||||
|
||||
@Override protected String getOperator() { return "+"; }
|
||||
@Override protected int getPriority() { return 1; }
|
||||
@Override protected boolean isRightAssoc() { return false; }
|
||||
|
||||
@Override
|
||||
protected int applyInt(int a, int b) {
|
||||
int result = a + b;
|
||||
if (((a ^ result) & (b ^ result)) < 0) {
|
||||
throw new OverflowException("addition");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override protected BigInteger applyBi(BigInteger a, BigInteger b) { return a.add(b); }
|
||||
@Override protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { return a.add(b); }
|
||||
}
|
||||
37
java/expression/exceptions/CheckedDivide.java
Normal file
37
java/expression/exceptions/CheckedDivide.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package expression.exceptions;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.MathContext;
|
||||
|
||||
import expression.*;
|
||||
|
||||
/**
|
||||
* @author Doschennikov Nikita (me@fymio.us)
|
||||
*/
|
||||
public class CheckedDivide extends AbstractBinaryOperation {
|
||||
public CheckedDivide(AbstractExpression l, AbstractExpression r) { super(l, r); }
|
||||
|
||||
@Override protected String getOperator() { return "/"; }
|
||||
@Override protected int getPriority() { return 2; }
|
||||
@Override protected boolean isRightAssoc() { return true; }
|
||||
|
||||
@Override
|
||||
protected int applyInt(int a, int b) {
|
||||
if (b == 0) throw new DivisionByZeroException();
|
||||
if (a == Integer.MIN_VALUE && b == -1) throw new OverflowException("division");
|
||||
return a / b;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger applyBi(BigInteger a, BigInteger b) {
|
||||
if (b.signum() == 0) throw new DivisionByZeroException();
|
||||
return a.divide(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigDecimal applyBd(BigDecimal a, BigDecimal b) {
|
||||
if (b.signum() == 0) throw new DivisionByZeroException();
|
||||
return a.divide(b, MathContext.DECIMAL128);
|
||||
}
|
||||
}
|
||||
32
java/expression/exceptions/CheckedMultiply.java
Normal file
32
java/expression/exceptions/CheckedMultiply.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package expression.exceptions;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
|
||||
import expression.*;
|
||||
|
||||
/**
|
||||
* @author Doschennikov Nikita (me@fymio.us)
|
||||
*/
|
||||
public class CheckedMultiply extends AbstractBinaryOperation {
|
||||
public CheckedMultiply(AbstractExpression l, AbstractExpression r) { super(l, r); }
|
||||
|
||||
@Override protected String getOperator() { return "*"; }
|
||||
@Override protected int getPriority() { return 2; }
|
||||
@Override protected boolean isRightAssoc() { return false; }
|
||||
|
||||
@Override
|
||||
protected int applyInt(int a, int b) {
|
||||
if (a == 0 || b == 0) return 0;
|
||||
if (a == Integer.MIN_VALUE && b == -1) throw new OverflowException("multiplication");
|
||||
if (b == Integer.MIN_VALUE && a == -1) throw new OverflowException("multiplication");
|
||||
int result = a * b;
|
||||
if (result / a != b) {
|
||||
throw new OverflowException("multiplication");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override protected BigInteger applyBi(BigInteger a, BigInteger b) { return a.multiply(b); }
|
||||
@Override protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { return a.multiply(b); }
|
||||
}
|
||||
44
java/expression/exceptions/CheckedNegate.java
Normal file
44
java/expression/exceptions/CheckedNegate.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package expression.exceptions;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
import expression.*;
|
||||
|
||||
/**
|
||||
* @author Doschennikov Nikita (me@fymio.us)
|
||||
*/
|
||||
public class CheckedNegate extends AbstractExpression {
|
||||
private final AbstractExpression operand;
|
||||
|
||||
public CheckedNegate(AbstractExpression operand) {
|
||||
this.operand = operand;
|
||||
}
|
||||
|
||||
private static int checkedNegate(int n) {
|
||||
if (n == Integer.MIN_VALUE) throw new OverflowException("negation");
|
||||
return -n;
|
||||
}
|
||||
|
||||
@Override public int evaluate(int x) { return checkedNegate(operand.evaluate(x)); }
|
||||
@Override public int evaluate(int x, int y, int z) { return checkedNegate(operand.evaluate(x,y,z)); }
|
||||
@Override public int evaluate(List<Integer> vars) { return checkedNegate(operand.evaluate(vars)); }
|
||||
@Override public BigInteger evaluateBi(List<BigInteger> vars) { return operand.evaluateBi(vars).negate(); }
|
||||
@Override public BigDecimal evaluateBd(List<BigDecimal> vars) { return operand.evaluateBd(vars).negate(); }
|
||||
|
||||
@Override public String toString() { return "-(" + operand + ")"; }
|
||||
@Override public String toMiniString() {
|
||||
if (operand instanceof AbstractBinaryOperation) return "-(" + operand.toMiniString() + ")";
|
||||
return "- " + operand.toMiniString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof CheckedNegate)) return false;
|
||||
return operand.equals(((CheckedNegate) obj).operand);
|
||||
}
|
||||
|
||||
@Override public int hashCode() { return -operand.hashCode(); }
|
||||
}
|
||||
31
java/expression/exceptions/CheckedSubtract.java
Normal file
31
java/expression/exceptions/CheckedSubtract.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package expression.exceptions;
|
||||
|
||||
import expression.AbstractBinaryOperation;
|
||||
import expression.AbstractExpression;
|
||||
import expression.OverflowException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* @author Doschennikov Nikita (me@fymio.us)
|
||||
*/
|
||||
public class CheckedSubtract extends AbstractBinaryOperation {
|
||||
public CheckedSubtract(AbstractExpression l, AbstractExpression r) { super(l, r); }
|
||||
|
||||
@Override protected String getOperator() { return "-"; }
|
||||
@Override protected int getPriority() { return 1; }
|
||||
@Override protected boolean isRightAssoc() { return true; }
|
||||
|
||||
@Override
|
||||
protected int applyInt(int a, int b) {
|
||||
int result = a - b;
|
||||
if (((a ^ b) & (a ^ result)) < 0) {
|
||||
throw new OverflowException("subtraction");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override protected BigInteger applyBi(BigInteger a, BigInteger b) { return a.subtract(b); }
|
||||
@Override protected BigDecimal applyBd(BigDecimal a, BigDecimal b) { return a.subtract(b); }
|
||||
}
|
||||
30
java/expression/exceptions/ExceptionsTest.java
Normal file
30
java/expression/exceptions/ExceptionsTest.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package expression.exceptions;
|
||||
|
||||
import base.Selector;
|
||||
import expression.ListExpression;
|
||||
import expression.parser.Operations;
|
||||
|
||||
import static expression.parser.Operations.*;
|
||||
|
||||
/**
|
||||
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
|
||||
*/
|
||||
public final class ExceptionsTest {
|
||||
private static final ExpressionParser PARSER = new ExpressionParser();
|
||||
private static final Operations.Operation LIST = kind(ListExpression.KIND, PARSER::parse);
|
||||
|
||||
public static final Selector SELECTOR = Selector.composite(ExceptionsTest.class, ExceptionsTester::new, "easy", "hard")
|
||||
.variant("Base", LIST, ADD, SUBTRACT, MULTIPLY, DIVIDE, NEGATE)
|
||||
.variant("3637", ABS, CUBE, CBRT)
|
||||
.variant("3839", ABS, CUBE, CBRT, SQUARE, SQRT)
|
||||
.variant("3435", ABS, SQRT)
|
||||
.variant("3233", ABS, CBRT)
|
||||
.selector();
|
||||
|
||||
private ExceptionsTest() {
|
||||
}
|
||||
|
||||
public static void main(final String... args) {
|
||||
SELECTOR.main(args);
|
||||
}
|
||||
}
|
||||
162
java/expression/exceptions/ExceptionsTestSet.java
Normal file
162
java/expression/exceptions/ExceptionsTestSet.java
Normal file
@@ -0,0 +1,162 @@
|
||||
package expression.exceptions;
|
||||
|
||||
import base.Functional;
|
||||
import base.Named;
|
||||
import base.Pair;
|
||||
import expression.ToMiniString;
|
||||
import expression.Variable;
|
||||
import expression.common.ExpressionKind;
|
||||
import expression.parser.ParserTestSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.LongBinaryOperator;
|
||||
|
||||
/**
|
||||
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
|
||||
*/
|
||||
public class ExceptionsTestSet<E extends ToMiniString, C> extends ParserTestSet<E, C> {
|
||||
private static final int D = 5;
|
||||
private static final List<Integer> OVERFLOW_VALUES = new ArrayList<>();
|
||||
private final char[] CHARS = "AZ+-*%()[]<>".toCharArray();
|
||||
|
||||
static {
|
||||
Functional.addRange(OVERFLOW_VALUES, D, Integer.MIN_VALUE + D);
|
||||
Functional.addRange(OVERFLOW_VALUES, D, Integer.MIN_VALUE / 2);
|
||||
Functional.addRange(OVERFLOW_VALUES, D, (int) -Math.sqrt(Integer.MAX_VALUE));
|
||||
Functional.addRange(OVERFLOW_VALUES, D, 0);
|
||||
Functional.addRange(OVERFLOW_VALUES, D, (int) Math.sqrt(Integer.MAX_VALUE));
|
||||
Functional.addRange(OVERFLOW_VALUES, D, Integer.MAX_VALUE / 2);
|
||||
Functional.addRange(OVERFLOW_VALUES, D, Integer.MAX_VALUE - D);
|
||||
}
|
||||
|
||||
private final List<Named<String>> parsingTest;
|
||||
|
||||
public ExceptionsTestSet(final ExceptionsTester tester, final ParsedKind<E, C> kind) {
|
||||
super(tester, kind, false);
|
||||
parsingTest = tester.parsingTest;
|
||||
}
|
||||
|
||||
private void testParsingErrors() {
|
||||
counter.testForEach(parsingTest, op -> {
|
||||
final List<String> names = Functional.map(kind.kind().variables().generate(counter.random(), 3), Pair::first);
|
||||
final String expr = mangle(op.value(), names);
|
||||
try {
|
||||
kind.parse(expr, names);
|
||||
counter.fail("Successfully parsed '%s'", op.value());
|
||||
} catch (final Exception e) {
|
||||
counter.format("%-30s %s%n", op.name(), e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void testOverflow() {
|
||||
final List<Pair<String, E>> variables = kind.kind().variables().generate(counter.random(), 3);
|
||||
final List<String> names = Functional.map(variables, Pair::first);
|
||||
final Variable vx = (Variable) variables.get(0).second();
|
||||
final Variable vy = (Variable) variables.get(1).second();
|
||||
|
||||
//noinspection Convert2MethodRef
|
||||
testOverflow(names, (a, b) -> a + b, "+", new CheckedAdd(vx, vy));
|
||||
testOverflow(names, (a, b) -> a - b, "-", new CheckedSubtract(vx, vy));
|
||||
testOverflow(names, (a, b) -> a * b, "*", new CheckedMultiply(vx, vy));
|
||||
testOverflow(names, (a, b) -> b == 0 ? Long.MAX_VALUE : a / b, "/", new CheckedDivide(vx, vy));
|
||||
testOverflow(names, (a, b) -> -b, "<- ignore first argument, unary -", new CheckedNegate(vy));
|
||||
}
|
||||
|
||||
private void testOverflow(final List<String> names, final LongBinaryOperator f, final String op, final Object expression) {
|
||||
final ExpressionKind<E, C> kind = this.kind.kind();
|
||||
for (final int a : OVERFLOW_VALUES) {
|
||||
for (final int b : OVERFLOW_VALUES) {
|
||||
final long expected = f.applyAsLong(a, b);
|
||||
final boolean isInt = Integer.MIN_VALUE <= expected && expected <= Integer.MAX_VALUE;
|
||||
try {
|
||||
final C actual = kind.evaluate(
|
||||
kind.cast(expression),
|
||||
names,
|
||||
kind.fromInts(List.of(a, b, 0))
|
||||
);
|
||||
counter.checkTrue(
|
||||
isInt && kind.fromInt((int) expected).equals(actual),
|
||||
"%d %s %d == %d", a, op, b, actual
|
||||
);
|
||||
} catch (final Exception e) {
|
||||
if (isInt) {
|
||||
counter.fail(e, "Unexpected error in %d %s %d", a, op, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void test() {
|
||||
counter.scope("Overflow tests", (Runnable) this::testOverflow);
|
||||
super.test();
|
||||
counter.scope("Parsing error tests", this::testParsingErrors);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected E parse(final String expression, final List<String> variables, final boolean reparse) {
|
||||
final String expr = expression.strip();
|
||||
if (expr.length() > 10) {
|
||||
for (final char ch : CHARS) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final int index = 1 + tester.random().nextInt(expr.length() - 2);
|
||||
int pi = index - 1;
|
||||
while (Character.isWhitespace(expr.charAt(pi))) {
|
||||
pi--;
|
||||
}
|
||||
int ni = index;
|
||||
while (Character.isWhitespace(expr.charAt(ni))) {
|
||||
ni++;
|
||||
}
|
||||
final char pc = expr.charAt(pi);
|
||||
final char nc = expr.charAt(ni);
|
||||
if (
|
||||
"-([{*∛√²³₂₃!‖⎵⎴⌊⌈=?".indexOf(nc) < 0 &&
|
||||
(!Character.isLetterOrDigit(pc) || !Character.isLetterOrDigit(ch)) &&
|
||||
nc != ch && pc != ch &&
|
||||
!Character.isLetterOrDigit(nc) && nc != '$'
|
||||
) {
|
||||
shouldFail(
|
||||
variables,
|
||||
"Parsing error expected for " + insert(expr, index, "<ERROR_INSERTED -->" + ch + "<-- ERROR_INSERTED>"),
|
||||
insert(expr, index, String.valueOf(ch))
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
parens(variables, expr, '[', ']');
|
||||
parens(variables, expr, '{', '}');
|
||||
}
|
||||
|
||||
return counter.testV(() -> counter.call("parse", () -> kind.parse(expr, variables)));
|
||||
}
|
||||
|
||||
private static String insert(final String expr, final int index, final String value) {
|
||||
return expr.substring(0, index) + value + expr.substring(index);
|
||||
}
|
||||
|
||||
private void parens(final List<String> variables, final String expr, final char open, final char close) {
|
||||
if (expr.indexOf(open) >= 0) {
|
||||
replaces(variables, expr, open, '(');
|
||||
replaces(variables, expr, close, ')');
|
||||
if (expr.indexOf('(') >= 0) {
|
||||
replaces(variables, expr, '(', open);
|
||||
replaces(variables, expr, ')', close);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void replaces(final List<String> variables, final String expr, final char what, final char by) {
|
||||
final String input = expr.replace(what, by);
|
||||
shouldFail(variables, "Unmatched parentheses: " + input, input);
|
||||
}
|
||||
|
||||
private void shouldFail(final List<String> variables, final String message, final String input) {
|
||||
counter.shouldFail(message, () -> kind.parse(input, variables));
|
||||
}
|
||||
}
|
||||
100
java/expression/exceptions/ExceptionsTester.java
Normal file
100
java/expression/exceptions/ExceptionsTester.java
Normal file
@@ -0,0 +1,100 @@
|
||||
package expression.exceptions;
|
||||
|
||||
import base.Named;
|
||||
import base.TestCounter;
|
||||
import expression.common.Reason;
|
||||
import expression.parser.ParserTestSet;
|
||||
import expression.parser.ParserTester;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.LongBinaryOperator;
|
||||
import java.util.function.LongToIntFunction;
|
||||
|
||||
/**
|
||||
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
|
||||
*/
|
||||
public class ExceptionsTester extends ParserTester {
|
||||
/* package-private */ final List<Named<String>> parsingTest = new ArrayList<>(List.of(
|
||||
Named.of("No first argument", "* $y * $z"),
|
||||
Named.of("No middle argument", "$x * * $z"),
|
||||
Named.of("No last argument", "$x * $y * "),
|
||||
Named.of("No first argument'", "1 + (* $y * $z) + 2"),
|
||||
Named.of("No middle argument'", "1 + ($x * / 9) + 3"),
|
||||
Named.of("No last argument'", "1 + ($x * $y - ) + 3"),
|
||||
Named.of("No opening parenthesis", "$x * $y)"),
|
||||
Named.of("No closing parenthesis", "($x * $y"),
|
||||
Named.of("Mismatched closing parenthesis", "($x * $y]"),
|
||||
Named.of("Mismatched open parenthesis", "[$x * $y)"),
|
||||
Named.of("Start symbol", "@$x * $y"),
|
||||
Named.of("Middle symbol", "$x @ * $y"),
|
||||
Named.of("End symbol", "$x * $y@"),
|
||||
Named.of("Constant overflow 1", Integer.MIN_VALUE - 1L + ""),
|
||||
Named.of("Constant overflow 2", Integer.MAX_VALUE + 1L + ""),
|
||||
Named.of("Bare +", "+"),
|
||||
Named.of("Bare -", "-"),
|
||||
Named.of("Bare a", "a"),
|
||||
Named.of("(())", "(())"),
|
||||
Named.of("Spaces in numbers", "10 20")
|
||||
));
|
||||
|
||||
public ExceptionsTester(final TestCounter counter) {
|
||||
super(counter);
|
||||
}
|
||||
|
||||
|
||||
private void parsingTests(final String... tests) {
|
||||
for (final String test : tests) {
|
||||
parsingTest.add(Named.of(test, test));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unary(final String name, final int priority, final BiFunction<Long, LongToIntFunction, Long> op) {
|
||||
if (allowed(name)) {
|
||||
parsingTests(name, "1 * " + name, name + " * 1");
|
||||
}
|
||||
parsingTests(name + "()", name + "(1, 2)");
|
||||
if (name.length() > 1) {
|
||||
parsingTests(name + "q");
|
||||
}
|
||||
if (allLetterAndDigit(name)) {
|
||||
parsingTests(name + "1", name + "q");
|
||||
}
|
||||
super.unary(name, priority, op);
|
||||
}
|
||||
|
||||
private static boolean allowed(final String name) {
|
||||
return !"xyz".contains(name.substring(0, 1)) && !"xyz".contains(name.substring(name.length() - 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void binary(final String name, final int priority, final LongBinaryOperator op) {
|
||||
if (allowed(name)) {
|
||||
parsingTests(name);
|
||||
}
|
||||
parsingTests("1 " + name, "1 " + name + " * 3");
|
||||
if (!"-".equals(name)) {
|
||||
parsingTests(name + " 1", "1 * " + name + " 2");
|
||||
}
|
||||
if (allLetterAndDigit(name)) {
|
||||
parsingTests("5" + name + "5", "5 " + name + "5", "5 " + name + "5 5", "1" + name + "x 1", "1 " + name + "x 1");
|
||||
}
|
||||
super.binary(name, priority, op);
|
||||
}
|
||||
|
||||
private static boolean allLetterAndDigit(final String name) {
|
||||
return name.chars().allMatch(Character::isLetterOrDigit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void test(final ParserTestSet.ParsedKind<?, ?> kind) {
|
||||
new ExceptionsTestSet<>(this, kind).test();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int cast(final long value) {
|
||||
return Reason.overflow(value);
|
||||
}
|
||||
}
|
||||
215
java/expression/exceptions/ExpressionParser.java
Normal file
215
java/expression/exceptions/ExpressionParser.java
Normal file
@@ -0,0 +1,215 @@
|
||||
package expression.exceptions;
|
||||
|
||||
import expression.*;
|
||||
|
||||
/**
|
||||
* @author Doschennikov Nikita (me@fymio.us)
|
||||
*/
|
||||
public class ExpressionParser implements ListParser {
|
||||
|
||||
private String src;
|
||||
private int pos;
|
||||
private java.util.List<String> variables;
|
||||
|
||||
@Override
|
||||
public ListExpression parse(String expression, java.util.List<String> variables) {
|
||||
// System.err.println("PARSE: [" + expression + "]");
|
||||
this.src = expression;
|
||||
this.pos = 0;
|
||||
this.variables = variables;
|
||||
AbstractExpression result = parseMinMax();
|
||||
skipWhitespace();
|
||||
if (pos < src.length()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected character '" + src.charAt(pos) + "' at position " + pos);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private AbstractExpression parseMinMax() {
|
||||
AbstractExpression left = parseAddSub();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (tryConsume("min")) left = new Min(left, parseAddSub());
|
||||
else if (tryConsume("max")) left = new Max(left, parseAddSub());
|
||||
else if (tryConsume("set")) left = new SetBit(left, parseAddSub());
|
||||
else if (tryConsume("clear")) left = new Clear(left, parseAddSub());
|
||||
else break;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private AbstractExpression parseAddSub() {
|
||||
AbstractExpression left = parseMulDiv();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (pos < src.length() && src.charAt(pos) == '+') {
|
||||
pos++;
|
||||
left = new CheckedAdd(left, parseMulDiv());
|
||||
} else if (pos < src.length() && src.charAt(pos) == '-') {
|
||||
pos++;
|
||||
left = new CheckedSubtract(left, parseMulDiv());
|
||||
} else break;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private AbstractExpression parseMulDiv() {
|
||||
AbstractExpression left = parsePower();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (pos < src.length() && src.charAt(pos) == '*' && nextCharIs('*')) {
|
||||
pos++;
|
||||
left = new CheckedMultiply(left, parsePower());
|
||||
} else if (pos < src.length() && src.charAt(pos) == '/' && nextCharIs('/')) {
|
||||
pos++;
|
||||
left = new CheckedDivide(left, parsePower());
|
||||
} else break;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private AbstractExpression parsePower() {
|
||||
AbstractExpression left = parseUnary();
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (pos + 1 < src.length() && src.charAt(pos) == '*' && src.charAt(pos + 1) == '*') {
|
||||
pos += 2;
|
||||
left = new Power(left, parseUnary());
|
||||
} else if (pos + 1 < src.length() && src.charAt(pos) == '/' && src.charAt(pos + 1) == '/') {
|
||||
pos += 2;
|
||||
left = new Log(left, parseUnary());
|
||||
} else break;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private AbstractExpression parseUnary() {
|
||||
skipWhitespace();
|
||||
if (pos >= src.length())
|
||||
throw new IllegalArgumentException("Unexpected end of expression at position " + pos);
|
||||
|
||||
if (src.charAt(pos) == '-') {
|
||||
pos++;
|
||||
if (pos < src.length() && Character.isDigit(src.charAt(pos))) return parsePostfix(parseNumber(true));
|
||||
return new CheckedNegate(parseUnary());
|
||||
}
|
||||
|
||||
// abs: ||x|| (U+2016 DOUBLE VERTICAL LINE)
|
||||
if (src.charAt(pos) == '‖') {
|
||||
pos++;
|
||||
AbstractExpression inner = parseMinMax();
|
||||
skipWhitespace();
|
||||
if (pos >= src.length() || src.charAt(pos) != '‖')
|
||||
throw new IllegalArgumentException("Expected ‖ at position " + pos);
|
||||
pos++;
|
||||
return parsePostfix(new Abs(inner));
|
||||
}
|
||||
|
||||
// prefix unary: sqrt U+221A, cbrt U+221B
|
||||
if (src.charAt(pos) == '√') { pos++; return parsePostfix(new Sqrt(parseUnary())); }
|
||||
if (src.charAt(pos) == '∛') { pos++; return parsePostfix(new Cbrt(parseUnary())); }
|
||||
|
||||
if (tryConsume("reverse")) return parsePostfix(new Reverse(parseUnary()));
|
||||
if (tryConsume("digits")) return parsePostfix(new Digits(parseUnary()));
|
||||
if (tryConsume("floor")) return parsePostfix(new Floor(parseUnary()));
|
||||
if (tryConsume("ceiling")) return parsePostfix(new Ceiling(parseUnary()));
|
||||
if (tryConsume("log₂") || tryConsume("log2")) return parsePostfix(new Log2(parseUnary()));
|
||||
if (tryConsume("pow₂") || tryConsume("pow2")) return parsePostfix(new Pow2(parseUnary()));
|
||||
if (tryConsume("low")) return parsePostfix(new Low(parseUnary()));
|
||||
if (tryConsume("high")) return parsePostfix(new High(parseUnary()));
|
||||
|
||||
return parsePostfix(parsePrimary());
|
||||
}
|
||||
|
||||
private AbstractExpression parsePostfix(AbstractExpression base) {
|
||||
skipWhitespace();
|
||||
if (pos < src.length() && src.charAt(pos) == '²') { pos++; return parsePostfix(new Square(base)); }
|
||||
if (pos < src.length() && src.charAt(pos) == '³') { pos++; return parsePostfix(new Cube(base)); }
|
||||
return base;
|
||||
}
|
||||
|
||||
private AbstractExpression parsePrimary() {
|
||||
skipWhitespace();
|
||||
if (pos >= src.length()) throw new IllegalArgumentException("Unexpected end of expression");
|
||||
char c = src.charAt(pos);
|
||||
|
||||
if (c == '(') {
|
||||
pos++;
|
||||
AbstractExpression inner = parseMinMax();
|
||||
skipWhitespace();
|
||||
expect();
|
||||
return inner;
|
||||
}
|
||||
if (c == '$') { pos++; return new Variable(parseIndex()); }
|
||||
if (Character.isDigit(c)) return parseNumber(false);
|
||||
|
||||
if (Character.isLetter(c)) {
|
||||
int start = pos;
|
||||
while (pos < src.length() && (Character.isLetterOrDigit(src.charAt(pos)) || src.charAt(pos) == '_'))
|
||||
pos++;
|
||||
String name = src.substring(start, pos);
|
||||
int idx = variables != null ? variables.indexOf(name) : -1;
|
||||
if (idx >= 0) return new Variable(idx, name);
|
||||
throw new IllegalArgumentException("Unknown identifier '" + name + "' at position " + start);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unexpected character '" + c + "' at position " + pos);
|
||||
}
|
||||
|
||||
private void skipWhitespace() {
|
||||
while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) pos++;
|
||||
}
|
||||
|
||||
private boolean nextCharIs(char next) {
|
||||
return pos + 1 >= src.length() || src.charAt(pos + 1) != next;
|
||||
}
|
||||
|
||||
private boolean tryConsume(String keyword) {
|
||||
skipWhitespace();
|
||||
if (!src.startsWith(keyword, pos)) return false;
|
||||
int end = pos + keyword.length();
|
||||
if (end < src.length()) {
|
||||
char next = src.charAt(end);
|
||||
if (Character.isLetterOrDigit(next) || next == '_') return false;
|
||||
}
|
||||
if (variables != null && variables.contains(keyword)) return false;
|
||||
pos = end;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void expect() {
|
||||
if (pos >= src.length() || src.charAt(pos) != ')')
|
||||
throw new IllegalArgumentException("Expected '" + ')' + "' at position " + pos
|
||||
+ (pos < src.length() ? ", got '" + src.charAt(pos) + "'" : ", got end of input"));
|
||||
pos++;
|
||||
}
|
||||
|
||||
private AbstractExpression parseNumber(boolean negative) {
|
||||
int start = pos;
|
||||
while (pos < src.length() && Character.isDigit(src.charAt(pos))) pos++;
|
||||
if (start == pos) throw new IllegalArgumentException("Expected digit at position " + pos);
|
||||
String numStr = src.substring(start, pos);
|
||||
int result = 0;
|
||||
for (int i = 0; i < numStr.length(); i++) {
|
||||
int digit = numStr.charAt(i) - '0';
|
||||
if (!negative) {
|
||||
if (result > (Integer.MAX_VALUE - digit) / 10)
|
||||
throw new OverflowException("constant " + numStr);
|
||||
result = result * 10 + digit;
|
||||
} else {
|
||||
if (result < (Integer.MIN_VALUE + digit) / 10)
|
||||
throw new OverflowException("constant -" + numStr);
|
||||
result = result * 10 - digit;
|
||||
}
|
||||
}
|
||||
return new Const(result);
|
||||
}
|
||||
|
||||
private int parseIndex() {
|
||||
int start = pos;
|
||||
while (pos < src.length() && Character.isDigit(src.charAt(pos))) pos++;
|
||||
if (start == pos) throw new IllegalArgumentException("Expected digit after '$' at position " + pos);
|
||||
return Integer.parseInt(src.substring(start, pos));
|
||||
}
|
||||
}
|
||||
13
java/expression/exceptions/ListParser.java
Normal file
13
java/expression/exceptions/ListParser.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package expression.exceptions;
|
||||
|
||||
import expression.ListExpression;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ListParser {
|
||||
ListExpression parse(String expression, final List<String> variables) throws Exception;
|
||||
}
|
||||
8
java/expression/exceptions/package-info.java
Normal file
8
java/expression/exceptions/package-info.java
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Tests for <a href="https://www.kgeorgiy.info/courses/paradigms/homeworks.html#expressions-exceptions">Expression Error Handling</a> homework
|
||||
* of <a href="https://www.kgeorgiy.info/courses/paradigms/">Paradigms of Programming</a> course.
|
||||
*
|
||||
*
|
||||
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
|
||||
*/
|
||||
package expression.exceptions;
|
||||
Reference in New Issue
Block a user