initial commit

This commit is contained in:
2026-01-29 23:20:12 +05:00
parent fb1ce36970
commit 67357cf271
76 changed files with 7115 additions and 0 deletions

View 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", 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);
}
}

View 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));
}
}

View 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);
}
}

View 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;
}

View File

@@ -0,0 +1,8 @@
/**
* Tests for <a href="https://www.kgeorgiy.info/courses/prog-intro/homeworks.html#expressions-exceptions">Expression Error Handling</a> homework
* of <a href="https://www.kgeorgiy.info/courses/prog-intro/">Introduction to Programming</a> course.
*
*
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
package expression.exceptions;