From c19005cfc09a2c89a4bb30ef29987668deebb01e Mon Sep 17 00:00:00 2001 From: codejava Date: Mon, 13 Apr 2026 10:52:14 +0300 Subject: [PATCH] Upload files to "java/expression/parser" --- java/expression/parser/ExpressionParser.java | 247 +++++++++++++++++++ java/expression/parser/ListParser.java | 12 + java/expression/parser/Operations.java | 232 +++++++++++++++++ java/expression/parser/ParserTest.java | 39 +++ java/expression/parser/package-info.java | 7 + 5 files changed, 537 insertions(+) create mode 100644 java/expression/parser/ExpressionParser.java create mode 100644 java/expression/parser/ListParser.java create mode 100644 java/expression/parser/Operations.java create mode 100644 java/expression/parser/ParserTest.java create mode 100644 java/expression/parser/package-info.java diff --git a/java/expression/parser/ExpressionParser.java b/java/expression/parser/ExpressionParser.java new file mode 100644 index 0000000..a59cb45 --- /dev/null +++ b/java/expression/parser/ExpressionParser.java @@ -0,0 +1,247 @@ +package expression.parser; + +import expression.*; +import java.util.List; + +/** + * @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 Add(left, parseMulDiv()); + } else if ( + pos < src.length() && + src.charAt(pos) == '-' && + !isUnaryContext() + ) { + pos++; + left = new Subtract(left, parseMulDiv()); + } else { + break; + } + } + return left; + } + + private AbstractExpression parseMulDiv() { + AbstractExpression left = parseUnary(); + while (true) { + skipWhitespace(); + if (pos < src.length() && src.charAt(pos) == '*') { + pos++; + left = new Multiply(left, parseUnary()); + } else if (pos < src.length() && src.charAt(pos) == '/') { + pos++; + left = new Divide(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 Negate(parseUnary()); + } + + if (tryConsume("reverse")) { + requireSeparatorOrParen(); + return new Reverse(parseUnary()); + } + if (tryConsume("digits")) { + requireSeparatorOrParen(); + return new Digits(parseUnary()); + } + if (tryConsume("floor")) { + requireSeparatorOrParen(); + return new Floor(parseUnary()); + } + if (tryConsume("ceiling")) { + requireSeparatorOrParen(); + return new Ceiling(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.indexOf(name); + 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 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 requireSeparatorOrParen() { + skipWhitespace(); + } + + 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); + long val = Long.parseLong(numStr); + if (negative) val = -val; + if (val < Integer.MIN_VALUE || val > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Integer overflow: " + val); + } + return new Const((int) val); + } + + 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)); + } + + private boolean isUnaryContext() { + return false; + } +} diff --git a/java/expression/parser/ListParser.java b/java/expression/parser/ListParser.java new file mode 100644 index 0000000..500da80 --- /dev/null +++ b/java/expression/parser/ListParser.java @@ -0,0 +1,12 @@ +package expression.parser; + +import expression.ListExpression; +import java.util.List; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@FunctionalInterface +public interface ListParser { + ListExpression parse(String expression, List variables); +} diff --git a/java/expression/parser/Operations.java b/java/expression/parser/Operations.java new file mode 100644 index 0000000..2fbec57 --- /dev/null +++ b/java/expression/parser/Operations.java @@ -0,0 +1,232 @@ +package expression.parser; + +import expression.ToMiniString; +import expression.common.ExpressionKind; +import expression.common.Reason; +import java.math.BigInteger; +import java.util.function.*; +import java.util.stream.LongStream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class Operations { + + // === Base + + public static final Operation NEGATE = unary("-", 1, a -> -a); + + @SuppressWarnings("Convert2MethodRef") + public static final Operation ADD = binary("+", 1600, (a, b) -> a + b); + + public static final Operation SUBTRACT = binary("-", 1602, (a, b) -> a - b); + public static final Operation MULTIPLY = binary("*", 2001, (a, b) -> a * b); + public static final Operation DIVIDE = binary("/", 2002, (a, b) -> + b == 0 ? Reason.DBZ.error() : a / b + ); + + // === MinMax + public static final Operation MIN = binary("min", 401, Math::min); + public static final Operation MAX = binary("max", 401, Math::max); + + // === Reverse + + private static Operation digits( + final String name, + final boolean mask, + final int r, + final LongBinaryOperator q + ) { + return unary(name, 1, v -> + LongStream.iterate( + mask ? v & 0xffff_ffffL : v, + n -> n != 0, + n -> n / r + ) + .map(n -> n % r) + .reduce(0, q) + ); + } + + public static final Operation REVERSE = digits( + "reverse", + false, + 10, + (a, b) -> a * 10 + b + ); + + // === Digits + public static final Operation DIGITS = digits( + "digits", + false, + 10, + Long::sum + ); + + // === Floor and Ceiling + + private static long floor(final long a) { + return ( + ((a >= 0 ? a : a - FLOOR_CEILING_STEP + 1) / FLOOR_CEILING_STEP) * + FLOOR_CEILING_STEP + ); + } + + private static long ceiling(final long a) { + return ( + ((a >= 0 ? a + FLOOR_CEILING_STEP - 1 : a) / FLOOR_CEILING_STEP) * + FLOOR_CEILING_STEP + ); + } + + public static final int FLOOR_CEILING_STEP = 1000; + public static final Operation FLOOR = unary("floor", 1, Operations::floor); + public static final Operation CEILING = unary( + "ceiling", + 1, + Operations::ceiling + ); + + // === Set, Clear + + @SuppressWarnings("IntegerMultiplicationImplicitCastToLong") + public static final Operation SET = binary( + "set", + 202, + (a, b) -> a | (1 << b) + ); + + @SuppressWarnings("IntegerMultiplicationImplicitCastToLong") + public static final Operation CLEAR = binary( + "clear", + 202, + (a, b) -> a & ~(1 << b) + ); + + // === Pow, Log + public static final Operation POW_O = binary("**", 2402, (a, b) -> + b < 0 + ? 1 + : BigInteger.valueOf(a) + .modPow(BigInteger.valueOf(b), BigInteger.valueOf(1L << 32)) + .intValue() + ); + public static final Operation LOG_O = binary("//", 2402, (a, b) -> + a == 0 && b > 0 + ? Integer.MIN_VALUE + : a <= 0 || b <= 0 || (a == 1 && b == 1) + ? 0 + : a > 1 && b == 1 + ? Integer.MAX_VALUE + : LongStream.iterate(b, v -> v <= a, v -> v * b).count() + ); + + private static final Reason INVALID_POW = new Reason("Invalid power"); + public static final Operation POW = binary("**", 2402, Operations::powC); + + private static long powC(final long a, final long b) { + if (b < 0 || (a == 0 && b == 0)) { + return INVALID_POW.error(); + } + if (Math.abs(a) > 1 && b > 32) { + return Reason.OVERFLOW.error(); + } + final BigInteger result = BigInteger.valueOf(a).pow((int) b); + if ( + result.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0 || + BigInteger.valueOf(Integer.MAX_VALUE).compareTo(result) < 0 + ) { + return Reason.OVERFLOW.error(); + } + return result.intValue(); + } + + private static final Reason INVALID_LOG = new Reason("Invalid log"); + public static final Operation LOG = binary("//", 2402, (a, b) -> + a <= 0 || b <= 1 + ? INVALID_LOG.error() + : (int) (Math.log(a) / Math.log(b)) + ); + + // Pow2, Log2 + + private static final Reason NEG_LOG = new Reason( + "Logarithm of negative value" + ); + public static final Operation LOG_2 = unary( + "log₂", + 1, + NEG_LOG.less(1, a -> (long) (Math.log(a) / Math.log(2))) + ); + + private static final Reason NEG_POW = new Reason( + "Exponentiation to negative power" + ); + public static final Operation POW_2 = unary( + "pow₂", + 1, + NEG_POW.less(0, Reason.OVERFLOW.greater(31, a -> (long) Math.pow(2, a))) + ); + + // === High, Low + public static final Operation HIGH = unary("high", 1, v -> + Integer.highestOneBit((int) v) + ); + public static final Operation LOW = unary("low", 1, v -> + Integer.lowestOneBit((int) v) + ); + + // === Common + + private Operations() {} + + public static Operation unary( + final String name, + final int priority, + final LongUnaryOperator op + ) { + return unary(name, priority, (a, c) -> op.applyAsLong(a)); + } + + public static Operation unary( + final String left, + final String right, + final LongUnaryOperator op + ) { + return unary(left, right, (a, c) -> op.applyAsLong(a)); + } + + public static Operation unary( + final String name, + final int priority, + final BiFunction op + ) { + return tests -> tests.unary(name, priority, op); + } + + public static Operation unary( + final String left, + final String right, + final BiFunction op + ) { + return tests -> tests.unary(left, right, op); + } + + public static Operation binary( + final String name, + final int priority, + final LongBinaryOperator op + ) { + return tests -> tests.binary(name, priority, op); + } + + public static Operation kind( + final ExpressionKind kind, + final ParserTestSet.Parser parser + ) { + return factory -> factory.kind(kind, parser); + } + + @FunctionalInterface + public interface Operation extends Consumer {} +} diff --git a/java/expression/parser/ParserTest.java b/java/expression/parser/ParserTest.java new file mode 100644 index 0000000..e1fe230 --- /dev/null +++ b/java/expression/parser/ParserTest.java @@ -0,0 +1,39 @@ +package expression.parser; + +import static expression.parser.Operations.*; + +import base.Selector; +import expression.ListExpression; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class ParserTest { + + private static final ExpressionParser PARSER = new ExpressionParser(); + private static final Operations.Operation LIST = kind( + ListExpression.KIND, + PARSER::parse + ); + + // === Common + + public static final Selector SELECTOR = Selector.composite( + ParserTest.class, + ParserTester::new, + "easy", + "hard" + ) + .variant("Base", LIST, ADD, SUBTRACT, MULTIPLY, DIVIDE, NEGATE) + .variant("3637", MIN, MAX, REVERSE) + .variant("3839", MIN, MAX, REVERSE, DIGITS) + .variant("3435", FLOOR, CEILING, SET, CLEAR) + .variant("3233", FLOOR, CEILING) + .selector(); + + private ParserTest() {} + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/expression/parser/package-info.java b/java/expression/parser/package-info.java new file mode 100644 index 0000000..b2d8935 --- /dev/null +++ b/java/expression/parser/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Expressions Parsing homework + * of Introduction to Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package expression.parser;