From e7650f4092b4ab30c3526e8dc12fa954015b9813 Mon Sep 17 00:00:00 2001 From: codejava Date: Mon, 13 Apr 2026 10:54:52 +0300 Subject: [PATCH] Upload files to "java/expression/exceptions" --- .../expression/exceptions/ExceptionsTest.java | 30 +++ .../exceptions/ExceptionsTestSet.java | 162 +++++++++++++++ .../exceptions/ExceptionsTester.java | 100 +++++++++ .../exceptions/ExpressionParser.java | 193 ++++++++++++++++++ java/expression/exceptions/ListParser.java | 13 ++ 5 files changed, 498 insertions(+) create mode 100644 java/expression/exceptions/ExceptionsTest.java create mode 100644 java/expression/exceptions/ExceptionsTestSet.java create mode 100644 java/expression/exceptions/ExceptionsTester.java create mode 100644 java/expression/exceptions/ExpressionParser.java create mode 100644 java/expression/exceptions/ListParser.java diff --git a/java/expression/exceptions/ExceptionsTest.java b/java/expression/exceptions/ExceptionsTest.java new file mode 100644 index 0000000..851f635 --- /dev/null +++ b/java/expression/exceptions/ExceptionsTest.java @@ -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", POW, LOG) + .variant("3839", POW, LOG, POW_2, LOG_2) + .variant("3435", POW_2, LOG_2) + .variant("3233", HIGH, LOW) + .selector(); + + private ExceptionsTest() { + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/expression/exceptions/ExceptionsTestSet.java b/java/expression/exceptions/ExceptionsTestSet.java new file mode 100644 index 0000000..f19baa5 --- /dev/null +++ b/java/expression/exceptions/ExceptionsTestSet.java @@ -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 extends ParserTestSet { + private static final int D = 5; + private static final List 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> parsingTest; + + public ExceptionsTestSet(final ExceptionsTester tester, final ParsedKind kind) { + super(tester, kind, false); + parsingTest = tester.parsingTest; + } + + private void testParsingErrors() { + counter.testForEach(parsingTest, op -> { + final List 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> variables = kind.kind().variables().generate(counter.random(), 3); + final List 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 names, final LongBinaryOperator f, final String op, final Object expression) { + final ExpressionKind 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 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, "" + 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 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 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 variables, final String message, final String input) { + counter.shouldFail(message, () -> kind.parse(input, variables)); + } +} diff --git a/java/expression/exceptions/ExceptionsTester.java b/java/expression/exceptions/ExceptionsTester.java new file mode 100644 index 0000000..df9048b --- /dev/null +++ b/java/expression/exceptions/ExceptionsTester.java @@ -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> 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 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); + } +} diff --git a/java/expression/exceptions/ExpressionParser.java b/java/expression/exceptions/ExpressionParser.java new file mode 100644 index 0000000..a287628 --- /dev/null +++ b/java/expression/exceptions/ExpressionParser.java @@ -0,0 +1,193 @@ +package expression.exceptions; + +import java.util.List; +import expression.*; + +/** + * @author Doschennikov Nikita (me@fymio.us) + */ +public class ExpressionParser implements ListParser { + + private String src; + private int pos; + private List variables; + + @Override + public ListExpression parse(String expression, List variables) { + 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 parseNumber(true); + return new CheckedNegate(parseUnary()); + } + + if (tryConsume("reverse")) return new Reverse(parseUnary()); + if (tryConsume("digits")) return new Digits(parseUnary()); + if (tryConsume("floor")) return new Floor(parseUnary()); + if (tryConsume("ceiling")) return new Ceiling(parseUnary()); + if (tryConsume("log₂") || tryConsume("log2")) return new Log2(parseUnary()); + if (tryConsume("pow₂") || tryConsume("pow2")) return new Pow2(parseUnary()); + if (tryConsume("low")) return new Low(parseUnary()); + if (tryConsume("high")) return new High(parseUnary()); + + return parsePrimary(); + } + + 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)); + } +} \ No newline at end of file diff --git a/java/expression/exceptions/ListParser.java b/java/expression/exceptions/ListParser.java new file mode 100644 index 0000000..2370790 --- /dev/null +++ b/java/expression/exceptions/ListParser.java @@ -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 variables) throws Exception; +}